Files
r2sleigh/scripts/gen_syscalls.py

103 lines
3.9 KiB
Python
Executable File

#!/usr/bin/env python3
"""Generate r2abi's Linux syscall tables from the kernel's own UAPI headers.
Each table is what the C preprocessor makes of the header a program for that
architecture includes, with the feature macros that architecture's own
`asm/unistd.h` defines before it includes the generic list. Nothing is typed
in by hand, so a refresh is a re-run against newer headers:
scripts/gen_syscalls.py crates/r2abi/data
radare2 keeps its tables as hand-maintained `sdb` text; its arm-32 table is
the obsolete OABI numbering, and every one of them states an interrupt vector
the x86-64 `syscall` instruction does not have. These are the kernel's.
"""
import re
import subprocess
import sys
from pathlib import Path
INCLUDE = Path("/usr/include")
# (table, header, defines): the defines are what the architecture's own
# uapi asm/unistd.h sets before including asm-generic/unistd.h.
TABLES = [
("linux-x86-64", "x86_64-linux-gnu/asm/unistd_64.h", []),
("linux-x86-32", "x86_64-linux-gnu/asm/unistd_32.h", []),
(
"linux-arm-64",
"asm-generic/unistd.h",
[
"__BITS_PER_LONG=64",
"__ARCH_WANT_RENAMEAT",
"__ARCH_WANT_NEW_STAT",
"__ARCH_WANT_SET_GET_RLIMIT",
"__ARCH_WANT_TIME32_SYSCALLS",
"__ARCH_WANT_SYS_CLONE3",
"__ARCH_WANT_MEMFD_SECRET",
],
),
]
NAME = re.compile(r"^#define __NR_([A-Za-z0-9_]+)\s")
def names(header: Path, defines: list[str]) -> list[str]:
macros = subprocess.run(
["cpp", "-dM", *[f"-D{d}" for d in defines], str(header)],
check=True, capture_output=True, text=True,
).stdout
return sorted({m.group(1) for line in macros.splitlines() if (m := NAME.match(line))})
def numbers(header: Path, defines: list[str], wanted: list[str]) -> dict[str, int]:
"""Each name's number, as the preprocessor expands its macro."""
probe = "\n".join(f"@{name}@ __NR_{name}" for name in wanted)
source = f"#include \"{header}\"\n{probe}\n"
expanded = subprocess.run(
["cpp", "-P", *[f"-D{d}" for d in defines], "-"],
input=source, check=True, capture_output=True, text=True,
).stdout
out = {}
for line in expanded.splitlines():
match = re.match(r"^@(\w+)@\s+(.*)$", line.strip())
if not match:
continue
expression = match.group(2)
# Only integer arithmetic survives expansion; anything else is a name
# the header leaves undefined here, and is not a syscall of this table.
if not re.fullmatch(r"[0-9xa-fA-F+\-() ]+", expression):
continue
out[match.group(1)] = int(eval(expression, {"__builtins__": {}})) # noqa: S307
return out
def main() -> int:
destination = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("crates/r2abi/data")
version = subprocess.run(
["dpkg-query", "-W", "-f", "${Version}", "linux-libc-dev"],
capture_output=True, text=True,
).stdout.strip() or "unknown"
for table, header, defines in TABLES:
path = INCLUDE / header
found = numbers(path, defines, names(path, defines))
# Bookkeeping macros, not calls: the generic list's bounds and the
# first number an architecture may give its own calls.
for bound in ("syscalls", "arch_specific_syscall"):
found.pop(bound, None)
lines = [
f"# Linux syscall numbers for {table}, from the kernel UAPI header",
f"# {header} (linux-libc-dev {version}), expanded by the C preprocessor",
f"# with {', '.join(defines) or 'no feature macros'}.",
"# Generated by scripts/gen_syscalls.py; do not edit.",
]
lines += [f"{number} {name}" for name, number in sorted(found.items(), key=lambda kv: (kv[1], kv[0]))]
(destination / f"syscalls-{table}.txt").write_text("\n".join(lines) + "\n")
print(f"{table}: {len(found)} syscalls")
return 0
if __name__ == "__main__":
sys.exit(main())