Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

scripts/gdb/symbols: make BPF debug info available to GDB

One can debug BPF programs with QEMU gdbstub by setting a breakpoint on
bpf_prog_kallsyms_add(), waiting for a hit with a matching aux.name, and
then setting a breakpoint on bpf_func. This is tedious, error-prone, and
also lacks line numbers.

Automate this in a way similar to the existing support for modules in
lx-symbols.

Enumerate and monitor changes to both BPF kallsyms and JITed progs. For
each ksym, generate and compile a synthetic .s file containing the name,
code, and size. In addition, if this ksym is also a prog, and not a
trampoline, add line number information.

Ensure that this is a no-op if the kernel is built without BPF support or
if "as" is missing. In theory the "as" dependency may be dropped by
generating the synthetic .o file manually, but this is too much complexity
for too little benefit.

Now one can debug BPF progs out of the box like this:

(gdb) lx-symbols -bpf
(gdb) b bpf_prog_4e612a6a881a086b_arena_list_add
Breakpoint 2 (bpf_prog_4e612a6a881a086b_arena_list_add) pending.

# ./test_progs -t arena_list

Thread 4 hit Breakpoint 2, bpf_prog_4e612a6a881a086b_arena_list_add ()
at linux/tools/testing/selftests/bpf/progs/arena_list.c:51
51 list_head = &global_head;
(gdb) n
bpf_prog_4e612a6a881a086b_arena_list_add () at linux/tools/testing/selftests/bpf/progs/arena_list.c:53
53 for (i = zero; i < cnt && can_loop; i++) {

This also works for subprogs.

Link: https://lkml.kernel.org/r/20251106124600.86736-3-iii@linux.ibm.com
Signed-off-by: Ilya Leoshkevich <iii@linux.ibm.com>
Cc: Alexander Gordeev <agordeev@linux.ibm.com>
Cc: Alexei Starovoitov <ast@kernel.org>
Cc: Andrii Nakryiko <andrii@kernel.org>
Cc: Daniel Borkman <daniel@iogearbox.net>
Cc: Heiko Carstens <hca@linux.ibm.com>
Cc: Jan Kiszka <jan.kiszka@siemens.com>
Cc: Kieran Bingham <kbingham@kernel.org>
Cc: Vasily Gorbik <gor@linux.ibm.com>
Signed-off-by: Andrew Morton <akpm@linux-foundation.org>

authored by

Ilya Leoshkevich and committed by
Andrew Morton
581ee79a caa71919

+349 -12
+253
scripts/gdb/linux/bpf.py
··· 1 + # SPDX-License-Identifier: GPL-2.0 2 + 3 + import json 4 + import subprocess 5 + import tempfile 6 + 7 + import gdb 8 + 9 + from linux import constants, lists, radixtree, utils 10 + 11 + 12 + if constants.LX_CONFIG_BPF and constants.LX_CONFIG_BPF_JIT: 13 + bpf_ksym_type = utils.CachedType("struct bpf_ksym") 14 + if constants.LX_CONFIG_BPF_SYSCALL: 15 + bpf_prog_type = utils.CachedType("struct bpf_prog") 16 + 17 + 18 + def get_ksym_name(ksym): 19 + name = ksym["name"].bytes 20 + end = name.find(b"\x00") 21 + if end != -1: 22 + name = name[:end] 23 + return name.decode() 24 + 25 + 26 + def list_ksyms(): 27 + if not (constants.LX_CONFIG_BPF and constants.LX_CONFIG_BPF_JIT): 28 + return [] 29 + bpf_kallsyms = gdb.parse_and_eval("&bpf_kallsyms") 30 + bpf_ksym_ptr_type = bpf_ksym_type.get_type().pointer() 31 + return list(lists.list_for_each_entry(bpf_kallsyms, 32 + bpf_ksym_ptr_type, 33 + "lnode")) 34 + 35 + 36 + class KsymAddBreakpoint(gdb.Breakpoint): 37 + def __init__(self, monitor): 38 + super(KsymAddBreakpoint, self).__init__("bpf_ksym_add", internal=True) 39 + self.silent = True 40 + self.monitor = monitor 41 + 42 + def stop(self): 43 + self.monitor.add(gdb.parse_and_eval("ksym")) 44 + return False 45 + 46 + 47 + class KsymRemoveBreakpoint(gdb.Breakpoint): 48 + def __init__(self, monitor): 49 + super(KsymRemoveBreakpoint, self).__init__("bpf_ksym_del", 50 + internal=True) 51 + self.silent = True 52 + self.monitor = monitor 53 + 54 + def stop(self): 55 + self.monitor.remove(gdb.parse_and_eval("ksym")) 56 + return False 57 + 58 + 59 + class KsymMonitor: 60 + def __init__(self, add, remove): 61 + self.add = add 62 + self.remove = remove 63 + 64 + self.add_bp = KsymAddBreakpoint(self) 65 + self.remove_bp = KsymRemoveBreakpoint(self) 66 + 67 + self.notify_initial() 68 + 69 + def notify_initial(self): 70 + for ksym in list_ksyms(): 71 + self.add(ksym) 72 + 73 + def delete(self): 74 + self.add_bp.delete() 75 + self.remove_bp.delete() 76 + 77 + 78 + def list_progs(): 79 + if not constants.LX_CONFIG_BPF_SYSCALL: 80 + return [] 81 + idr_rt = gdb.parse_and_eval("&prog_idr.idr_rt") 82 + bpf_prog_ptr_type = bpf_prog_type.get_type().pointer() 83 + progs = [] 84 + for _, slot in radixtree.for_each_slot(idr_rt): 85 + prog = slot.dereference().cast(bpf_prog_ptr_type) 86 + progs.append(prog) 87 + # Subprogs are not registered in prog_idr, fetch them manually. 88 + # func[0] is the current prog. 89 + aux = prog["aux"] 90 + func = aux["func"] 91 + real_func_cnt = int(aux["real_func_cnt"]) 92 + for i in range(1, real_func_cnt): 93 + progs.append(func[i]) 94 + return progs 95 + 96 + 97 + class ProgAddBreakpoint(gdb.Breakpoint): 98 + def __init__(self, monitor): 99 + super(ProgAddBreakpoint, self).__init__("bpf_prog_kallsyms_add", 100 + internal=True) 101 + self.silent = True 102 + self.monitor = monitor 103 + 104 + def stop(self): 105 + self.monitor.add(gdb.parse_and_eval("fp")) 106 + return False 107 + 108 + 109 + class ProgRemoveBreakpoint(gdb.Breakpoint): 110 + def __init__(self, monitor): 111 + super(ProgRemoveBreakpoint, self).__init__("bpf_prog_free_id", 112 + internal=True) 113 + self.silent = True 114 + self.monitor = monitor 115 + 116 + def stop(self): 117 + self.monitor.remove(gdb.parse_and_eval("prog")) 118 + return False 119 + 120 + 121 + class ProgMonitor: 122 + def __init__(self, add, remove): 123 + self.add = add 124 + self.remove = remove 125 + 126 + self.add_bp = ProgAddBreakpoint(self) 127 + self.remove_bp = ProgRemoveBreakpoint(self) 128 + 129 + self.notify_initial() 130 + 131 + def notify_initial(self): 132 + for prog in list_progs(): 133 + self.add(prog) 134 + 135 + def delete(self): 136 + self.add_bp.delete() 137 + self.remove_bp.delete() 138 + 139 + 140 + def btf_str_by_offset(btf, offset): 141 + while offset < btf["start_str_off"]: 142 + btf = btf["base_btf"] 143 + 144 + offset -= btf["start_str_off"] 145 + if offset < btf["hdr"]["str_len"]: 146 + return (btf["strings"] + offset).string() 147 + 148 + return None 149 + 150 + 151 + def bpf_line_info_line_num(line_col): 152 + return line_col >> 10 153 + 154 + 155 + def bpf_line_info_line_col(line_col): 156 + return line_col & 0x3ff 157 + 158 + 159 + class LInfoIter: 160 + def __init__(self, prog): 161 + # See bpf_prog_get_file_line() for details. 162 + self.pos = 0 163 + self.nr_linfo = 0 164 + 165 + if prog is None: 166 + return 167 + 168 + self.bpf_func = int(prog["bpf_func"]) 169 + aux = prog["aux"] 170 + self.btf = aux["btf"] 171 + linfo_idx = aux["linfo_idx"] 172 + self.nr_linfo = int(aux["nr_linfo"]) - linfo_idx 173 + if self.nr_linfo == 0: 174 + return 175 + 176 + linfo_ptr = aux["linfo"] 177 + tpe = linfo_ptr.type.target().array(self.nr_linfo).pointer() 178 + self.linfo = (linfo_ptr + linfo_idx).cast(tpe).dereference() 179 + jited_linfo_ptr = aux["jited_linfo"] 180 + tpe = jited_linfo_ptr.type.target().array(self.nr_linfo).pointer() 181 + self.jited_linfo = (jited_linfo_ptr + linfo_idx).cast(tpe).dereference() 182 + 183 + self.filenos = {} 184 + 185 + def get_code_off(self): 186 + if self.pos >= self.nr_linfo: 187 + return -1 188 + return self.jited_linfo[self.pos] - self.bpf_func 189 + 190 + def advance(self): 191 + self.pos += 1 192 + 193 + def get_fileno(self): 194 + file_name_off = int(self.linfo[self.pos]["file_name_off"]) 195 + fileno = self.filenos.get(file_name_off) 196 + if fileno is not None: 197 + return fileno, None 198 + file_name = btf_str_by_offset(self.btf, file_name_off) 199 + fileno = len(self.filenos) + 1 200 + self.filenos[file_name_off] = fileno 201 + return fileno, file_name 202 + 203 + def get_line_col(self): 204 + line_col = int(self.linfo[self.pos]["line_col"]) 205 + return bpf_line_info_line_num(line_col), \ 206 + bpf_line_info_line_col(line_col) 207 + 208 + 209 + def generate_debug_obj(ksym, prog): 210 + name = get_ksym_name(ksym) 211 + # Avoid read_memory(); it throws bogus gdb.MemoryError in some contexts. 212 + start = ksym["start"] 213 + code = start.cast(gdb.lookup_type("unsigned char") 214 + .array(int(ksym["end"]) - int(start)) 215 + .pointer()).dereference().bytes 216 + linfo_iter = LInfoIter(prog) 217 + 218 + result = tempfile.NamedTemporaryFile(suffix=".o", mode="wb") 219 + try: 220 + with tempfile.NamedTemporaryFile(suffix=".s", mode="w") as src: 221 + # ".loc" does not apply to ".byte"s, only to ".insn"s, but since 222 + # this needs to work for all architectures, the latter are not an 223 + # option. Ask the assembler to apply ".loc"s to labels as well, 224 + # and generate dummy labels after each ".loc". 225 + src.write(".loc_mark_labels 1\n") 226 + 227 + src.write(".globl {}\n".format(name)) 228 + src.write(".type {},@function\n".format(name)) 229 + src.write("{}:\n".format(name)) 230 + for code_off, code_byte in enumerate(code): 231 + if linfo_iter.get_code_off() == code_off: 232 + fileno, file_name = linfo_iter.get_fileno() 233 + if file_name is not None: 234 + src.write(".file {} {}\n".format( 235 + fileno, json.dumps(file_name))) 236 + line, col = linfo_iter.get_line_col() 237 + src.write(".loc {} {} {}\n".format(fileno, line, col)) 238 + src.write("0:\n") 239 + linfo_iter.advance() 240 + src.write(".byte {}\n".format(code_byte)) 241 + src.write(".size {},{}\n".format(name, len(code))) 242 + src.flush() 243 + 244 + try: 245 + subprocess.check_call(["as", "-c", src.name, "-o", result.name]) 246 + except FileNotFoundError: 247 + # "as" is not installed. 248 + result.close() 249 + return None 250 + return result 251 + except: 252 + result.close() 253 + raise
+3
scripts/gdb/linux/constants.py.in
··· 170 170 LX_CONFIG(CONFIG_SLUB_DEBUG) 171 171 LX_CONFIG(CONFIG_SLAB_FREELIST_HARDENED) 172 172 LX_CONFIG(CONFIG_MMU) 173 + LX_CONFIG(CONFIG_BPF) 174 + LX_CONFIG(CONFIG_BPF_JIT) 175 + LX_CONFIG(CONFIG_BPF_SYSCALL)
+93 -12
scripts/gdb/linux/symbols.py
··· 11 11 # This work is licensed under the terms of the GNU GPL version 2. 12 12 # 13 13 14 + import atexit 14 15 import gdb 15 16 import os 16 17 import re 17 18 import struct 18 19 19 20 from itertools import count 20 - from linux import modules, utils, constants 21 + from linux import bpf, constants, modules, utils 21 22 22 23 23 24 if hasattr(gdb, 'Breakpoint'): ··· 115 114 The kernel (vmlinux) is taken from the current working directly. Modules (.ko) 116 115 are scanned recursively, starting in the same directory. Optionally, the module 117 116 search path can be extended by a space separated list of paths passed to the 118 - lx-symbols command.""" 117 + lx-symbols command. 118 + 119 + When the -bpf flag is specified, symbols from the currently loaded BPF programs 120 + are loaded as well.""" 119 121 120 122 module_paths = [] 121 123 module_files = [] 122 124 module_files_updated = False 123 125 loaded_modules = [] 124 126 breakpoint = None 127 + bpf_prog_monitor = None 128 + bpf_ksym_monitor = None 129 + bpf_progs = {} 130 + # The remove-symbol-file command, even when invoked with -a, requires the 131 + # respective object file to exist, so keep them around. 132 + bpf_debug_objs = {} 125 133 126 134 def __init__(self): 127 135 super(LxSymbols, self).__init__("lx-symbols", gdb.COMMAND_FILES, 128 136 gdb.COMPLETE_FILENAME) 137 + atexit.register(self.cleanup_bpf) 129 138 130 139 def _update_module_files(self): 131 140 self.module_files = [] ··· 208 197 else: 209 198 gdb.write("no module object found for '{0}'\n".format(module_name)) 210 199 200 + def add_bpf_prog(self, prog): 201 + if prog["jited"]: 202 + self.bpf_progs[int(prog["bpf_func"])] = prog 203 + 204 + def remove_bpf_prog(self, prog): 205 + self.bpf_progs.pop(int(prog["bpf_func"]), None) 206 + 207 + def add_bpf_ksym(self, ksym): 208 + addr = int(ksym["start"]) 209 + name = bpf.get_ksym_name(ksym) 210 + with utils.pagination_off(): 211 + gdb.write("loading @{addr}: {name}\n".format( 212 + addr=hex(addr), name=name)) 213 + debug_obj = bpf.generate_debug_obj(ksym, self.bpf_progs.get(addr)) 214 + if debug_obj is None: 215 + return 216 + try: 217 + cmdline = "add-symbol-file {obj} {addr}".format( 218 + obj=debug_obj.name, addr=hex(addr)) 219 + gdb.execute(cmdline, to_string=True) 220 + except: 221 + debug_obj.close() 222 + raise 223 + self.bpf_debug_objs[addr] = debug_obj 224 + 225 + def remove_bpf_ksym(self, ksym): 226 + addr = int(ksym["start"]) 227 + debug_obj = self.bpf_debug_objs.pop(addr, None) 228 + if debug_obj is None: 229 + return 230 + try: 231 + name = bpf.get_ksym_name(ksym) 232 + gdb.write("unloading @{addr}: {name}\n".format( 233 + addr=hex(addr), name=name)) 234 + cmdline = "remove-symbol-file {path}".format(path=debug_obj.name) 235 + gdb.execute(cmdline, to_string=True) 236 + finally: 237 + debug_obj.close() 238 + 239 + def cleanup_bpf(self): 240 + self.bpf_progs = {} 241 + while len(self.bpf_debug_objs) > 0: 242 + self.bpf_debug_objs.popitem()[1].close() 243 + 244 + 211 245 def load_all_symbols(self): 212 246 gdb.write("loading vmlinux\n") 213 247 ··· 280 224 else: 281 225 [self.load_module_symbols(module) for module in module_list] 282 226 227 + self.cleanup_bpf() 228 + if self.bpf_prog_monitor is not None: 229 + self.bpf_prog_monitor.notify_initial() 230 + if self.bpf_ksym_monitor is not None: 231 + self.bpf_ksym_monitor.notify_initial() 232 + 283 233 for saved_state in saved_states: 284 234 saved_state['breakpoint'].enabled = saved_state['enabled'] 285 235 286 236 def invoke(self, arg, from_tty): 287 237 skip_decompressor() 288 238 289 - self.module_paths = [os.path.abspath(os.path.expanduser(p)) 290 - for p in arg.split()] 239 + monitor_bpf = False 240 + self.module_paths = [] 241 + for p in arg.split(): 242 + if p == "-bpf": 243 + monitor_bpf = True 244 + else: 245 + p.append(os.path.abspath(os.path.expanduser(p))) 291 246 self.module_paths.append(os.getcwd()) 247 + 248 + if self.breakpoint is not None: 249 + self.breakpoint.delete() 250 + self.breakpoint = None 251 + if self.bpf_prog_monitor is not None: 252 + self.bpf_prog_monitor.delete() 253 + self.bpf_prog_monitor = None 254 + if self.bpf_ksym_monitor is not None: 255 + self.bpf_ksym_monitor.delete() 256 + self.bpf_ksym_monitor = None 292 257 293 258 # enforce update 294 259 self.module_files = [] ··· 317 240 318 241 self.load_all_symbols() 319 242 320 - if not modules.has_modules(): 243 + if not hasattr(gdb, 'Breakpoint'): 244 + gdb.write("Note: symbol update on module and BPF loading not " 245 + "supported with this gdb version\n") 321 246 return 322 247 323 - if hasattr(gdb, 'Breakpoint'): 324 - if self.breakpoint is not None: 325 - self.breakpoint.delete() 326 - self.breakpoint = None 248 + if modules.has_modules(): 327 249 self.breakpoint = LoadModuleBreakpoint( 328 250 "kernel/module/main.c:do_init_module", self) 329 - else: 330 - gdb.write("Note: symbol update on module loading not supported " 331 - "with this gdb version\n") 251 + 252 + if monitor_bpf: 253 + if constants.LX_CONFIG_BPF_SYSCALL: 254 + self.bpf_prog_monitor = bpf.ProgMonitor(self.add_bpf_prog, 255 + self.remove_bpf_prog) 256 + if constants.LX_CONFIG_BPF and constants.LX_CONFIG_BPF_JIT: 257 + self.bpf_ksym_monitor = bpf.KsymMonitor(self.add_bpf_ksym, 258 + self.remove_bpf_ksym) 332 259 333 260 334 261 LxSymbols()