Commit 7293f59a authored by Eric Ackermann's avatar Eric Ackermann Committed by Carles Cufi
Browse files

tests: llext: Test RISC-V non-paired relocation



Tests an edge case in the RISC-V PSABI: In the medany and the medlow code
models, the compiler emits auipc/lui (U-type) and ld/sw (I-type/S-type)
instruction pairs for accessing a non-local symbol.
The U-type instruction sets the upper 20 bits, the I/S-type the lower 12.
The U-type and I-type/S-type instruction pairs are often adjacent in code.
This is also what the current llext architecture-specific relocations
expect.
However, this need not be the case - compilers can re-use the upper 20
bits set by the U-type instruction with multiple I/S-type instructions,
which is a useful optimization for multiple loads/stores of or within
the same symbol.
This commit adds a unit test for this behavior, which currently fails
for RISC-V.

Signed-off-by: default avatarEric Ackermann <eric.ackermann@cispa.de>
parent 2255b638
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -91,3 +91,13 @@ if(NOT CONFIG_LLEXT_TYPE_ELF_OBJECT AND CONFIG_RISCV AND CONFIG_RISCV_ISA_EXT_C)
    ${ZEPHYR_BINARY_DIR}/include/generated/riscv_edge_case_cb_type.inc
  )
endif()

if(NOT CONFIG_LLEXT_TYPE_ELF_OBJECT AND CONFIG_RISCV)
  add_llext_target(riscv_edge_case_non_paired_hi20_lo12_ext
    OUTPUT  ${PROJECT_BINARY_DIR}/llext/riscv_edge_case_non_paired_hi20_lo12_ext.llext
    SOURCES ${PROJECT_SOURCE_DIR}/src/riscv_edge_case_non_paired_hi20_lo12.c ${PROJECT_SOURCE_DIR}/src/riscv_edge_case_non_paired_hi20_lo12_trigger.S
  )
  generate_inc_file_for_target(app ${PROJECT_BINARY_DIR}/llext/riscv_edge_case_non_paired_hi20_lo12_ext.llext
    ${ZEPHYR_BINARY_DIR}/include/generated/riscv_edge_case_non_paired_hi20_lo12.inc
  )
endif()
+42 −0
Original line number Diff line number Diff line
/*
 * Copyright (c) 2024 CISPA Helmholtz Center for Information Security
 *
 * SPDX-License-Identifier: Apache-2.0
 */

/*
 * This extension tests a relocation edge case in RISC-V:
 * U-type instructions in conjunction with I-type/S-type instructions can be used to
 * relocate symbols within a 32-bit range from the PC (medany code model) or 0
 * (medlow code model).
 * The compiler usually emits the U-type instructions and I-type/S-type instructions in sequence.
 * However, this is not guaranteed.
 * The accompanying assembly listing generates a scenario in which this assumption does NOT hold
 * and tests that the llext loader can handle it.
 */

#include <stdbool.h>
#include <zephyr/llext/symbol.h>
#include <zephyr/ztest_assert.h>

extern int _riscv_edge_case_non_paired_hi20_lo12(void);


#define DATA_SEGMENT_SYMBOL_INITIAL 21
#define DATA_SEGMENT_SYMBOL_EXPECTED (DATA_SEGMENT_SYMBOL_INITIAL+42)

/* changed from the assembly script */
volatile int _data_segment_symbol = DATA_SEGMENT_SYMBOL_INITIAL;


void test_entry(void)
{
	int ret_value;

	ret_value = _riscv_edge_case_non_paired_hi20_lo12();

	zassert_equal(ret_value, DATA_SEGMENT_SYMBOL_INITIAL);

	zassert_equal(_data_segment_symbol, DATA_SEGMENT_SYMBOL_EXPECTED);
}
EXPORT_SYMBOL(test_entry);
+54 −0
Original line number Diff line number Diff line
/*
 * Copyright (c) 2025 CISPA Helmholtz Center for Information Security
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include <zephyr/toolchain.h>

/* 32-bit global defined in C */
GDATA(_data_segment_symbol)

GTEXT(_riscv_edge_case_non_paired_hi20_lo12)

/*
 * Tests an edge case in the RISC-V PSABI: In the medany and the medlow code models,
 * the compiler emits auipc/lui (U-type) and ld/sw (I-type/S-type) instruction pairs
 * for accessing a non-local symbol.
 * The U-type instruction sets the upper 20 bits, the I/S-type the lower 12.
 * Thus, any address in a 32-bit range from 0 (medlow) / the PC (medany) can be reached.
 * Often, the U-type and I-type/S-type instruction pairs are adjacent in code.
 * However, this need not be the case - compilers can re-use the upper 20 bits set by
 * the U-type instruction with multiple I/S-type instructions, which is a useful optimization
 * for multiple loads/stores of or within the same symbol.
 * The U-type instruction can also appear after the I-type in code, e.g., due to control flow.
 * When the U-type and I/S-type instructions are not in sequence, this triggers an edge case
 * in the llext loader.
 * This test triggers this edge case by loading a global, modifying it and storing it back.
 */
SECTION_FUNC(TEXT, _riscv_edge_case_non_paired_hi20_lo12)
	/* jump beyond the I-type/load instruction initially to break sequence assumption */
	j _do_utype

_do_load:
	/* re-use the upper-bit value set by the U-type below for a load */
        lw    a0, %pcrel_lo(.LUtype)(a1)

	addi t1, a0, 42

	j _do_store

_do_utype:
	/* this u-type sets the higher 20 bits of the global */
	.LUtype:  auipc a1, %pcrel_hi(_data_segment_symbol)

	/* backwards jump to test loading */
	j _do_load

_do_store:

	/* write the modified value back for the C code to check */
	sw t1, %pcrel_lo(.LUtype)(a1)

	/* return a0, i.e., the value we read, for the C code to check */
	ret
+7 −0
Original line number Diff line number Diff line
@@ -396,6 +396,13 @@ static LLEXT_CONST uint8_t riscv_edge_case_cb_type_ext[] ELF_ALIGN = {
LLEXT_LOAD_UNLOAD(riscv_edge_case_cb_type)
#endif /* CONFIG_RISCV && CONFIG_RISCV_ISA_EXT_C */

#if defined(CONFIG_RISCV)
static LLEXT_CONST uint8_t riscv_edge_case_non_paired_hi20_lo12_ext[] ELF_ALIGN = {
	#include "riscv_edge_case_non_paired_hi20_lo12.inc"
};
LLEXT_LOAD_UNLOAD(riscv_edge_case_non_paired_hi20_lo12)
#endif /* CONFIG_RISCV */

#endif /* !CONFIG_LLEXT_TYPE_ELF_OBJECT */

#ifndef CONFIG_USERSPACE