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>
This commit is contained in:
Ilya Leoshkevich
2025-11-06 13:43:42 +01:00
committed by Andrew Morton
parent caa71919a6
commit 581ee79a25
3 changed files with 349 additions and 12 deletions

253
scripts/gdb/linux/bpf.py Normal file
View File

@@ -0,0 +1,253 @@
# SPDX-License-Identifier: GPL-2.0
import json
import subprocess
import tempfile
import gdb
from linux import constants, lists, radixtree, utils
if constants.LX_CONFIG_BPF and constants.LX_CONFIG_BPF_JIT:
bpf_ksym_type = utils.CachedType("struct bpf_ksym")
if constants.LX_CONFIG_BPF_SYSCALL:
bpf_prog_type = utils.CachedType("struct bpf_prog")
def get_ksym_name(ksym):
name = ksym["name"].bytes
end = name.find(b"\x00")
if end != -1:
name = name[:end]
return name.decode()
def list_ksyms():
if not (constants.LX_CONFIG_BPF and constants.LX_CONFIG_BPF_JIT):
return []
bpf_kallsyms = gdb.parse_and_eval("&bpf_kallsyms")
bpf_ksym_ptr_type = bpf_ksym_type.get_type().pointer()
return list(lists.list_for_each_entry(bpf_kallsyms,
bpf_ksym_ptr_type,
"lnode"))
class KsymAddBreakpoint(gdb.Breakpoint):
def __init__(self, monitor):
super(KsymAddBreakpoint, self).__init__("bpf_ksym_add", internal=True)
self.silent = True
self.monitor = monitor
def stop(self):
self.monitor.add(gdb.parse_and_eval("ksym"))
return False
class KsymRemoveBreakpoint(gdb.Breakpoint):
def __init__(self, monitor):
super(KsymRemoveBreakpoint, self).__init__("bpf_ksym_del",
internal=True)
self.silent = True
self.monitor = monitor
def stop(self):
self.monitor.remove(gdb.parse_and_eval("ksym"))
return False
class KsymMonitor:
def __init__(self, add, remove):
self.add = add
self.remove = remove
self.add_bp = KsymAddBreakpoint(self)
self.remove_bp = KsymRemoveBreakpoint(self)
self.notify_initial()
def notify_initial(self):
for ksym in list_ksyms():
self.add(ksym)
def delete(self):
self.add_bp.delete()
self.remove_bp.delete()
def list_progs():
if not constants.LX_CONFIG_BPF_SYSCALL:
return []
idr_rt = gdb.parse_and_eval("&prog_idr.idr_rt")
bpf_prog_ptr_type = bpf_prog_type.get_type().pointer()
progs = []
for _, slot in radixtree.for_each_slot(idr_rt):
prog = slot.dereference().cast(bpf_prog_ptr_type)
progs.append(prog)
# Subprogs are not registered in prog_idr, fetch them manually.
# func[0] is the current prog.
aux = prog["aux"]
func = aux["func"]
real_func_cnt = int(aux["real_func_cnt"])
for i in range(1, real_func_cnt):
progs.append(func[i])
return progs
class ProgAddBreakpoint(gdb.Breakpoint):
def __init__(self, monitor):
super(ProgAddBreakpoint, self).__init__("bpf_prog_kallsyms_add",
internal=True)
self.silent = True
self.monitor = monitor
def stop(self):
self.monitor.add(gdb.parse_and_eval("fp"))
return False
class ProgRemoveBreakpoint(gdb.Breakpoint):
def __init__(self, monitor):
super(ProgRemoveBreakpoint, self).__init__("bpf_prog_free_id",
internal=True)
self.silent = True
self.monitor = monitor
def stop(self):
self.monitor.remove(gdb.parse_and_eval("prog"))
return False
class ProgMonitor:
def __init__(self, add, remove):
self.add = add
self.remove = remove
self.add_bp = ProgAddBreakpoint(self)
self.remove_bp = ProgRemoveBreakpoint(self)
self.notify_initial()
def notify_initial(self):
for prog in list_progs():
self.add(prog)
def delete(self):
self.add_bp.delete()
self.remove_bp.delete()
def btf_str_by_offset(btf, offset):
while offset < btf["start_str_off"]:
btf = btf["base_btf"]
offset -= btf["start_str_off"]
if offset < btf["hdr"]["str_len"]:
return (btf["strings"] + offset).string()
return None
def bpf_line_info_line_num(line_col):
return line_col >> 10
def bpf_line_info_line_col(line_col):
return line_col & 0x3ff
class LInfoIter:
def __init__(self, prog):
# See bpf_prog_get_file_line() for details.
self.pos = 0
self.nr_linfo = 0
if prog is None:
return
self.bpf_func = int(prog["bpf_func"])
aux = prog["aux"]
self.btf = aux["btf"]
linfo_idx = aux["linfo_idx"]
self.nr_linfo = int(aux["nr_linfo"]) - linfo_idx
if self.nr_linfo == 0:
return
linfo_ptr = aux["linfo"]
tpe = linfo_ptr.type.target().array(self.nr_linfo).pointer()
self.linfo = (linfo_ptr + linfo_idx).cast(tpe).dereference()
jited_linfo_ptr = aux["jited_linfo"]
tpe = jited_linfo_ptr.type.target().array(self.nr_linfo).pointer()
self.jited_linfo = (jited_linfo_ptr + linfo_idx).cast(tpe).dereference()
self.filenos = {}
def get_code_off(self):
if self.pos >= self.nr_linfo:
return -1
return self.jited_linfo[self.pos] - self.bpf_func
def advance(self):
self.pos += 1
def get_fileno(self):
file_name_off = int(self.linfo[self.pos]["file_name_off"])
fileno = self.filenos.get(file_name_off)
if fileno is not None:
return fileno, None
file_name = btf_str_by_offset(self.btf, file_name_off)
fileno = len(self.filenos) + 1
self.filenos[file_name_off] = fileno
return fileno, file_name
def get_line_col(self):
line_col = int(self.linfo[self.pos]["line_col"])
return bpf_line_info_line_num(line_col), \
bpf_line_info_line_col(line_col)
def generate_debug_obj(ksym, prog):
name = get_ksym_name(ksym)
# Avoid read_memory(); it throws bogus gdb.MemoryError in some contexts.
start = ksym["start"]
code = start.cast(gdb.lookup_type("unsigned char")
.array(int(ksym["end"]) - int(start))
.pointer()).dereference().bytes
linfo_iter = LInfoIter(prog)
result = tempfile.NamedTemporaryFile(suffix=".o", mode="wb")
try:
with tempfile.NamedTemporaryFile(suffix=".s", mode="w") as src:
# ".loc" does not apply to ".byte"s, only to ".insn"s, but since
# this needs to work for all architectures, the latter are not an
# option. Ask the assembler to apply ".loc"s to labels as well,
# and generate dummy labels after each ".loc".
src.write(".loc_mark_labels 1\n")
src.write(".globl {}\n".format(name))
src.write(".type {},@function\n".format(name))
src.write("{}:\n".format(name))
for code_off, code_byte in enumerate(code):
if linfo_iter.get_code_off() == code_off:
fileno, file_name = linfo_iter.get_fileno()
if file_name is not None:
src.write(".file {} {}\n".format(
fileno, json.dumps(file_name)))
line, col = linfo_iter.get_line_col()
src.write(".loc {} {} {}\n".format(fileno, line, col))
src.write("0:\n")
linfo_iter.advance()
src.write(".byte {}\n".format(code_byte))
src.write(".size {},{}\n".format(name, len(code)))
src.flush()
try:
subprocess.check_call(["as", "-c", src.name, "-o", result.name])
except FileNotFoundError:
# "as" is not installed.
result.close()
return None
return result
except:
result.close()
raise

