| #!/usr/bin/env python3 |
| # Copyright 2019 The ChromiumOS Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Print syscall table as markdown.""" |
| |
| from __future__ import print_function |
| |
| import argparse |
| import collections |
| import os |
| from pathlib import Path |
| import re |
| import subprocess |
| import sys |
| from typing import Dict, List, Optional, Tuple |
| |
| import constants |
| |
| |
| # The directory where all the code & markdown files live. |
| TOPDIR = Path(__file__).resolve().parent |
| |
| # The C library header to find symbols. |
| HEADER = "syscall.h" |
| |
| # The markdown file where we store this table. |
| MARKDOWN = "syscalls.md" |
| |
| # The string we use to start the table header. |
| START_OF_TABLE = "## Tables" |
| |
| # We order things based on expected usage. |
| ORDER = collections.OrderedDict( |
| ( |
| ("x86_64", "x86_64 (64-bit)"), |
| ("arm", "arm (32-bit/EABI)"), |
| ("aarch64", "arm64 (64-bit)"), |
| ("i686", "x86 (32-bit)"), |
| ) |
| ) |
| |
| # Which register is used for the syscall NR. |
| REGS = { |
| "arm": {"nr": "r7", "args": ["r0", "r1", "r2", "r3", "r4", "r5"]}, |
| "aarch64": {"nr": "x8", "args": ["x0", "x1", "x2", "x3", "x4", "x5"]}, |
| "i686": {"nr": "eax", "args": ["ebx", "ecx", "edx", "esi", "edi", "ebp"]}, |
| "x86_64": {"nr": "rax", "args": ["rdi", "rsi", "rdx", "r10", "r8", "r9"]}, |
| } |
| |
| |
| def manpage_link(name: str) -> str: |
| """Get a link to the man page for this symbol.""" |
| # There are man pages for almost every syscall, so blindly link them. |
| return "[man/](https://man7.org/linux/man-pages/man2/%s.2.html)" % (name,) |
| |
| |
| def cs_link(name: str) -> str: |
| """Get a link to code search for this symbol.""" |
| return ( |
| "[cs/](https://source.chromium.org/search?ss=chromiumos&" |
| f"q=f:third_party/kernel+SYSCALL_DEFINE[^,]*\\b{name}\\b)" |
| ) |
| |
| |
| def kernel_version(target: str) -> Tuple[int]: |
| """Figure out what version of the kernel we're looking at.""" |
| cc = "%s-clang" % (target,) |
| source = "#include <linux/version.h>\nLINUX_VERSION_CODE\n" |
| ret = subprocess.run( |
| [cc, "-E", "-P", "-"], |
| check=True, |
| input=source, |
| stdout=subprocess.PIPE, |
| encoding="utf-8", |
| ) |
| line = ret.stdout.splitlines()[-1] |
| ver = int(line) |
| return (ver >> 16 & 0xFF, ver >> 8 & 0xFF, ver & 0xFF) |
| |
| |
| def find_symbols(target: str) -> Dict[str, str]: |
| """Find all the symbols using |target|.""" |
| cc = "%s-clang" % (target,) |
| source = "#include <%s>\n" % (HEADER,) |
| ret = subprocess.run( |
| [cc, "-E", "-dD", "-P", "-"], |
| check=True, |
| input=source, |
| stdout=subprocess.PIPE, |
| encoding="utf-8", |
| ) |
| |
| table = {} |
| |
| # Find all the symbols that are known. We have to do two passes as the |
| # headers like to #undef & redefine names. |
| matcher = re.compile(r"^#define\s+(SYS_[^\s]+)\s+__NR_") |
| symbols = set() |
| for line in ret.stdout.splitlines(): |
| m = matcher.match(line) |
| if m: |
| sym = m.group(1) |
| symbols.add(sym) |
| |
| # Because they can ... |
| matcher = re.compile(r"^#define\s+(__ARM_NR_[^\s]+)\s+") |
| for line in ret.stdout.splitlines(): |
| m = matcher.match(line) |
| if m: |
| sym = m.group(1) |
| symbols.add(sym) |
| |
| source += "\n".join("__%s %s" % (x, x) for x in symbols) |
| |
| # Parse our custom code and extract the symbols. |
| ret = subprocess.run( |
| [cc, "-E", "-P", "-"], |
| check=True, |
| input=source, |
| stdout=subprocess.PIPE, |
| encoding="utf-8", |
| ) |
| |
| for line in ret.stdout.splitlines(): |
| if line.startswith("__"): |
| sym, val = line.strip().split(" ", 1) |
| if sym.startswith("____ARM"): |
| sym = "ARM_" + sym[11:] |
| if sym == "ARM_BASE": |
| continue |
| else: |
| sym = sym[6:] |
| if val.startswith("("): |
| val = constants.math_eval(val) |
| val = int(val) |
| assert sym not in table, "sym %s already found" % (sym,) |
| table[sym] = val |
| |
| return table |
| |
| |
| def load_table() -> Dict[str, Dict[str, str]]: |
| """Return a table of all the symbol values (and aliases).""" |
| all_tables = {} |
| for target in constants.TARGETS: |
| all_tables[target] = find_symbols(target) |
| return all_tables |
| |
| |
| def sort_table(table: Dict[str, str]) -> List[Tuple[str, str]]: |
| """Return a sorted table.""" |
| |
| def sorter(element): |
| try: |
| num = int(element[1]) |
| except ValueError: |
| num = 0 |
| return (num, element[0]) |
| |
| return sorted(table.items(), key=sorter) |
| |
| |
| def get_md_table( |
| table: Dict[str, str], prototypes: Dict[str, List[str]], arch: str |
| ) -> List[str]: |
| """Return the table in markdown format.""" |
| ret = [] |
| last_num = 0 |
| for sym, num in sort_table(table): |
| # Fill in holes in the table so it's obvious to the user when searching. |
| # Unless it's an obviously large gap for ABI reasons. We pick 100 as an |
| # arbitrary constant that seems to work. |
| if num - last_num < 100: |
| for stub in range(last_num + 1, num): |
| anchor = f"{arch}_{stub}" |
| ret.append( |
| f'| <a name="{anchor}"></a> [{stub}](#{anchor}) ' |
| f"| *not implemented* " |
| "| " |
| f"| 0x{stub:02x} " |
| "||" |
| ) |
| last_num = num |
| |
| anchor = f"{arch}_{num}" |
| line = '| <a name="%s"></a> [%i](#%s) | %s | %s %s | 0x%02x | ' % ( |
| anchor, |
| num, |
| anchor, |
| sym, |
| manpage_link(sym), |
| cs_link(sym), |
| num, |
| ) |
| prototype = prototypes.get(sym, ["?"] * 6) |
| prototype = (prototype + ["-"] * 6)[0:6] |
| line += " | ".join( |
| x.replace("*", r"\*").replace("_", r"\_") for x in prototype |
| ) |
| line += " |" |
| |
| ret.append(line) |
| return ret |
| |
| |
| def get_md_common(tables: Dict[str, Dict[str, str]]) -> List[str]: |
| all_syscalls = set() |
| for table in tables.values(): |
| all_syscalls.update(table.keys()) |
| |
| desc = [x.split(" ")[0] for x in ORDER.values()] |
| targets = [ |
| [x for x in constants.TARGETS if x.startswith(frag)][0] |
| for frag in ORDER.keys() |
| ] |
| |
| ret = [ |
| "", |
| "### Cross-arch Numbers", |
| "", |
| ( |
| "This shows the syscall numbers for (hopefully) the same syscall " |
| "name across architectures." |
| ), |
| "Consult the [Random Names](#naming) section for common gotchas.", |
| "", |
| "| syscall name | %s |" % (" | ".join(desc),), |
| "|---|" + (":---:|" * len(desc)), |
| ] |
| for syscall in sorted(all_syscalls): |
| ret.append( |
| "| %s | %s |" |
| % ( |
| syscall, |
| " | ".join(str(tables[x].get(syscall, "-")) for x in targets), |
| ) |
| ) |
| return ret |
| |
| |
| def load_prototypes() -> Dict[str, List[str]]: |
| """Parse out prototypes from the kernel headers.""" |
| # Find the path to the kernel trees in the checkout. |
| kernels = TOPDIR.parent.parent / "src/third_party/kernel" |
| # Load all the kernel versions that have the header file we need. |
| versions = [ |
| x |
| for x in os.listdir(kernels) |
| if ( |
| re.match("^v[0-9.]+$", x) |
| and (kernels / x / "include/linux/syscalls.h").exists() |
| ) |
| ] |
| # Sort the versions to find the latest (which should be best?). |
| versort = sorted([int(x) for x in v[1:].split(".")] for v in versions) |
| latest = versort[-1] |
| |
| syscalls = ( |
| kernels |
| / ("v%s" % ".".join(str(x) for x in latest)) |
| / "include/linux/syscalls.h" |
| ) |
| ret = {} |
| data = syscalls.read_text() |
| entries = re.findall( |
| r"^asmlinkage long sys_([^(]*)\s*\(([^)]+)\)", data, flags=re.M |
| ) |
| for name, prototype in entries: |
| # Clean up the prototype a bit. |
| params = [x.strip() for x in prototype.split(",")] |
| if params == ["void"]: |
| params = [] |
| for i, param in enumerate(params): |
| keywords = [x for x in param.split() if x not in ("__user",)] |
| params[i] = " ".join(keywords) |
| ret[name] = params |
| return ret |
| |
| |
| def get_parser() -> argparse.ArgumentParser: |
| """Return a command line parser.""" |
| parser = argparse.ArgumentParser(description=__doc__) |
| parser.add_argument( |
| "-i", |
| "--inplace", |
| action="store_true", |
| help="Update the markdown file directly.", |
| ) |
| return parser |
| |
| |
| def main(argv: List[str]) -> Optional[int]: |
| """The main func!""" |
| parser = get_parser() |
| opts = parser.parse_args(argv) |
| |
| tables = load_table() |
| prototypes = load_prototypes() |
| |
| md_data = [] |
| |
| for frag, desc in ORDER.items(): |
| target = [x for x in constants.TARGETS if x.startswith(frag)][0] |
| |
| ver = kernel_version(target) |
| sver = ".".join(str(x) for x in ver) |
| |
| table = tables[target] |
| reg_nr = REGS[frag]["nr"] |
| reg_args = REGS[frag]["args"] |
| md_data += [ |
| "", |
| "### %s" % (desc,), |
| "", |
| "Compiled from [Linux %s headers][linux-headers]." % (sver,), |
| "", |
| ( |
| "| NR | syscall name | references | %%%s | arg0\u00a0(%%%s) | " |
| "arg1\u00a0(%%%s) | arg2\u00a0(%%%s) | arg3\u00a0(%%%s) | " |
| "arg4\u00a0(%%%s) | arg5\u00a0(%%%s) |" |
| ) |
| % (reg_nr, *reg_args), |
| "|:---:|---|:---:|:---:|---|---|---|---|---|---|", |
| ] |
| md_data += get_md_table(table, prototypes, frag) |
| |
| md_data += get_md_common(tables) |
| |
| if opts.inplace: |
| md_file = TOPDIR / MARKDOWN |
| old_data = md_file.read_text().splitlines(keepends=True) |
| |
| i = None |
| for i, line in enumerate(old_data): |
| if line.startswith(START_OF_TABLE): |
| break |
| else: |
| print( |
| "ERROR: could not find table in %s" % (md_file,), |
| file=sys.stderr, |
| ) |
| sys.exit(1) |
| |
| old_data = old_data[0 : i + 1] |
| with md_file.open("w") as fp: |
| fp.writelines(old_data) |
| fp.write("\n".join(md_data) + "\n") |
| else: |
| print("\n".join(md_data)) |
| |
| |
| if __name__ == "__main__": |
| sys.exit(main(sys.argv[1:])) |