diff --git a/scripts/llext_link_helper.py b/scripts/llext_link_helper.py index 3b6c2d451e7c..727234654334 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,159 @@ def max_alignment(addr, align1, align2): upper = addr + align1 - 1 return upper - (upper % align1) +# The Zephyr LLEXT loader merges the sections of an extension into regions by +# type: code, read-only data and writable data, and pads the start of each +# region down to the largest alignment of its sections. For relocatable +# objects it only moves the file offset of the region that way, not its +# address, so if the first section of a region isn't at a file offset with +# that alignment, addresses within that region are translated to wrong file +# offsets. Among other things the names of symbols, exported by the module, +# can't be found then. Return the sections, which have to be aligned in the +# file to avoid that, and their required alignment. +def region_alignment_fixes(path, detached): + regions = {} + + with open(path, 'rb') as f: + elf = ELFFile(f) + + for section in elf.iter_sections(): + s_flags = section.header['sh_flags'] + + if (not s_flags & SH_FLAGS.SHF_ALLOC or s_flags & SH_FLAGS.SHF_TLS or + not section.header['sh_size'] or section.name in detached): + continue + + if section.name == '.exported_sym': + region = 'export' + elif section.header['sh_type'] != 'SHT_PROGBITS': + continue + elif s_flags & SH_FLAGS.SHF_EXECINSTR: + region = 'text' + elif s_flags & SH_FLAGS.SHF_WRITE: + region = 'data' + else: + region = 'rodata' + + regions.setdefault(region, []).append((section.header['sh_offset'], + section.name, + section.header['sh_addralign'], + section.header['sh_addr'])) + + fixes = [] + + for sections in regions.values(): + offset, name, align, addr = min(sections) + region_align = max(s[2] for s in sections) + + if offset % region_align == 0: + continue + + if addr % region_align: + print(f'llext_link_helper.py: WARNING: cannot align {name} to {region_align}', + file=sys.stderr) + continue + + fixes.append((name, region_align)) + + return fixes + +# 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('