| #!/usr/bin/env python3 |
| # Copyright 2022 The ChromiumOS Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Python script to generate ebpf skeletons from bpf code. |
| |
| This script compiles the C code with target bpf and then runs bpftool against |
| the resulting object file to generate bpf skeleton header files that can then be |
| used by userspace programs to load, attach and communicate with bpf functions. |
| """ |
| |
| import argparse |
| import logging |
| from pathlib import Path |
| import re |
| import subprocess |
| import sys |
| from typing import List |
| |
| |
| _HACK_VAR_TO_DISABLE_ISORT = "hack" |
| # pylint: disable=wrong-import-position |
| import chromite_init # pylint: disable=unused-import |
| |
| from chromite.lib import commandline |
| from chromite.lib import cros_build_lib |
| |
| |
| _ARCH_TO_DEFINE = { |
| "amd64": "__TARGET_ARCH_x86", |
| "amd64-linux": "__TARGET_ARCH_x86", |
| "arm": "__TARGET_ARCH_arm", |
| "arm-linux": "__TARGET_ARCH_arm", |
| "arm64": "__TARGET_ARCH_arm64", |
| "mips": "__TARGET_ARCH_mips", |
| "ppc": "__TARGET_ARCH_powerpc", |
| "ppc64": "__TARGET_ARCH_powerpc", |
| "ppc64-linux": "__TARGET_ARCH_powerpc", |
| "x86": "__TARGET_ARCH_x86", |
| "x86-linux": "__TARGET_ARCH_x86", |
| } |
| |
| |
| def _run_command(command: List[str]) -> subprocess.CompletedProcess: |
| """Run a command with default options. |
| |
| Run a command using subprocess.run with default configuration. |
| """ |
| return cros_build_lib.run( |
| command, |
| capture_output=True, |
| encoding="utf-8", |
| ) |
| |
| |
| def do_compile_bpf(opts: argparse.Namespace) -> int: |
| """Compile BPF program from C. |
| |
| Takes a BPF application written in C and generates the BPF object file. |
| If args.out_header is specified, the BPF object file is also processed to |
| generate BPF skeletons using bpftool. |
| If args.out_min_btf is specified, the BPF object file is also processed to |
| generate a min CO-RE BTF. |
| """ |
| out_obj = opts.out_obj |
| out_bpf_skeleton_header = opts.out_bpf_skeleton_header |
| out_btf = opts.out_min_btf |
| vmlinux_btf = opts.vmlinux_btf |
| source = opts.source |
| arch = _ARCH_TO_DEFINE[opts.arch] |
| includes = opts.includes |
| defines = opts.defines or [] |
| sysroot = opts.sysroot |
| |
| # Create the folder to hold the output if it does not exist. |
| out_obj.parent.mkdir(parents=True, exist_ok=True) |
| |
| # Calling bpf-clang is equivalent to "clang --target bpf". |
| # It may seem odd that the application needs to be compiled with -g but |
| # then llvm-strip is ran against the resulting object. |
| # The -g is needed for the bpf application to compile properly but we |
| # want to reduce the file size by stripping it. |
| call_bpf_clang = ( |
| ["/usr/bin/bpf-clang", "-g", "-O2", f"--sysroot={sysroot}"] |
| + [f"-I{x}" for x in includes] |
| + [f"-D{x}" for x in defines] |
| + [f"-D{arch}", "-c", source, "-o", out_obj] |
| ) |
| strip_dwarf = ["llvm-strip", "-g", out_obj] |
| |
| # Compile the BPF C application. |
| _run_command(call_bpf_clang) |
| # Strip useless dwarf information. |
| _run_command(strip_dwarf) |
| |
| # Use bpftools to generate skeletons from the BPF object files. |
| if out_bpf_skeleton_header: |
| gen_skeleton = ["/usr/sbin/bpftool", "gen", "skeleton", out_obj] |
| bpftool_proc = _run_command(gen_skeleton) |
| |
| # BPFtools will output the C formatted dump of kernel symbols to stdout. |
| # Write the contents to file. |
| out_bpf_skeleton_header.write_text( |
| bpftool_proc.stdout, encoding="utf-8" |
| ) |
| |
| # Generate a detached min_core BTF. |
| if out_btf: |
| if not vmlinux_btf: |
| print( |
| "Need a full vmlinux BTF as input in order to generate a min " |
| "BTF" |
| ) |
| return 1 |
| gen_min_core_btf = [ |
| "/usr/sbin/bpftool", |
| "gen", |
| "min_core_btf", |
| vmlinux_btf, |
| out_btf, |
| out_obj, |
| ] |
| _run_command(gen_min_core_btf) |
| |
| return 0 |
| |
| |
| def do_gen_vmlinux(opts: argparse.Namespace) -> int: |
| """Generate vmlinux.h for use in BPF programs. |
| |
| Invokes pahole and bpftool to generate vmlinux.h from vmlinux from the |
| kernel build. Uses BTF as an intermediate format. The generated BTF is |
| preserved for possible use in generation of min CO-RE BTFs. |
| """ |
| sysroot = opts.sysroot |
| vmlinux_out = opts.out_header |
| btf_out = opts.out_btf |
| vmlinux_in = f"{sysroot}/usr/lib/debug/boot/vmlinux" |
| gen_detached_btf = [ |
| "/usr/bin/pahole", |
| "--btf_encode_detached", |
| btf_out, |
| vmlinux_in, |
| ] |
| gen_vmlinux = [ |
| "/usr/sbin/bpftool", |
| "btf", |
| "dump", |
| "file", |
| btf_out, |
| "format", |
| "c", |
| ] |
| read_symbols = [ |
| "/usr/bin/readelf", |
| "--symbols", |
| "--wide", |
| vmlinux_in, |
| ] |
| read_sections = [ |
| "/usr/bin/readelf", |
| "--sections", |
| "--wide", |
| vmlinux_in, |
| ] |
| |
| # First, run pahole to generate a detached vmlinux BTF. This step works |
| # regardless of whether the vmlinux was built with CONFIG_DEBUG_BTF_INFO. |
| btf_out.parent.mkdir(parents=True, exist_ok=True) |
| _run_command(gen_detached_btf) |
| |
| symbols = _run_command(read_symbols) |
| for line in symbols.stdout.splitlines(): |
| # pylint: disable=line-too-long |
| # Example line to parse: |
| # 145612: ffffffff823e1ca0 274 OBJECT GLOBAL DEFAULT 2 linux_banner |
| d = line.split() |
| if len(d) == 8 and d[-1] == "linux_banner": |
| linux_banner_addr = int(d[1], 16) |
| linux_banner_len = int(d[2]) |
| linux_banner_sec = d[-2] |
| break |
| else: |
| logging.error("Failed to find linux_banner from %s", vmlinux_in) |
| return 1 |
| |
| sections = _run_command(read_sections) |
| target_section_str = f"[{linux_banner_sec:>2}]" |
| for line in sections.stdout.splitlines(): |
| # pylint: disable=line-too-long |
| # Example line to parse: |
| # [ 2] .rodata PROGBITS ffffffff82200000 1400000 5e104e 00 WAMS 0 0 4096 |
| if line.lstrip().startswith(target_section_str): |
| d = line.split("]", 1)[1].split() |
| section_addr = int(d[2], 16) |
| section_off = int(d[3], 16) |
| break |
| else: |
| logging.error( |
| "Failed to find section '%s' from %s", |
| target_section_str, |
| vmlinux_in, |
| ) |
| return 1 |
| |
| offset = section_off + linux_banner_addr - section_addr |
| with open(vmlinux_in, "rb") as fp: |
| fp.seek(offset) |
| linux_banner = fp.read(linux_banner_len).decode("utf-8") |
| |
| m = re.match(r"Linux version (\d+).(\d+).(\d+)", linux_banner) |
| if not m: |
| logging.error("Failed to match Linux version: %s", linux_banner) |
| return 1 |
| |
| version = m.group(1) |
| patch_level = m.group(2) |
| sub_level = m.group(3) |
| |
| # Constructing LINUX_VERSION_CODE. |
| linux_version_code = int(version) * 65536 + int(patch_level) * 256 |
| linux_version_code += min(int(sub_level), 255) |
| |
| # Then, use the generated BTF (and not vmlinux itself) to generate the |
| # header. |
| vmlinux_cmd = _run_command(gen_vmlinux) |
| with vmlinux_out.open("w", encoding="utf-8") as vmlinux: |
| written = False |
| |
| for line in vmlinux_cmd.stdout.splitlines(): |
| vmlinux.write(f"{line}\n") |
| |
| if not written and line == "#define __VMLINUX_H__": |
| vmlinux.write( |
| f"\n#define LINUX_VERSION_CODE {linux_version_code}\n" |
| ) |
| written = True |
| |
| if not written: |
| logging.error("Failed to write LINUX_VERSION_CODE") |
| return 1 |
| |
| return 0 |
| |
| |
| def get_parser() -> argparse.ArgumentParser: |
| parser = commandline.ArgumentParser(description=__doc__) |
| subparsers = parser.add_subparsers(help="sub-command help") |
| |
| compile_bpf = subparsers.add_parser("compile_bpf") |
| compile_bpf.add_argument( |
| "--out-obj", |
| required=True, |
| type=Path, |
| help="The name of the output object file.", |
| ) |
| compile_bpf.add_argument( |
| "--out-bpf-skeleton-header", |
| required=False, |
| type=Path, |
| help=( |
| "The name of the eBPF skeleton output header file." |
| " Specifying this argument will result in eBPF skeletons being" |
| " generated in addition to the eBPF objects." |
| ), |
| ) |
| compile_bpf.add_argument( |
| "--source", required=True, type=Path, help="The bpf source code." |
| ) |
| compile_bpf.add_argument( |
| "--arch", |
| required=True, |
| choices=_ARCH_TO_DEFINE.keys(), |
| help="The target architecture.", |
| ) |
| compile_bpf.add_argument( |
| "--includes", |
| required=True, |
| nargs="+", |
| help="Additional include directories.", |
| ) |
| compile_bpf.add_argument( |
| "--defines", |
| required=False, |
| nargs="*", |
| help="Additional preprocessor defines.", |
| ) |
| compile_bpf.add_argument( |
| "--vmlinux-btf", |
| required=False, |
| type=Path, |
| help="The detached full vmlinux BTF file.", |
| ) |
| compile_bpf.add_argument( |
| "--out-min-btf", |
| required=False, |
| type=Path, |
| help="The name of the output min BTF file.", |
| ) |
| # We require the board sysroot so that BPF compilations will use board |
| # libbpf headers. |
| compile_bpf.add_argument( |
| "--sysroot", |
| required=True, |
| type="dir_exists", |
| help="The path that should be treated as the root directory.", |
| ) |
| |
| compile_bpf.set_defaults(func=do_compile_bpf) |
| |
| gen_vmlinux = subparsers.add_parser("gen_vmlinux") |
| gen_vmlinux.add_argument( |
| "--sysroot", |
| required=True, |
| type="dir_exists", |
| help="The path that should be treated as the root directory.", |
| ) |
| gen_vmlinux.add_argument( |
| "--out-header", |
| required=True, |
| type=Path, |
| help="The name of the output vmlinux.h file.", |
| ) |
| gen_vmlinux.add_argument( |
| "--out-btf", |
| required=True, |
| type=Path, |
| help="The name of the output vmlinux BTF file.", |
| ) |
| gen_vmlinux.set_defaults(func=do_gen_vmlinux) |
| |
| return parser |
| |
| |
| def main(argv: List[str]) -> int: |
| """A command line tool for all things BPF. |
| |
| A command line tool to help compile eBPF code to object file and generate C |
| BPF skeletons, and to generate vmlinux.h from kernel build artifacts. |
| """ |
| parser = get_parser() |
| opts = parser.parse_args(argv) |
| |
| try: |
| return opts.func(opts) |
| except subprocess.CalledProcessError as e: |
| logging.error(e) |
| return 1 |
| |
| |
| if __name__ == "__main__": |
| sys.exit(main(sys.argv[1:])) |