Skip to content

[WIP] Relocatable ELF Enablement - #3

Closed
onaffaa wants to merge 7 commits into
mainfrom
onaffaa/enable_pie_elf
Closed

onaffaa wants to merge 7 commits into
mainfrom
onaffaa/enable_pie_elf

Conversation

@onaffaa

@onaffaa onaffaa commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

Problem Description

Currently the behavior of Zephyr is to compile its ELF as position dependent, which results in the generated ELF needing to be loaded at the physical address the firmware was compiled against.

By adding support to compile position independent flags using the -fPIC and -pie flags several benefits are introduced:

  1. A PIE executable can be loaded from a different physical address since a fixed physical address is not assumed
  2. Improved support for relocatable images by allowing self-relocation to take place, which is beneficial as it removes reliance on statically compiling an ELF for a different address or requiring the bootloader to maintain logic to perform the relocation of the image

Proposed Change (Summary)

The high-level scope of changes to enable this functionality are:

  1. Add compilation / linking support by introducing a new feature flag, CONFIG_PIC_OPTIONS, that determines which compiler / linker flags are to be included.
  2. Update the linker script for RISC-V targets to address newly generated sections specific to PIE execution
  3. Perform self-relocation early in the boot-up process (i.e. just after reset)

Proposed Change (Detailed)

Adding PIE Compilation / Linking Support

A new feature flag CONFIG_PIC_OPTIONS would be introduced to support building PIE if enabled. If disabled (default), the current functionality that prevents using position independent compilation / linking remains.

  • If disabled the property no_position_independent is used, which adds the compiler properties -fno-pic and -fno-pie and --no-pie as a linker property

With CONFIG_PIC_OPTIONS=y new a property position_independent_elf that has the following effects:

  • Sets -fPIC as a property for compilation
  • Sets -Wl, -pie -Wl, --no-dynamic-linker as a property for linking

Note: The --no-dynamic-linker linker option prevents generation of the PT_INTERP program header and other metadata required for dynamic loading. As a result, the relocatable PIE image remains a static, self-contained ELF binary that performs any required relocations internally during startup.

Modify Linker Scripts to Handle PIE-specific Sections

At link-time several sections are generated for PIE images:

  • .dynsym: Dynamic symbol table containing symbols that may need to be referenced or resolved at load/runtime.
  • .dynstr: String table containing the names associated with entries in .dynsym and other dynamic-linking metadata.
  • .hash: Hash table used by the dynamic linker to efficiently locate symbols in .dynsym.
  • .plt: Procedure Linkage Table containing stubs used to call dynamically linked functions.
  • .got: Global Offset Table containing addresses used by position-independent code to access global data and external symbols.

Rather than resolve symbols dynamically at run-time early boot-up code will be added that resolves all symbols present in relocation sections. To identify this the location of the Global Offset Table (GOT) and relocation sections are determined by the symbols:

  1. __got_start, __got_end for the GOT section range
  2. __rela_dyn_start, __rela_dyn_end for the range that defines relocation sections

Performing Self Relocation

Relocation Process (Example)

With a PIE binary self relocation is done for global data that would otherwise reference a stale address if the ELF is executed from a different physical address than what was compiled against. PC-relative addressing is used to get the address of the GOT entry for some global symbol, which is then used to get the linking address of the global symbol. The image below shows a typical entry within the rela.dyn section.

The example below uses a GOT entry, but this is not the only r_offset type in rela.dyn; one other example would be const string pointers.

Image
  • The address representing the GOT entry (computed via auipc) is marked in red
  • The address representing the link-time address of the global symbol is marked in yellow
Procedure

Adjusting Entries in GOT

To determine the offset where the ELF is loaded from relative to its physical address logic will be added within the __start routine within vector.S. The logic below introduces a label - reloc_pc - whose runtime and link-time address are used to determine the offset used to adjust the entries in the GOT.

reloc_pc:
auipc t0, 0 /* Get the runtime address of this instruction */
la t1, reloc_pc /* Get the linker time address of this instruction */
sub t2, t0, t1

Once this offset is computed iterate through each entry stored in __rel_dyn_start to __rel_dyn_end, where each entry is 12 bytes composed of 3 4-byte values: r_offset, r_info, and r_addend, that have an r_info value of type R_RISCV_RELATIVE.

  • r_offset - the link-time address of the memory location that needs to be patched
  • r_info - packs the symbol index and relocation type
  • r_addend - the constant value used to compute the relocated address; adjusted by the load offset to get the actual runtime value that gets stored

