llext: add support for SLID-based linking

This commit introduces support for an alternate linking method in the
LLEXT subsystem, called "SLID" (short for Symbol Link Identifier),
enabled by the CONFIG_LLEXT_EXPORT_BUILTINS_BY_SLID Kconfig option.

SLID-based linking uses a unique identifier (integer) to identify
exported symbols, instead of using the symbol name as done currently.
This approach provides several benefits:
 * linking is faster because the comparison operation to determine
   whether we found the correct symbol in the export table is now an
   integer compare, instead of a string compare
 * binary size is reduced as symbol names can be dropped from the binary
 * confidentiality is improved as a side-effect, as symbol names are no
   longer present in the binary

Signed-off-by: Mathieu Choplain <mathieu.choplain@st.com>
This commit is contained in:
Mathieu Choplain 2024-05-06 13:03:26 +02:00 committed by Anas Nashif
commit 8aa6ae43ce
45 changed files with 981 additions and 7 deletions

View file

@ -1888,6 +1888,21 @@ if(CONFIG_BUILD_OUTPUT_INFO_HEADER)
)
endif()
if (CONFIG_LLEXT AND CONFIG_LLEXT_EXPORT_BUILTINS_BY_SLID)
#slidgen must be the first post-build command to be executed
#on the Zephyr ELF to ensure that all other commands, such as
#binary file generation, are operating on a preparated ELF.
list(PREPEND
post_build_commands
COMMAND ${PYTHON_EXECUTABLE}
${ZEPHYR_BASE}/scripts/build/llext_prepare_exptab.py
--elf-file ${PROJECT_BINARY_DIR}/${KERNEL_ELF_NAME}
--slid-listing ${PROJECT_BINARY_DIR}/slid_listing.txt
-vvv
)
endif()
if(NOT CMAKE_C_COMPILER_ID STREQUAL "ARMClang")
set(check_init_priorities_input
$<IF:$<TARGET_EXISTS:native_runner_executable>,${BYPRODUCT_KERNEL_EXE_NAME},${BYPRODUCT_KERNEL_ELF_NAME}>

View file

@ -254,6 +254,10 @@ SECTIONS
#include <zephyr/linker/rel-sections.ld>
#ifdef CONFIG_LLEXT
#include <zephyr/linker/llext-sections.ld>
#endif
/DISCARD/ :
{
*(.plt)

View file

@ -31,6 +31,11 @@
cmake_minimum_required(VERSION 3.20.0)
if (CONFIG_LLEXT_EXPORT_BUILTINS_BY_SLID)
message(FATAL_ERROR
"The LLEXT EDK is not compatible with CONFIG_LLEXT_EXPORT_BUILTINS_BY_SLID.")
endif()
set(llext_edk ${PROJECT_BINARY_DIR}/${llext_edk_name})
set(llext_edk_inc ${llext_edk}/include)

View file

@ -5431,6 +5431,23 @@ function(add_llext_target target_name)
COMMAND_EXPAND_LISTS
)
# LLEXT ELF processing for importing via SLID
#
# This command must be executed as last step of the packaging process,
# to ensure that the ELF processed for binary generation contains SLIDs.
# If executed too early, it is possible that some tools executed to modify
# the ELF file (e.g., strip) undo the work performed here.
if (CONFIG_LLEXT_EXPORT_BUILTINS_BY_SLID)
set(slid_inject_cmd
${PYTHON_EXECUTABLE}
${ZEPHYR_BASE}/scripts/build/llext_inject_slids.py
--elf-file ${llext_pkg_output}
-vvv
)
else()
set(slid_inject_cmd ${CMAKE_COMMAND} -E true)
endif()
# Type-specific packaging of the built binary file into an .llext file
if(CONFIG_LLEXT_TYPE_ELF_OBJECT)
@ -5438,6 +5455,7 @@ function(add_llext_target target_name)
add_custom_command(
OUTPUT ${llext_pkg_output}
COMMAND ${CMAKE_COMMAND} -E copy ${llext_pkg_input} ${llext_pkg_output}
COMMAND ${slid_inject_cmd}
DEPENDS ${llext_proc_target} ${llext_pkg_input}
)
@ -5453,6 +5471,7 @@ function(add_llext_target target_name)
$<TARGET_PROPERTY:bintools,elfconvert_flag_infile>${llext_pkg_input}
$<TARGET_PROPERTY:bintools,elfconvert_flag_outfile>${llext_pkg_output}
$<TARGET_PROPERTY:bintools,elfconvert_flag_final>
COMMAND ${slid_inject_cmd}
DEPENDS ${llext_proc_target} ${llext_pkg_input}
)
@ -5467,6 +5486,7 @@ function(add_llext_target target_name)
$<TARGET_PROPERTY:bintools,strip_flag_infile>${llext_pkg_input}
$<TARGET_PROPERTY:bintools,strip_flag_outfile>${llext_pkg_output}
$<TARGET_PROPERTY:bintools,strip_flag_final>
COMMAND ${slid_inject_cmd}
DEPENDS ${llext_proc_target} ${llext_pkg_input}
)

