diff --git a/scripts/llext_link_helper.py b/scripts/llext_link_helper.py index 3b6c2d451e7c..d8e1f64cbdb5 100755 --- a/scripts/llext_link_helper.py +++ b/scripts/llext_link_helper.py @@ -8,7 +8,9 @@ # line parameters to this script and call the linker directly. import os +import sys import argparse +import struct import subprocess from elftools.elf.elffile import ELFFile from elftools.elf.constants import SH_FLAGS @@ -48,6 +50,103 @@ def max_alignment(addr, align1, align2): upper = addr + align1 - 1 return upper - (upper % align1) +# A partial (-r) link keeps every per-function section and every C++ COMDAT +# group as a separate section. Below, all executable sections except .text +# are linked to run from DRAM, and sections only pulled in from libraries +# wouldn't be seen in the input object at all and would stay at address 0. +# So for relocatable modules first partially link all objects and libraries +# into a single object, dissolving section groups and merging sections of +# the same type, and then calculate addresses for that object. +# Returns the merged object and the linker parameters to use with it, or +# None if the linker doesn't support this. +def prelink_relocatable(): + merged = f'{args.file}.merged' + script = pathlib.Path(__file__).parent / 'llext_merge_sections.ld' + + command = [args.command] + args.params + ['-Wl,--force-group-allocation', + f'-Wl,-T,{script}', '-o', merged] + if subprocess.run(command).returncode != 0: + print('llext_link_helper.py: WARNING: pre-linking failed, ' + 'linking without merging sections', file=sys.stderr) + return None + + # All objects and libraries are in the merged object now, keep options + options = [p for p in args.params if p.startswith('-') and + not p.startswith(('-l', '-L'))] + + return merged, options + [merged] + +R_XTENSA_NONE = 0 +R_XTENSA_ASM_EXPAND = 11 +R_XTENSA_SLOT0_OP = 20 + +# Check if the Zephyr LLEXT loader would leave this instruction unchanged, +# see r_xtensa_slot0_op_relocate(): it only patches L32R and CALLn +# instructions with an all-zero operand field +def xtensa_slot0_resolved(insn): + b0, b1, b2 = insn + if b1 or b2: + return True + if (b0 & 0xf) == 1: + return False + return not ((b0 & 0xf) == 5 and not (b0 & 0xc0)) + +# Most relocations in a partially linked Xtensa object need no action at +# run time: R_XTENSA_NONE and R_XTENSA_ASM_EXPAND ones, and R_XTENSA_SLOT0_OP +# ones for PC-relative references within a section, which the linker has +# already resolved. Still the loader has to read each of them, together with +# its symbol and the instruction in the module image, which is in slow DRAM. +# Remove them from the relocation sections. +def prune_xtensa_relocs(path): + RELA_SIZE = 12 + + with open(path, 'rb') as f: + elf = ELFFile(f) + if elf['e_machine'] != 'EM_XTENSA' or elf['e_type'] != 'ET_REL': + return + + shoff = elf['e_shoff'] + shentsize = elf['e_shentsize'] + updates = [] + dropped = 0 + total = 0 + + for idx, section in enumerate(elf.iter_sections()): + if section['sh_type'] != 'SHT_RELA' or section['sh_entsize'] != RELA_SIZE: + continue + + target = elf.get_section(section['sh_info']) + data = target.data() if target['sh_type'] != 'SHT_NOBITS' else b'' + raw = section.data() + kept = bytearray() + + for i in range(0, len(raw), RELA_SIZE): + r_offset, r_info = struct.unpack_from('