View File

@@ -170,3 +170,6 @@ LX_CONFIG(CONFIG_PAGE_OWNER)
LX_CONFIG(CONFIG_SLUB_DEBUG)
LX_CONFIG(CONFIG_SLAB_FREELIST_HARDENED)
LX_CONFIG(CONFIG_MMU)
LX_CONFIG(CONFIG_BPF)
LX_CONFIG(CONFIG_BPF_JIT)
LX_CONFIG(CONFIG_BPF_SYSCALL)

View File

@@ -11,13 +11,14 @@
# This work is licensed under the terms of the GNU GPL version 2.
#
import atexit
import gdb
import os
import re
import struct
from itertools import count
from linux import modules, utils, constants
from linux import bpf, constants, modules, utils
if hasattr(gdb, 'Breakpoint'):
@@ -114,17 +115,27 @@ class LxSymbols(gdb.Command):
The kernel (vmlinux) is taken from the current working directly. Modules (.ko)
are scanned recursively, starting in the same directory. Optionally, the module
search path can be extended by a space separated list of paths passed to the
lx-symbols command."""
lx-symbols command.
When the -bpf flag is specified, symbols from the currently loaded BPF programs
are loaded as well."""
module_paths = []
module_files = []
module_files_updated = False
loaded_modules = []
breakpoint = None
bpf_prog_monitor = None
bpf_ksym_monitor = None
bpf_progs = {}
# The remove-symbol-file command, even when invoked with -a, requires the
# respective object file to exist, so keep them around.
bpf_debug_objs = {}
def __init__(self):
super(LxSymbols, self).__init__("lx-symbols", gdb.COMMAND_FILES,
gdb.COMPLETE_FILENAME)
atexit.register(self.cleanup_bpf)
def _update_module_files(self):
self.module_files = []
@@ -197,6 +208,51 @@ lx-symbols command."""
else:
gdb.write("no module object found for '{0}'\n".format(module_name))
def add_bpf_prog(self, prog):
if prog["jited"]:
self.bpf_progs[int(prog["bpf_func"])] = prog
def remove_bpf_prog(self, prog):
self.bpf_progs.pop(int(prog["bpf_func"]), None)
def add_bpf_ksym(self, ksym):
addr = int(ksym["start"])
name = bpf.get_ksym_name(ksym)
with utils.pagination_off():
gdb.write("loading @{addr}: {name}\n".format(
addr=hex(addr), name=name))
debug_obj = bpf.generate_debug_obj(ksym, self.bpf_progs.get(addr))
if debug_obj is None:
return
try:
cmdline = "add-symbol-file {obj} {addr}".format(
obj=debug_obj.name, addr=hex(addr))
gdb.execute(cmdline, to_string=True)
except:
debug_obj.close()
raise
self.bpf_debug_objs[addr] = debug_obj
def remove_bpf_ksym(self, ksym):
addr = int(ksym["start"])
debug_obj = self.bpf_debug_objs.pop(addr, None)
if debug_obj is None:
return
try:
name = bpf.get_ksym_name(ksym)
gdb.write("unloading @{addr}: {name}\n".format(
addr=hex(addr), name=name))
cmdline = "remove-symbol-file {path}".format(path=debug_obj.name)
gdb.execute(cmdline, to_string=True)
finally:
debug_obj.close()
def cleanup_bpf(self):
self.bpf_progs = {}
while len(self.bpf_debug_objs) > 0:
self.bpf_debug_objs.popitem()[1].close()
def load_all_symbols(self):
gdb.write("loading vmlinux\n")
@@ -224,34 +280,59 @@ lx-symbols command."""
else:
[self.load_module_symbols(module) for module in module_list]
self.cleanup_bpf()
if self.bpf_prog_monitor is not None:
self.bpf_prog_monitor.notify_initial()
if self.bpf_ksym_monitor is not None:
self.bpf_ksym_monitor.notify_initial()
for saved_state in saved_states:
saved_state['breakpoint'].enabled = saved_state['enabled']
def invoke(self, arg, from_tty):
skip_decompressor()
self.module_paths = [os.path.abspath(os.path.expanduser(p))
for p in arg.split()]
monitor_bpf = False
self.module_paths = []
for p in arg.split():
if p == "-bpf":
monitor_bpf = True
else:
p.append(os.path.abspath(os.path.expanduser(p)))
self.module_paths.append(os.getcwd())
if self.breakpoint is not None:
self.breakpoint.delete()
self.breakpoint = None
if self.bpf_prog_monitor is not None:
self.bpf_prog_monitor.delete()
self.bpf_prog_monitor = None
if self.bpf_ksym_monitor is not None:
self.bpf_ksym_monitor.delete()
self.bpf_ksym_monitor = None
# enforce update
self.module_files = []
self.module_files_updated = False
self.load_all_symbols()
if not modules.has_modules():
if not hasattr(gdb, 'Breakpoint'):
gdb.write("Note: symbol update on module and BPF loading not "
"supported with this gdb version\n")
return
if hasattr(gdb, 'Breakpoint'):
if self.breakpoint is not None:
self.breakpoint.delete()
self.breakpoint = None
if modules.has_modules():
self.breakpoint = LoadModuleBreakpoint(
"kernel/module/main.c:do_init_module", self)
else:
gdb.write("Note: symbol update on module loading not supported "
"with this gdb version\n")
if monitor_bpf:
if constants.LX_CONFIG_BPF_SYSCALL:
self.bpf_prog_monitor = bpf.ProgMonitor(self.add_bpf_prog,
self.remove_bpf_prog)
if constants.LX_CONFIG_BPF and constants.LX_CONFIG_BPF_JIT:
self.bpf_ksym_monitor = bpf.KsymMonitor(self.add_bpf_ksym,
self.remove_bpf_ksym)
LxSymbols()