View file

@ -81,6 +81,10 @@ SECTIONS {
#include <zephyr/linker/rel-sections.ld>
#ifdef CONFIG_LLEXT
#include <zephyr/linker/llext-sections.ld>
#endif
GROUP_START(ROMABLE_REGION)
SECTION_PROLOGUE(_TEXT_SECTION_NAME,,ALIGN(1024)) {

View file

@ -98,6 +98,10 @@ SECTIONS
{
#include <zephyr/linker/rel-sections.ld>
#ifdef CONFIG_LLEXT
#include <zephyr/linker/llext-sections.ld>
#endif
/*
* .plt and .iplt are here according to 'arm-zephyr-elf-ld --verbose',
* before text section.

View file

@ -113,6 +113,10 @@ SECTIONS
#include <zephyr/linker/rel-sections.ld>
#ifdef CONFIG_LLEXT
#include <zephyr/linker/llext-sections.ld>
#endif
/*
* .plt and .iplt are here according to 'arm-zephyr-elf-ld --verbose',
* before text section.

View file

@ -77,6 +77,10 @@ SECTIONS
#include <zephyr/linker/rel-sections.ld>
#ifdef CONFIG_LLEXT
#include <zephyr/linker/llext-sections.ld>
#endif
/*
* .plt and .iplt are here according to 'arm-zephyr-elf-ld --verbose',
* before text section.

View file

@ -46,6 +46,10 @@ SECTIONS
#include <zephyr/linker/rel-sections.ld>
#ifdef CONFIG_LLEXT
#include <zephyr/linker/llext-sections.ld>
#endif
SECTION_PROLOGUE(_VECTOR_SECTION_NAME,,)
{
. = ALIGN(0x1000);

View file

@ -85,6 +85,10 @@ SECTIONS
#include <zephyr/linker/rel-sections.ld>
#ifdef CONFIG_LLEXT
#include <zephyr/linker/llext-sections.ld>
#endif
/*
* .plt and .iplt are here according to
* 'nios2-zephyr-elf-ld --verbose', before text section.

View file

@ -21,6 +21,10 @@
SECTIONS
{
#ifdef CONFIG_LLEXT
#include <zephyr/linker/llext-sections.ld>
#endif
SECTION_PROLOGUE(rom_start,,)
{
/* Located in generated directory. This file is populated by the

View file

@ -122,6 +122,10 @@ SECTIONS
#include <zephyr/linker/rel-sections.ld>
#ifdef CONFIG_LLEXT
#include <zephyr/linker/llext-sections.ld>
#endif
/*
* The .plt and .iplt are here according to
* 'riscv32-zephyr-elf-ld --verbose', before text section.

View file

@ -23,6 +23,10 @@ SECTIONS
#include <zephyr/linker/rel-sections.ld>
#ifdef CONFIG_LLEXT
#include <zephyr/linker/llext-sections.ld>
#endif
__rom_region_start = .;
SECTION_PROLOGUE(_TEXT_SECTION_NAME,,)

View file

@ -77,6 +77,10 @@ SECTIONS
#include <zephyr/linker/rel-sections.ld>
#ifdef CONFIG_LLEXT
#include <zephyr/linker/llext-sections.ld>
#endif
/DISCARD/ :
{
*(.plt)

View file

@ -236,4 +236,7 @@ SECTIONS
.shstrtab 0 : { *(.shstrtab) }
#endif
#ifdef CONFIG_LLEXT
#include <zephyr/linker/llext-sections.ld>
#endif
}

View file

@ -0,0 +1,19 @@
/* SPDX-License-Identifier: Apache-2.0 */
/*
* Special section used by LLEXT if CONFIG_LLEXT_EXPORT_BUILTINS_BY_SLID
* is enabled. Declare this section to prevent it from being considered orphan.
*
* This section is used to temporarily save the exported symbols' names in the
* Zephyr ELF for post-processing, but it is not included in the final binary.
*
* NOTE: This section MUST start at address 0, as the post-processing scripts
* assume that the address of any data in this section (i.e., symbol names) is
* strictly equivalent to the offset inside the section.
*/
#ifdef CONFIG_LLEXT_EXPORT_BUILTINS_BY_SLID
SECTION_PROLOGUE(llext_exports_strtab, 0 (COPY), )
{
KEEP(*(llext_exports_strtab))
}
#endif

View file

@ -10,6 +10,7 @@
#include <zephyr/sys/iterable_sections.h>
#include <zephyr/toolchain.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
@ -27,14 +28,27 @@ extern "C" {
* Symbols may be named function or global objects that have been exported
* for linking. These constant symbols are useful in the base image
* as they may be placed in ROM.
*
* @note When updating this structure, make sure to also update the
* 'scripts/build/llext_prepare_exptab.py' build script.
*/
struct llext_const_symbol {
/** Name of symbol */
const char *const name;
/** At build time, we always write to 'name'.
* At runtime, which field is used depends
* on CONFIG_LLEXT_EXPORT_BUILTINS_BY_SLID.
*/
union {
/** Name of symbol */
const char *const name;
/** Symbol Link Identifier */
const uintptr_t slid;
};
/** Address of symbol */
const void *const addr;
};
BUILD_ASSERT(sizeof(struct llext_const_symbol) == 2 * sizeof(uintptr_t));
/**
* @brief Symbols are named memory addresses
@ -75,10 +89,19 @@ struct llext_symtable {
*
* @param x Symbol to export to extensions
*/
#ifdef CONFIG_LLEXT_EXPORT_BUILTINS_BY_SLID
#define EXPORT_SYMBOL(x) \
static const char Z_GENERIC_SECTION("llext_exports_strtab") __used \
x ## _sym_name[] = STRINGIFY(x); \
static const STRUCT_SECTION_ITERABLE(llext_const_symbol, x ## _sym) = { \
.name = x ## _sym_name, .addr = (const void *)&x, \
}
#else
#define EXPORT_SYMBOL(x) \
static const STRUCT_SECTION_ITERABLE(llext_const_symbol, x ## _sym) = { \
.name = STRINGIFY(x), .addr = (const void *)&x, \
}
#endif
/**
* @brief Exports a symbol from an extension to the base image

View file

@ -0,0 +1,196 @@
#!/usr/bin/env python3
#
# Copyright (c) 2024 STMicroelectronics
# SPDX-License-Identifier: Apache-2.0
"""Injects SLIDs in LLEXT ELFs' symbol tables.
When Kconfig option CONFIG_LLEXT_EXPORT_BUILTINS_BY_SLID is enabled,
all imports from the Zephyr kernel & application are resolved using
SLIDs instead of symbol names. This script stores the SLID of all
imported symbols in their associated entry in the ELF symbol table
to allow the LLEXT subsystem to link it properly at runtime.
Note that this script is idempotent in theory. However, to prevent
any catastrophic problem, the script will abort if the 'st_value'
field of the `ElfX_Sym` structure is found to be non-zero, which is
the case after one invocation. For this reason, in practice, the script
cannot actually be executed twice on the same ELF file.
"""
import argparse
import logging
import shutil
import sys
from elftools.elf.elffile import ELFFile
from elftools.elf.sections import SymbolTableSection
import llext_slidlib
class LLEXTSymtabPreparator():
def __init__(self, elf_path, log):
self.log = log
self.elf_path = elf_path
self.elf_fd = open(elf_path, "rb+")
self.elf = ELFFile(self.elf_fd)
def _find_symtab(self):
supported_symtab_sections = [
".symtab",
".dynsym",
]
symtab = None
for section_name in supported_symtab_sections:
symtab = self.elf.get_section_by_name(section_name)
if not isinstance(symtab, SymbolTableSection):
self.log.debug(f"section {section_name} not found.")
else:
self.log.info(f"processing '{section_name}' symbol table...")
self.log.debug(f"(symbol table is at file offset 0x{symtab['sh_offset']:X})")
break
return symtab
def _find_imports_in_symtab(self, symtab):
i = 0
imports = []
for sym in symtab.iter_symbols():
#Check if symbol is an import
if sym.entry['st_info']['type'] == 'STT_NOTYPE' and \
sym.entry['st_info']['bind'] == 'STB_GLOBAL' and \
sym.entry['st_shndx'] == 'SHN_UNDEF':
self.log.debug(f"found imported symbol '{sym.name}' at index {i}")
imports.append((i, sym))
i += 1
return imports
def _prepare_inner(self):
#1) Locate the symbol table
symtab = self._find_symtab()
if symtab is None:
self.log.error("no symbol table found in file")
return 1
#2) Find imported symbols in symbol table
imports = self._find_imports_in_symtab(symtab)
self.log.info(f"LLEXT has {len(imports)} import(s)")
#3) Write SLIDs in each symbol's 'st_value' field
def make_stvalue_reader_writer():
byteorder = "little" if self.elf.little_endian else "big"
if self.elf.elfclass == 32:
sizeof_Elf_Sym = 0x10 #sizeof(Elf32_Sym)
offsetof_st_value = 0x4 #offsetof(Elf32_Sym, st_value)
sizeof_st_value = 0x4 #sizeof(Elf32_Sym.st_value)
else:
sizeof_Elf_Sym = 0x18
offsetof_st_value = 0x8
sizeof_st_value = 0x8
def seek(symidx):
self.elf_fd.seek(
symtab['sh_offset'] +
symidx * sizeof_Elf_Sym +
offsetof_st_value)
def reader(symbol_index):
seek(symbol_index)
return int.from_bytes(self.elf_fd.read(sizeof_st_value), byteorder)
def writer(symbol_index, st_value):
seek(symbol_index)
self.elf_fd.write(int.to_bytes(st_value, sizeof_st_value, byteorder))
return reader, writer
rd_st_val, wr_st_val = make_stvalue_reader_writer()
slid_size = self.elf.elfclass // 8
for (index, symbol) in imports:
slid = llext_slidlib.generate_slid(symbol.name, slid_size)
slid_as_str = llext_slidlib.format_slid(slid, slid_size)
msg = f"{symbol.name} -> {slid_as_str}"
self.log.info(msg)
# Make sure we're not overwriting something actually important
original_st_value = rd_st_val(index)
if original_st_value != 0:
self.log.error(f"unexpected non-zero st_value for symbol {symbol.name}")
return 1
wr_st_val(index, slid)
return 0
def prepare_llext(self):
res = self._prepare_inner()
self.elf_fd.close()
return res
# Disable duplicate code warning for the code that follows,
# as it is expected for these functions to be similar.
# 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", required=True,
help="LLEXT ELF file to process")
parser.add_argument("-o", "--output-file",
help=("Additional output file where processed ELF "
"will be copied"))
parser.add_argument("-sl", "--slid-listing",
help="write the SLID listing to a file")
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"inject_slids_in_llext: {args.elf_file}")
preparator = LLEXTSymtabPreparator(args.elf_file, log)
res = preparator.prepare_llext()
if args.always_succeed:
return 0
if res == 0 and args.output_file:
shutil.copy(args.elf_file, args.output_file)
return res
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))

View file

@ -0,0 +1,377 @@
#!/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:]))

65
scripts/build/llext_slidlib.py Executable file
View file

@ -0,0 +1,65 @@
#!/usr/bin/env python3
#
# Copyright (c) 2024 STMicroelectronics
# SPDX-License-Identifier: Apache-2.0
"""
This file implements the Symbol Link Identifer (SLID)
generation code, for use by the LLEXT subsystem.
SLID-based linking is enabled by the Kconfig
option 'CONFIG_LLEXT_EXPORT_BUILTINS_BY_SLID'.
When executed as a script, this file can be used as
an interactive prompt to calculate the SLID of arbitrary
symbols, which can be useful for debugging purposes.
IMPLEMENTATION NOTES:
Currently, SLIDs are generated by taking the first
[pointer size] bytes of the symbol name's SHA-256
hash, taken in big-endian order.
This ordering provides one advantage: the 32-bit
SLID for an export is present in the top 32 bits of
the 64-bit SLID for the same export.
"""
from hashlib import sha256
def generate_slid(symbol_name: str, slid_size: int) -> int:
"""
Generates the Symbol Link Identifier (SLID) for a symbol.
symbol_name: Name of the symbol for which to generate a SLID
slid_side: Size of the SLID in bytes (4/8)
"""
if slid_size not in (4, 8):
raise AssertionError(f"Invalid SLID size {slid_size}")
m = sha256()
m.update(symbol_name.encode("utf-8"))
hash = m.digest()
return int.from_bytes(hash[0:slid_size], byteorder='big', signed=False)
def format_slid(slid: int, slid_size: int) -> str:
if slid_size == 4:
fmt = f"0x{slid:08X}"
elif slid_size == 8:
fmt = f"0x{slid:016X}"
return fmt
def repl():
while True:
sym_name = input("Symbol name? ")
slid32 = generate_slid(sym_name, 4)
slid64 = generate_slid(sym_name, 8)
print(f" 32-bit SLID for '{sym_name}': {format_slid(slid32, 4)}")
print(f" 64-bit SLID for '{sym_name}': {format_slid(slid64, 8)}")
print()
if __name__ == "__main__":
print("LLEXT SLID calculation REPL")
print("Press ^C to exit.")
try:
repl()
except KeyboardInterrupt:
print()

View file

@ -92,6 +92,10 @@ SECTIONS
#include <zephyr/linker/rel-sections.ld>
#ifdef CONFIG_LLEXT
#include <zephyr/linker/llext-sections.ld>
#endif
/*
* The .plt and .iplt are here according to
* 'riscv32-zephyr-elf-ld --verbose', before text section.

View file

@ -113,6 +113,10 @@ SECTIONS
#include <zephyr/linker/rel-sections.ld>
#ifdef CONFIG_LLEXT
#include <zephyr/linker/llext-sections.ld>
#endif
#ifdef CONFIG_GEN_ISR_TABLES
#include <zephyr/linker/intlist.ld>
#endif

View file

@ -177,6 +177,10 @@ SECTIONS
#include <zephyr/linker/rel-sections.ld>
#ifdef CONFIG_LLEXT
#include <zephyr/linker/llext-sections.ld>
#endif
#ifdef CONFIG_GEN_ISR_TABLES
#include <zephyr/linker/intlist.ld>
#endif

View file

@ -152,6 +152,10 @@ SECTIONS
#include <zephyr/linker/rel-sections.ld>
#ifdef CONFIG_LLEXT
#include <zephyr/linker/llext-sections.ld>
#endif
/* --- RTC BEGIN --- */
/* RTC fast memory holds RTC wake stub code,

View file

@ -55,6 +55,10 @@ SECTIONS
{
#include <zephyr/linker/rel-sections.ld>
#ifdef CONFIG_LLEXT
#include <zephyr/linker/llext-sections.ld>
#endif
_image_iram_start = LOADADDR(.iram0.vectors);
_image_iram_size = LOADADDR(_TEXT_SECTION_NAME) + SIZEOF(_TEXT_SECTION_NAME) - _image_iram_start;
_image_iram_vaddr = ADDR(.iram0.vectors);

View file

@ -129,6 +129,10 @@ SECTIONS
#include <zephyr/linker/rel-sections.ld>
#ifdef CONFIG_LLEXT
#include <zephyr/linker/llext-sections.ld>
#endif
/* --- START OF RTC --- */
.rtc.text :

View file

@ -136,6 +136,10 @@ SECTIONS
/* Virtual non-loadable sections */
#include <zephyr/linker/rel-sections.ld>
#ifdef CONFIG_LLEXT
#include <zephyr/linker/llext-sections.ld>
#endif
/* --- START OF RTC --- */
/* RTC fast memory holds RTC wake stub code,

View file

@ -135,6 +135,10 @@ SECTIONS
/* Virtual non-loadable sections */
#include <zephyr/linker/rel-sections.ld>
#ifdef CONFIG_LLEXT
#include <zephyr/linker/llext-sections.ld>
#endif
/* --- START OF RTC --- */
/* RTC fast memory holds RTC wake stub code */

View file

@ -38,6 +38,10 @@ SECTIONS
{
#include <zephyr/linker/rel-sections.ld>
#ifdef CONFIG_LLEXT
#include <zephyr/linker/llext-sections.ld>
#endif
/* Send .iram0 code to iram */
.iram0.vectors : ALIGN(4)
{

View file

@ -104,6 +104,10 @@ SECTIONS
#include <zephyr/linker/rel-sections.ld>
#ifdef CONFIG_LLEXT
#include <zephyr/linker/llext-sections.ld>
#endif
/*
* .plt and .iplt are here according to 'arm-zephyr-elf-ld --verbose',
* before text section.

View file

@ -490,4 +490,8 @@ SECTIONS {
#ifdef CONFIG_GEN_ISR_TABLES
#include <zephyr/linker/intlist.ld>
#endif
#ifdef CONFIG_LLEXT
#include <zephyr/linker/llext-sections.ld>
#endif
}

View file

@ -503,4 +503,8 @@ SECTIONS {
#ifdef CONFIG_GEN_ISR_TABLES
#include <zephyr/linker/intlist.ld>
#endif
#ifdef CONFIG_LLEXT
#include <zephyr/linker/llext-sections.ld>
#endif
}

View file

@ -91,6 +91,10 @@ SECTIONS
#include <zephyr/linker/rel-sections.ld>
#ifdef CONFIG_LLEXT
#include <zephyr/linker/llext-sections.ld>
#endif
/*
* The .plt and .iplt are here according to
* 'riscv32-zephyr-elf-ld --verbose', before text section.

View file

@ -173,6 +173,10 @@ SECTIONS
{
#include <zephyr/linker/rel-sections.ld>
#ifdef CONFIG_LLEXT
#include <zephyr/linker/llext-sections.ld>
#endif
.ResetVector.text : ALIGN(4)
{
_ResetVector_text_start = ABSOLUTE(.);

View file

@ -179,6 +179,10 @@ SECTIONS
#endif
#include <zephyr/linker/rel-sections.ld>
#ifdef CONFIG_LLEXT
#include <zephyr/linker/llext-sections.ld>
#endif
.ResetVector.text : ALIGN(4)
{
_ResetVector_text_start = ABSOLUTE(.);

View file

@ -173,6 +173,10 @@ SECTIONS
{
#include <zephyr/linker/rel-sections.ld>
#ifdef CONFIG_LLEXT
#include <zephyr/linker/llext-sections.ld>
#endif
.ResetVector.text : ALIGN(4)
{
_ResetVector_text_start = ABSOLUTE(.);

View file

@ -173,6 +173,10 @@ SECTIONS
{
#include <zephyr/linker/rel-sections.ld>
#ifdef CONFIG_LLEXT
#include <zephyr/linker/llext-sections.ld>
#endif
.ResetVector.text : ALIGN(4)
{
_ResetVector_text_start = ABSOLUTE(.);

View file

@ -71,6 +71,10 @@ SECTIONS
#include <zephyr/linker/rel-sections.ld>
#ifdef CONFIG_LLEXT
#include <zephyr/linker/llext-sections.ld>
#endif
/*
* .plt and .iplt are here according to 'arm-zephyr-elf-ld --verbose',
* before text section.

View file

@ -134,6 +134,10 @@ SECTIONS
{
#include <zephyr/linker/rel-sections.ld>
#ifdef CONFIG_LLEXT
#include <zephyr/linker/llext-sections.ld>
#endif
.ResetVector.text : ALIGN(4)
{
_ResetVector_text_start = ABSOLUTE(.);

View file

@ -87,6 +87,10 @@ SECTIONS
#include <zephyr/linker/rel-sections.ld>
#ifdef CONFIG_LLEXT
#include <zephyr/linker/llext-sections.ld>
#endif
SECTION_PROLOGUE(.plt,,)
{
*(.plt)

View file

@ -13,6 +13,11 @@ SECTIONS
{
#include <zephyr/linker/rel-sections.ld>
#ifdef CONFIG_LLEXT
#include <zephyr/linker/llext-sections.ld>
#endif
#ifdef CONFIG_OPENAMP_RSC_TABLE
SECTION_PROLOGUE(.resource_table,, SUBALIGN(4))

View file

@ -66,6 +66,21 @@ config LLEXT_STORAGE_WRITABLE
Select if LLEXT storage is writable, i.e. if extensions are stored in
RAM and can be modified in place
config LLEXT_EXPORT_BUILTINS_BY_SLID
bool "Export built-in symbols to llexts via SLIDs"
help
When enabled, symbols exported from the Zephyr kernel
or application (via EXPORT_SYMBOL) are linked to LLEXTs
via Symbol Link Identifiers (SLIDs) instead of name.
Enabling this option provides a huge size reduction,
makes the linking process faster and provides more
confidentiality, as exported symbol names are dropped
from the binary. However, it can make LLEXT debugging
harder and prevents usage of 'llext_find_sym' to look
up symbols from the built-in table by name. It also
requires the LLEXTs to be post-processed after build.
module = LLEXT
module-str = llext
source "subsys/logging/Kconfig.template.log_config"

View file

@ -24,6 +24,12 @@ LOG_MODULE_REGISTER(llext, CONFIG_LLEXT_LOG_LEVEL);
#define LLEXT_PAGE_SIZE 32
#endif
#ifdef CONFIG_LLEXT_EXPORT_BUILTINS_BY_SLID
#define SYM_NAME_OR_SLID(name, slid) ((const char *)slid)
#else
#define SYM_NAME_OR_SLID(name, slid) name
#endif
K_HEAP_DEFINE(llext_heap, CONFIG_LLEXT_HEAP_SIZE * 1024);
static const char ELF_MAGIC[] = {0x7f, 'E', 'L', 'F'};
@ -118,11 +124,27 @@ const void *llext_find_sym(const struct llext_symtable *sym_table, const char *s
{
if (sym_table == NULL) {
/* Built-in symbol table */
#ifdef CONFIG_LLEXT_EXPORT_BUILTINS_BY_SLID
/* 'sym_name' is actually a SLID to search for */
uintptr_t slid = (uintptr_t)sym_name;
/* TODO: perform a binary search instead of linear.
* Note that - as of writing - the llext_const_symbol_area
* section is sorted in ascending SLID order.
* (see scripts/build/llext_prepare_exptab.py)
*/
STRUCT_SECTION_FOREACH(llext_const_symbol, sym) {
if (slid == sym->slid) {
return sym->addr;
}
}
#else
STRUCT_SECTION_FOREACH(llext_const_symbol, sym) {
if (strcmp(sym->name, sym_name) == 0) {
return sym->addr;
}
}
#endif
} else {
/* find symbols in module */
for (size_t i = 0; i < sym_table->sym_cnt; i++) {
@ -728,7 +750,8 @@ static void llext_link_plt(struct llext_loader *ldr, struct llext *ext,
switch (stb) {
case STB_GLOBAL:
link_addr = llext_find_sym(NULL, name);
link_addr = llext_find_sym(NULL,
SYM_NAME_OR_SLID(name, sym_tbl.st_value));
if (!link_addr)
link_addr = llext_find_sym(&ext->sym_tab, name);
@ -864,7 +887,8 @@ static int llext_link(struct llext_loader *ldr, struct llext *ext, bool do_local
link_addr = 0;
} else if (sym.st_shndx == SHN_UNDEF) {
/* If symbol is undefined, then we need to look it up */
link_addr = (uintptr_t)llext_find_sym(NULL, name);
link_addr = (uintptr_t)llext_find_sym(NULL,
SYM_NAME_OR_SLID(name, sym.st_value));
if (link_addr == 0) {
LOG_ERR("Undefined symbol with no entry in "

View file

@ -24,6 +24,20 @@ LOG_MODULE_REGISTER(test_llext_simple);
#define LLEXT_CONST const
#endif
#ifdef CONFIG_LLEXT_EXPORT_BUILTINS_BY_SLID
#define LLEXT_FIND_BUILTIN_SYM(symbol_name) llext_find_sym(NULL, symbol_name ## _SLID)
#ifdef CONFIG_64BIT
#define printk_SLID ((const char *)0x87B3105268827052ull)
#define z_impl_ext_syscall_fail_SLID ((const char *)0xD58BC0E7C64CD965ull)
#else
#define printk_SLID ((const char *)0x87B31052ull)
#define z_impl_ext_syscall_fail_SLID ((const char *)0xD58BC0E7ull)
#endif
#else
#define LLEXT_FIND_BUILTIN_SYM(symbol_name) llext_find_sym(NULL, # symbol_name)
#endif
struct llext_test {
const char *name;
bool try_userspace;
@ -271,7 +285,7 @@ ZTEST(llext, test_pre_located)
*/
ZTEST(llext, test_printk_exported)
{
const void * const printk_fn = llext_find_sym(NULL, "printk");
const void * const printk_fn = LLEXT_FIND_BUILTIN_SYM(printk);
zassert_equal(printk_fn, printk, "printk should be an exported symbol");
}
@ -282,8 +296,9 @@ ZTEST(llext, test_printk_exported)
*/
ZTEST(llext, test_ext_syscall_fail)
{
const void * const esf_fn = llext_find_sym(NULL,
"z_impl_ext_syscall_fail");
const void * const esf_fn = LLEXT_FIND_BUILTIN_SYM(z_impl_ext_syscall_fail);
zassert_not_null(esf_fn, "est_fn should not be NULL");
zassert_is_null(*(uintptr_t **)esf_fn, NULL,
"ext_syscall_fail should be NULL");

View file

@ -80,3 +80,71 @@ tests:
- CONFIG_USERSPACE=y
- CONFIG_MODULES=y
- CONFIG_LLEXT_TEST_HELLO=m
llext.simple.readonly_slid_linking:
arch_exclude: xtensa # for now
filter: not CONFIG_MPU and not CONFIG_MMU and not CONFIG_SOC_SERIES_S32ZE
extra_configs:
- arch:arm:CONFIG_ARM_MPU=n
- CONFIG_LLEXT_STORAGE_WRITABLE=n
- CONFIG_LLEXT_EXPORT_BUILTINS_BY_SLID=y
llext.simple.readonly_mpu_slid_linking:
min_ram: 128
arch_exclude: xtensa # for now
filter: CONFIG_ARCH_HAS_USERSPACE
extra_configs:
- CONFIG_USERSPACE=y
- CONFIG_LLEXT_STORAGE_WRITABLE=n
- CONFIG_LLEXT_EXPORT_BUILTINS_BY_SLID=y
llext.simple.writable_slid_linking:
filter: not CONFIG_MPU and not CONFIG_MMU and not CONFIG_SOC_SERIES_S32ZE
extra_configs:
- arch:arm:CONFIG_ARM_MPU=n
- CONFIG_LLEXT_STORAGE_WRITABLE=y
llext.simple.modules_enabled_writable_slid_linking:
filter: not CONFIG_MPU and not CONFIG_MMU
platform_key:
- simulation
- arch
platform_exclude:
- qemu_cortex_a9 # MMU
extra_configs:
- arch:arm:CONFIG_ARM_MPU=n
- CONFIG_MODULES=y
- CONFIG_LLEXT_STORAGE_WRITABLE=y
- CONFIG_LLEXT_TEST_HELLO=m
- CONFIG_LLEXT_EXPORT_BUILTINS_BY_SLID=y
llext.simple.modules_enabled_writable_relocatable_slid_linking:
arch_exclude: arm arm64
filter: not CONFIG_MPU and not CONFIG_MMU
integration_platforms:
- qemu_xtensa
extra_configs:
- CONFIG_MODULES=y
- CONFIG_LLEXT_STORAGE_WRITABLE=y
- CONFIG_LLEXT_TYPE_ELF_RELOCATABLE=y
- CONFIG_LLEXT_TEST_HELLO=m
- CONFIG_LLEXT_EXPORT_BUILTINS_BY_SLID=y
llext.simple.modules_enabled_readonly_slid_linking:
filter: not CONFIG_MPU and not CONFIG_MMU
arch_exclude: xtensa # for now
platform_key:
- simulation
- arch
platform_exclude:
- qemu_cortex_a9 # MMU
extra_configs:
- arch:arm:CONFIG_ARM_MPU=n
- CONFIG_MODULES=y
- CONFIG_LLEXT_TEST_HELLO=m
- CONFIG_LLEXT_EXPORT_BUILTINS_BY_SLID=y
llext.simple.modules_enabled_readonly_mpu_slid_linking:
filter: CONFIG_ARCH_HAS_USERSPACE
arch_exclude: xtensa # for now
platform_key:
- simulation
- arch
extra_configs:
- CONFIG_USERSPACE=y
- CONFIG_MODULES=y
- CONFIG_LLEXT_TEST_HELLO=m
- CONFIG_LLEXT_EXPORT_BUILTINS_BY_SLID=y