| #!/usr/bin/env python3 |
| # |
| # Copyright (c) 2024 STMicroelectronics |
| # SPDX-License-Identifier: Apache-2.0 |
| |
| """ |
| Script to prepare the LLEXT exports table of a Zephyr ELF |
| |
| This script performs compile-time processing of the LLEXT exports |
| table for usage at runtime by the LLEXT subsystem code. The table |
| is a special section filled with 'llext_const_symbol' structures |
| generated by the EXPORT_SYMBOL macro. |
| |
| Currently, the preparatory work consists mostly of sorting the |
| exports table to allow usage of binary search algorithms at runtime. |
| If CONFIG_LLEXT_EXPORT_BUILTINS_BY_SLID option is enabled, SLIDs |
| of all exported functions are also injected in the export table by |
| this script. (In this case, the preparation process is destructive) |
| """ |
| |
| import llext_slidlib |
| |
| from elftools.elf.elffile import ELFFile |
| from elftools.elf.sections import Section |
| |
| import argparse |
| import logging |
| import pathlib |
| import struct |
| import sys |
| |
| #!!!!! WARNING !!!!! |
| # |
| #These constants MUST be kept in sync with the linker scripts |
| #and the EXPORT_SYMBOL macro located in 'subsys/llext/llext.h'. |
| #Otherwise, the LLEXT subsystem will be broken! |
| # |
| #!!!!! WARNING !!!!! |
| |
| LLEXT_EXPORT_TABLE_SECTION_NAME = "llext_const_symbol_area" |
| LLEXT_EXPORT_NAMES_SECTION_NAME = "llext_exports_strtab" |
| |
| def _llext_const_symbol_struct(ptr_size: int, endianness: str): |
| """ |
| ptr_size -- Platform pointer size in bytes |
| endianness -- Platform endianness ('little'/'big') |
| """ |
| endspec = "<" if endianness == 'little' else ">" |
| if ptr_size == 4: |
| ptrspec = "I" |
| elif ptr_size == 8: |
| ptrspec = "Q" |
| |
| # struct llext_const_symbol |
| # contains just two pointers. |
| lcs_spec = endspec + 2 * ptrspec |
| return struct.Struct(lcs_spec) |
| |
| #ELF Shdr flag applied to the export table section, to indicate |
| #the section has already been prepared by this script. This is |
| #mostly a security measure to prevent the script from running |
| #twice on the same ELF file, which can result in catastrophic |
| #failures if SLID-based linking is enabled (in this case, the |
| #preparation process is destructive). |
| # |
| #This flag is part of the SHF_MASKOS mask, of which all bits |
| #are "reserved for operating system-specific semantics". |
| #See: https://refspecs.linuxbase.org/elf/gabi4+/ch4.sheader.html |
| SHF_LLEXT_PREPARATION_DONE = 0x08000000 |
| |
| class SectionDescriptor(): |
| """ELF Section descriptor |
| |
| This is a wrapper class around pyelftools' "Section" object. |
| """ |
| def __init__(self, elffile, section_name): |
| self.name = section_name |
| self.section = elffile.get_section_by_name(section_name) |
| if not isinstance(self.section, Section): |
| raise KeyError(f"section {section_name} not found") |
| |
| self.shdr_index = elffile.get_section_index(section_name) |
| self.shdr_offset = elffile['e_shoff'] + \ |
| self.shdr_index * elffile['e_shentsize'] |
| self.size = self.section['sh_size'] |
| self.flags = self.section['sh_flags'] |
| self.offset = self.section['sh_offset'] |
| |
| class LLEXTExptabManipulator(): |
| """Class used to wrap the LLEXT export table manipulation.""" |
| def __init__(self, elf_fd, exptab_file_offset, lcs_struct, exports_count): |
| self.fd = elf_fd |
| self.exports_count = exports_count |
| self.base_offset = exptab_file_offset |
| self.lcs_struct = lcs_struct |
| |
| def _seek_to_sym(self, index): |
| self.fd.seek(self.base_offset + index * self.lcs_struct.size) |
| |
| def __getitem__(self, index): |
| if not isinstance(index, int): |
| raise TypeError(f"invalid type {type(index)} for index") |
| |
| if index >= self.exports_count: |
| raise IndexError(f"index {index} is out of bounds (max {self.exports_count})") |
| |
| self._seek_to_sym(index) |
| return self.lcs_struct.unpack(self.fd.read(self.lcs_struct.size)) |
| |
| def __setitem__(self, index, item): |
| if not isinstance(index, int): |
| raise TypeError(f"invalid type {type(index)} for index") |
| |
| if index >= self.exports_count: |
| raise IndexError(f"index {index} is out of bounds (max {self.exports_count})") |
| |
| (addr_or_slid, sym_addr) = item |
| |
| self._seek_to_sym(index) |
| self.fd.write(self.lcs_struct.pack(addr_or_slid, sym_addr)) |
| |
| class ZephyrElfExptabPreparator(): |
| """Prepares the LLEXT export table of a Zephyr ELF. |
| |
| Attributes: |
| elf_path: path to the Zephyr ELF to prepare |
| log: a logging.Logger object |
| slid_listing_path: path to the file where SLID listing should be saved |
| """ |
| def __init__(self, elf_path: str, log: logging.Logger, slid_listing_path: str | None): |
| self.elf_path = elf_path |
| self.elf_fd = open(self.elf_path, 'rb+') |
| self.elf = ELFFile(self.elf_fd) |
| self.log = log |
| |
| # Lazy-open the SLID listing file to ensure it is only created when necessary |
| self.slid_listing_path = slid_listing_path |
| self.slid_listing_fd = None |
| |
| def _prepare_exptab_for_slid_linking(self): |
| """ |
| IMPLEMENTATION NOTES: |
| In the linker script, we declare the export names table |
| as starting at address 0. Thanks to this, all "pointers" |
| to that section are equal to the offset inside the section. |
| Also note that symbol names are always NUL-terminated. |
| |
| The export table is sorted by SLID in ASCENDING order. |
| """ |
| def read_symbol_name(name_ptr): |
| raw_name = b'' |
| self.elf_fd.seek(self.expstrtab_section.offset + name_ptr) |
| |
| c = self.elf_fd.read(1) |
| while c != b'\0': |
| raw_name += c |
| c = self.elf_fd.read(1) |
| |
| return raw_name.decode("utf-8") |
| |
| #1) Load the export table |
| exports_list = [] |
| for (name_ptr, export_address) in self.exptab_manipulator: |
| export_name = read_symbol_name(name_ptr) |
| exports_list.append((export_name, export_address)) |
| |
| #2) Generate the SLID for all exports |
| collided = False |
| sorted_exptab = dict() |
| for export_name, export_addr in exports_list: |
| slid = llext_slidlib.generate_slid(export_name, self.ptrsize) |
| |
| collision = sorted_exptab.get(slid) |
| if collision: |
| #Don't abort immediately on collision: if there are others, we want to log them all. |
| self.log.error(f"SLID collision: {export_name} and {collision[0]} have the same SLID 0x{slid:X}") |
| collided = True |
| else: |
| sorted_exptab[slid] = (export_name, export_addr) |
| |
| if collided: |
| return 1 |
| |
| #3) Sort the export table (order specified above) |
| sorted_exptab = dict(sorted(sorted_exptab.items())) |
| |
| #4) Write the updated export table to ELF, and dump |
| #to SLID listing if requested by caller |
| if self.slid_listing_path: |
| self.slid_listing_fd = open(self.slid_listing_path, "w") |
| |
| def slidlist_write(msg): |
| if self.slid_listing_fd: |
| self.slid_listing_fd.write(msg + "\n") |
| |
| slidlist_write(f"/* SLID listing generated by {__file__} */") |
| slidlist_write("//") |
| slidlist_write("// This file contains the 'SLID -> name' mapping for all") |
| slidlist_write("// symbols exported to LLEXT by this Zephyr executable.") |
| slidlist_write("") |
| |
| self.log.info("SLID -> export name mapping:") |
| |
| i = 0 |
| for (slid, name_and_symaddr) in sorted_exptab.items(): |
| slid_as_str = llext_slidlib.format_slid(slid, self.ptrsize) |
| msg = f"{slid_as_str} -> {name_and_symaddr[0]}" |
| self.log.info(msg) |
| slidlist_write(msg) |
| |
| self.exptab_manipulator[i] = (slid, name_and_symaddr[1]) |
| i += 1 |
| |
| if self.slid_listing_fd: |
| self.slid_listing_fd.close() |
| |
| return 0 |
| |
| def _prepare_exptab_for_str_linking(self): |
| #TODO: sort the export table by symbol |
| # name to allow binary search too |
| # |
| # Plan of action: |
| # 1) Locate in which section the names are located |
| # 2) Load the export table and resolve names |
| # 3) Sort the exports by name |
| # WARN: THIS MUST USE THE SAME SORTING RULES |
| # AS LLEXT CODE OR DICHOTOMIC SEARCH WILL BREAK |
| # Using a custom sorting function might be required. |
| # 4) Write back the updated export table |
| # |
| # N.B.: reusing part of the code in _prepare_elf_for_slid_linking |
| # might be possible and desireable. |
| # |
| # As of writing, this function will never be called as this script |
| # is only called if CONFIG_LLEXT_EXPORT_BUILTINS_BY_SLID is enabled, |
| # which makes _prepare_exptab_for_slid_linking be called instead. |
| # |
| self.log.warn(f"_prepare_exptab_for_str_linking: do nothing") |
| return 0 |
| |
| def _set_prep_done_shdr_flag(self): |
| #Offset and size of the 'sh_flags' member of |
| #the Elf_Shdr structure. The offset does not |
| #change between ELF32 and ELF64. Size in both |
| #is equal to pointer size (4 bytes for ELF32, |
| #8 bytes for ELF64). |
| SHF_OFFSET = 8 |
| SHF_SIZE = self.ptrsize |
| |
| off = self.exptab_section.shdr_offset + SHF_OFFSET |
| |
| #Read existing sh_flags, set the PREPARATION_DONE flag |
| #and write back the new value. |
| self.elf_fd.seek(off) |
| sh_flags = int.from_bytes(self.elf_fd.read(SHF_SIZE), self.endianness) |
| |
| sh_flags |= SHF_LLEXT_PREPARATION_DONE |
| |
| self.elf_fd.seek(off) |
| self.elf_fd.write(int.to_bytes(sh_flags, self.ptrsize, self.endianness)) |
| |
| def _prepare_inner(self): |
| # Locate the export table section |
| try: |
| self.exptab_section = SectionDescriptor( |
| self.elf, LLEXT_EXPORT_TABLE_SECTION_NAME) |
| except KeyError as e: |
| self.log.error(e.args[0]) |
| return 1 |
| |
| # Abort if the ELF has already been processed |
| if (self.exptab_section.flags & SHF_LLEXT_PREPARATION_DONE) != 0: |
| self.log.warning("exptab section flagged with LLEXT_PREPARATION_DONE " |
| "- not preparing again") |
| return 0 |
| |
| # Get the struct.Struct for export table entry |
| self.ptrsize = self.elf.elfclass // 8 |
| self.endianness = 'little' if self.elf.little_endian else 'big' |
| self.lcs_struct = _llext_const_symbol_struct(self.ptrsize, self.endianness) |
| |
| # Verify that the export table size is coherent |
| if (self.exptab_section.size % self.lcs_struct.size) != 0: |
| self.log.error(f"export table size (0x{self.exptab_section.size:X}) " |
| f"not aligned to 'llext_const_symbol' size (0x{self.lcs_struct.size:X})") |
| return 1 |
| |
| # Create the export table manipulator |
| num_exports = self.exptab_section.size // self.lcs_struct.size |
| self.exptab_manipulator = LLEXTExptabManipulator( |
| self.elf_fd, self.exptab_section.offset, self.lcs_struct, num_exports) |
| |
| # Attempt to locate the export names section |
| try: |
| self.expstrtab_section = SectionDescriptor( |
| self.elf, LLEXT_EXPORT_NAMES_SECTION_NAME) |
| except KeyError: |
| self.expstrtab_section = None |
| |
| self.log.debug(f"exports table section at file offset 0x{self.exptab_section.offset:X}") |
| if self.expstrtab_section: |
| self.log.debug(f"exports strtab section at file offset 0x{self.expstrtab_section.offset:X}") |
| else: |
| self.log.debug("no exports strtab section in ELF") |
| self.log.info(f"{num_exports} symbols are exported to LLEXTs by this ELF") |
| |
| # Perform the export table preparation |
| if self.expstrtab_section: |
| res = self._prepare_exptab_for_slid_linking() |
| else: |
| res = self._prepare_exptab_for_str_linking() |
| |
| if res == 0: # Add the "prepared" flag to export table section |
| self._set_prep_done_shdr_flag() |
| |
| def prepare_elf(self): |
| res = self._prepare_inner() |
| self.elf_fd.close() |
| return res |
| |
| # pylint: disable=duplicate-code |
| def _parse_args(argv): |
| """Parse the command line arguments.""" |
| parser = argparse.ArgumentParser( |
| description=__doc__, |
| formatter_class=argparse.RawDescriptionHelpFormatter, |
| allow_abbrev=False) |
| |
| parser.add_argument("-f", "--elf-file", default=pathlib.Path("build", "zephyr", "zephyr.elf"), |
| help="ELF file to process") |
| parser.add_argument("-sl", "--slid-listing", |
| help=("write the SLID listing to a file (only useful" |
| "when CONFIG_LLEXT_EXPORT_BUILTINS_BY_SLID is enabled) ")) |
| parser.add_argument("-v", "--verbose", action="count", |
| help=("enable verbose output, can be used multiple times " |
| "to increase verbosity level")) |
| parser.add_argument("--always-succeed", action="store_true", |
| help="always exit with a return code of 0, used for testing") |
| |
| return parser.parse_args(argv) |
| |
| def _init_log(verbose): |
| """Initialize a logger object.""" |
| log = logging.getLogger(__file__) |
| |
| console = logging.StreamHandler() |
| console.setFormatter(logging.Formatter("%(levelname)s: %(message)s")) |
| log.addHandler(console) |
| |
| if verbose and verbose > 1: |
| log.setLevel(logging.DEBUG) |
| elif verbose and verbose > 0: |
| log.setLevel(logging.INFO) |
| else: |
| log.setLevel(logging.WARNING) |
| |
| return log |
| |
| def main(argv=None): |
| args = _parse_args(argv) |
| |
| log = _init_log(args.verbose) |
| |
| log.info(f"prepare_llext_exptab: {args.elf_file}") |
| |
| preparator = ZephyrElfExptabPreparator(args.elf_file, log, args.slid_listing) |
| |
| res = preparator.prepare_elf() |
| |
| if args.always_succeed: |
| return 0 |
| |
| return res |
| |
| if __name__ == "__main__": |
| sys.exit(main(sys.argv[1:])) |