378 lines
14 KiB
Python
Executable File
378 lines
14 KiB
Python
Executable File
#!/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:]))
|