Some rela.dyn entries aren't guaranteed to be 4-byte aligned (e.g. packed elements within a struct), which requires additional handling when writing the patched values (e.g. r_offset + t2) to perform half-word or single-byte stores to avoid a fault due to misalignment.

Interrupt Relocation

An additional update would be needed for gen_isr_defines.py, which parses and patches the .intList section to resolve addresses in _sw_isr_table. This is done in 2 steps:

  1. Update function pointer addresses representing the ISR to call - func member of _isr_list
  2. Update function parameter for non-direct IRQs - param member of _isr_list

Care must be taken when updating function parameters (point 2), as param values may represent a literal rather than an address. The example below illustrates this nuance:

Image
  • Entry 4 is a function pointer to isr_handler, which has a parameter 0x4 that is converted to an integer using POINTER_TO_INT
  • Entry 11 is a function pointer to plic_irq_handler, whose parameter is a pointer to a device struct that needs to be relocated

The symbol table (.symtab) is used to determine what interrupt relocations should occur at runtime and populates a list of interrupts to relocate. This table is consumed by a newly added z_adjust_isr_tables function that is invoked in z_cstart.

Image
  • Written into build/zephyr/isr_tables.c at compile-time
  • {11, 0} indicates that the parameter of ISR 11 was identified as needing to be relocated
  • {-1, -1} denotes the end of the list of interrupts to relocate

The second index (e.g. 0 in {11, 0}) is used when CONFIG_SHARED_INTERRUPTS are enabled to determine which client index requires it's arg to be relocated. If CONFIG_SHARED_INTERRUPTS the value is a "don't care".

Dependencies

GCC does not support the necessary -pie linking flag, which requires that this feature is compiled with LLVM.
Changes made were verified downstream using:

Additionally, CONFIG_QEMU_DEVICE_LOADER must be enabled if testing on QEMU as the dynamic sections are placed prior to rom_start.

Concerns and Unresolved Questions

Interactions with userspace, as kernel objects are added to a lookup table at compile time. Follow up work would be to address this similar to how interrupts are relocated, but for now CONFIG_PIC_OPTIONS would depend on userspace being disabled.

CONFIG_GEN_SW_ISR_TABLE_SWITCH is not supported with this feature, as interrupt relocation requires patching the pointers to each ISR at runtime. This feature generates C-code with literal addresses that can't be patched, unlike CONFIG_GEN_SW_ISR_TABLE_ARRAY

Alternatives Considered

  1. Mapping the physical address where the ELF resides to the desired virtual address. Infeasible because no MMU on hardware
  2. Perform relocation via bootloader, which requires additional loader logic / is less secure

Add a new option CONFIG_PIC_OPTIONS to enable pie compilation / linking.
Change compiler and linker flags when generating pie image.

Signed-off-by: Omar Naffaa <onaffaa@qti.qualcomm.com>
Update isr.S and pmp.S to use lla instead of la when accessing
local, file scoped labels. Using local addressing results in
unnecessary entries being placed in the GOT.

Signed-off-by: Omar Naffaa <onaffaa@qti.qualcomm.com>
At link-time with "-pie" enabled the following sections are added
into relocation sections within the ELF: __tdata_start, __tdata_end,
__tdata_size, __tbss_start, __tbss_end, and __tbss_size. At boot time
these variables are adjusted by the offset between the build time and
runtime address, resulting in an incorrect size. Compute size
explicitly as "end - start".

Signed-off-by: Omar Naffaa <onaffaa@qti.qualcomm.com>
At link-time with "-pie" enabled the following sections are added
into relocation sections within the ELF: __rom_region_start,
__rom_region_end, and __rom_region_size. At boot time these variables
are adjusted by the offset between the build time and runtime address,
resulting in an incorrect size. Compute size explicitly.

Signed-off-by: Omar Naffaa <onaffaa@qti.qualcomm.com>
Software ISR tables (isr_tables.c) are generated by reading symbol
values out of the pre-linked zephyr_pre0.elf, then baked directly into
the final image. For PIE builds those baked values are load-address-
relative, so they must be corrected once the image is actually placed
in memory at runtime.

