Files
Matheus 7ac6681ea8 kernel: generate symbol tables
In preparation for printing file+function names in backtraces, add
support for generating and linking three tables into the kernel:

- dbg_sym_table: a table consisting of <uint32_t addr_offset, uint32_t
  function_name_offset, uint32_t file_name_offset>, being ordered by
  offset.
- dbg_fn_name_table: a table consisting of packed NULL terminated
  strings, which the dbg_sym_table points into.
- dbg_file_name_table: a table consisting of packed NULL terminated
  strings, which the dbg_sym_table points into.
2026-10-08 23:36:37 -03:00

191 lines
6.5 KiB
Python
Executable File

#!/usr/bin/env python3
# USAGE:
# python3 gen_fn_sym.py kernel_exec base_addr source_cwd output_file
#
# - output_file is an ELF object exporting dbg_sym_table, dbg_fn_name_table,
# dbg_file_name_table, and their corresponding _end symbols.
# - Each symbol record is <uint32_t address, uint32_t function_name_offset,
# uint32_t file_name_offset>. Addresses are relative to base_addr and string
# offsets are byte offsets into the corresponding string table.
# - Select matching tools with --nm, --objdump and --objcopy (or NM,
# OBJDUMP and OBJCOPY envs).
import argparse
import os
from pathlib import Path
import shlex
import struct
import subprocess
import tempfile
from typing import NamedTuple
UINT32_MAX = (1 << 32) - 1
SECTION_MARKERS = {"_text_start", "_text_end"}
class BfdTarget(NamedTuple):
file_format: str
architecture: str
def run_tool(command):
return subprocess.run(
command, check=True, capture_output=True,
encoding="utf-8", errors="surrogateescape",
env={**os.environ, "LC_ALL": "C"},
).stdout
def tool_command(value):
command = shlex.split(value)
if not command:
raise argparse.ArgumentTypeError("tool command must not be empty")
return command
def get_bfd_target(kernel_exec, objdump):
output = run_tool([*objdump, "-f", "--", kernel_exec])
file_format = architecture = None
for line in output.splitlines():
if "file format " in line:
file_format = line.rsplit("file format ", 1)[1].strip()
if line.startswith("architecture: "):
architecture = line[len("architecture: "):].split(",", 1)[0].strip()
if not file_format or not architecture or architecture == "UNKNOWN!":
raise ValueError("objdump could not identify the kernel target; select a matching --objdump")
return BfdTarget(file_format, architecture)
def source_filename(location, source_prefix):
if not location:
return ""
path, separator, line_number = location.rpartition(":")
filename = path if separator and line_number.isdigit() else location
filename = os.path.normpath(filename)
if filename.startswith(source_prefix):
filename = filename[len(source_prefix):]
return filename
def collect_symbols(kernel_exec, base_addr, source_cwd, nm):
output = run_tool(
[*nm, "-n", "-l", "--defined-only", "--format=bsd",
"--no-demangle", "--radix=x", "--", kernel_exec],
)
source_prefix = os.path.abspath(source_cwd).rstrip(os.sep) + os.sep
symbols = []
for line in output.splitlines():
symbol_text, _, location = line.partition("\t")
fields = symbol_text.split(None, 2)
if len(fields) != 3 or fields[1] not in ("T", "t"):
continue
address, _, name = fields
if name in SECTION_MARKERS:
continue
offset = int(address, 16) - base_addr
if not 0 <= offset <= UINT32_MAX:
raise ValueError(f"address of {name} does not fit in uint32_t relative to base_addr")
filename = source_filename(location, source_prefix)
symbols.append((offset, name, filename))
if not symbols:
raise ValueError("kernel binary contains no text symbols")
return sorted(symbols, key=lambda symbol: (symbol[0], symbol[1]))
def intern_string(value, table, offsets):
if value not in offsets:
encoded = value.encode("utf-8", errors="surrogateescape") + b"\0"
if len(table) + len(encoded) - 1 > UINT32_MAX:
raise ValueError("string table does not fit in uint32_t offsets")
offsets[value] = len(table)
table.extend(encoded)
return offsets[value]
def make_tables(symbols):
records = bytearray()
functions = bytearray(b"\0")
files = bytearray(b"\0")
function_offsets = {"": 0}
file_offsets = {"": 0}
for address, name, filename in symbols:
function_offset = intern_string(name, functions, function_offsets)
file_offset = intern_string(filename, files, file_offsets)
records.extend(struct.pack("<III", address, function_offset, file_offset))
return (
("dbg_sym_table", records),
("dbg_fn_name_table", functions),
("dbg_file_name_table", files),
)
def write_object(tables, output_file, objcopy, bfd_target):
output = Path(output_file).absolute()
with tempfile.TemporaryDirectory(prefix=".gen_fn_sym-", dir=output.parent) as directory:
directory = Path(directory)
name, data = tables[0]
binary_path = directory / f"{name}.bin"
binary_path.write_bytes(data)
object_path = directory / "tables.o"
stack_note_path = directory / "empty"
stack_note_path.write_bytes(b"")
command = [
*objcopy,
"--input-target", "binary",
"--output-target", bfd_target.file_format,
"--binary-architecture", bfd_target.architecture,
"--rename-section", f".data=.{name},alloc,load,readonly,data,contents",
"--set-section-alignment", ".data=4", "--strip-all",
"--add-section", f".note.GNU-stack={stack_note_path}",
]
for name, data in tables[1:]:
table_path = directory / f"{name}.bin"
table_path.write_bytes(data)
command.extend(["--add-section", f".{name}={table_path}"])
command.extend(["--set-section-flags", f".{name}=alloc,load,readonly,data,contents"])
for name, data in tables:
command.extend(["--add-symbol", f"{name}=.{name}:0,global,object"])
command.extend(["--add-symbol", f"{name}_end=.{name}:{len(data)},global"])
run_tool([*command, str(binary_path), str(object_path)])
os.replace(object_path, output)
def main():
parser = argparse.ArgumentParser(description="Generate a linkable kernel debug-symbol object using GNU binutils.")
parser.add_argument("kernel_exec", help="kernel ELF binary")
parser.add_argument("base_addr", type=lambda value: int(value, 0), help="kernel base address")
parser.add_argument("source_cwd", help="build directory prefix to strip from source filenames")
parser.add_argument("output_file", help="output object")
parser.add_argument("--nm", type=tool_command, default=os.environ.get("NM", "nm"), help="GNU nm command (default: $NM or nm)")
parser.add_argument("--objdump", type=tool_command, default=os.environ.get("OBJDUMP", "objdump"), help="GNU objdump command (default: $OBJDUMP or objdump)")
parser.add_argument("--objcopy", type=tool_command, default=os.environ.get("OBJCOPY", "objcopy"), help="GNU objcopy command (default: $OBJCOPY or objcopy)")
args = parser.parse_args()
try:
bfd_target = get_bfd_target(args.kernel_exec, args.objdump)
symbols = collect_symbols(args.kernel_exec, args.base_addr, args.source_cwd, args.nm)
tables = make_tables(symbols)
write_object(tables, args.output_file, args.objcopy, bfd_target)
except subprocess.CalledProcessError as error:
parser.exit(1, f"{error.cmd[0]} failed: {error.stderr.strip()}\n")
except (OSError, ValueError) as error:
parser.exit(1, f"{error}\n")
if __name__ == "__main__":
main()