gen_isr_tables.py/gen_isr_tables_parser_carrays.py now read the
.rela.dyn section and resolve the vector table, sw_isr_table, and
shared-interrupt-table symbols against zephyr_pre0.elf when
CONFIG_PIC_OPTIONS is set, emitting each table's pre0 load address plus
a per-entry relocation list. z_adjust_isr_tables() in kernel/init.c
uses this at startup to patch the tables to their final, relocated
addresses.

CONFIG_GEN_SW_ISR_TABLE_SWITCH and CONFIG_ISR_TABLES_LOCAL_DECLARATION
are not supported for relocatable images; arch/Kconfig now makes them
depend on !PIC_OPTIONS so the two schemes can't be selected together.

Signed-off-by: Omar Naffaa <onaffaa@qti.qualcomm.com>
Add the load-address delta computation and .rela.dyn walk needed to
self-relocate a statically-linked position independent executable at
reset, before any GOT/data access or ISR dispatch can occur. Only
R_RISCV_RELATIVE entries are handled, since a static PIE has no
dynamic linker and therefore no symbol-resolution relocations.

Adjust the RISC-V linker script to place .got/.rela.plt/.rela.dyn for
the PIC_OPTIONS build instead of the .plt/.iplt layout used by
non-PIE images, and discard the .plt/.dynamic input sections that GNU
ld synthesizes for -pie but that a no-dynamic-linker static PIE never
uses.

Signed-off-by: Omar Naffaa <onaffaa@qti.qualcomm.com>
Create version of hello world with CONFIG_PIC_OPTIONS enabled
to validate dynamic ELF functionality.

Signed-off-by: Omar Naffaa <onaffaa@qti.qualcomm.com>
@onaffaa onaffaa self-assigned this Oct 2, 2026
Comment on lines +170 to +175
SECTION_PROLOGUE(.rela.dyn,, ALIGN(0x1000))
{
__rel_dyn_start = .;
KEEP(*(.rela*))
}
__rel_dyn_end = .;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For whatever reason moving __rel_dyn_end inside the .rela.dyn section results in errors.

In our downstream it was left commented out:

Image

Comment on lines +45 to +48
reloc_pc:
auipc t0, 0 /* Get the runtime address of this instruction */
la t1, reloc_pc /* Get the linker time address of this instruction */
sub t2, t0, t1

@onaffaa onaffaa Oct 2, 2026 •

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modification of the following from zephyr/kernel/config/soc/sifive/common/vector.S made with the intention that 4k alignment is not strictly required. If the value is offset by some value less than 4k the lui t1, 0xFFFFF instruction masks the offset entirely (e.g. if offsetting by 5k).

The code below relies on the address of __start being close to the runtime address where the calculation is done, whereas defining reloc_pc allows the calculation to be done anywhere in this routine.

    /* Calculate the delta between linker time address and run time address */
    /* Get the runtime address */
    auipc t0, 0
    lui t1, 0xFFFFF     /* load 0xFFFFF000 into t1 */
    and t0, t0, t1
    /* Get the linker time address */
    la t1, __start
    /* get the delta between t1 and t0 */
    sub t2, t0, t1

Comment on lines +55 to +59
andi t5, t2, 0x3
beqz t5, delta_aligned
delta_misaligned:
j delta_misaligned
delta_aligned:

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This goes in hand with relaxing the requirement that the offset must be a multiple of 4K. This leads to an infinite loop to diagnose the fault instead of silently masking the offset if it doesn't meet alignment criteria.

It may be useful to make a variant of this ( andi t5, t2, 0xFFF ) instead of using lui t1, 0xFFFFF above even if 4k alignment is required if we want to fault on a non-4k alignment instead of silently rounding it down.

Comment on lines +51 to +52
Using ``llvm-objcopy`` from a CPULLVM toolchain install
(``<cpullvm-toolchain>/bin/llvm-objcopy``), shift the LMA of every section in

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be cleaned up to reference LLVM rather than CPULLVM.

Comment on lines +41 to +42
Testing Self-Relocation with a Shifted LMA
===========================================

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feedback appreciated on this idea for testing in upstream. It requires manually shifting the LMA so that the ELF is interpreted as relocated (in downstream we use T32 to load the ELF at an offset).

@onaffaa onaffaa closed this Oct 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant