diff --git a/libexec/kgdb/Makefile b/libexec/kgdb/Makefile --- a/libexec/kgdb/Makefile +++ b/libexec/kgdb/Makefile @@ -1,5 +1,7 @@ FILESDIR?= /usr/libexec/kgdb -FILES= acttrace.py +FILES= acttrace.py \ + freebsd.py \ + vnet.py .include diff --git a/libexec/kgdb/README.txt b/libexec/kgdb/README.txt new file mode 100644 --- /dev/null +++ b/libexec/kgdb/README.txt @@ -0,0 +1,8 @@ +This directory contains Python scripts that can be loaded by GDB to help debug +FreeBSD kernel crashes. + +Add new commands and functions in their own files. Functions with general +utility should be added to freebsd.py. sys/tools/kernel-gdb.py is installed +into the kernel debug directory (typically /usr/lib/debug/boot/kernel). It will +be automatically loaded by kgdb when opening a vmcore, so if you add new GDB +commands or functions, that script should be updated to import them. diff --git a/libexec/kgdb/acttrace.py b/libexec/kgdb/acttrace.py --- a/libexec/kgdb/acttrace.py +++ b/libexec/kgdb/acttrace.py @@ -6,21 +6,10 @@ # import gdb +from freebsd import * -def symval(name): - return gdb.lookup_global_symbol(name).value() - - -def tid_to_gdb_thread(tid): - for thread in gdb.inferiors()[0].threads(): - if thread.ptid[2] == tid: - return thread - else: - return None - - -def all_pcpus(): +def pcpu_foreach(): mp_maxid = symval("mp_maxid") cpuid_to_pcpu = symval("cpuid_to_pcpu") @@ -33,6 +22,12 @@ class acttrace(gdb.Command): + """ + Register an acttrace command with gdb. + + When run, acttrace prints the stack trace of all threads that were on-CPU + at the time of the panic. + """ def __init__(self): super(acttrace, self).__init__("acttrace", gdb.COMMAND_USER) @@ -40,7 +35,7 @@ # Save the current thread so that we can switch back after. curthread = gdb.selected_thread() - for pcpu in all_pcpus(): + for pcpu in pcpu_foreach(): td = pcpu['pc_curthread'] tid = td['td_tid'] diff --git a/libexec/kgdb/freebsd.py b/libexec/kgdb/freebsd.py new file mode 100644 --- /dev/null +++ b/libexec/kgdb/freebsd.py @@ -0,0 +1,59 @@ +#- +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2025 Mark Johnston +# + + +import gdb + +def symval(name): + sym = gdb.lookup_global_symbol(name) + if sym is None: + sym = gdb.lookup_static_symbol(name) + if sym is None: + raise gdb.GdbError(f"Symbol '{name}' not found") + return sym.value() + + +def _queue_foreach(head, field, headf, nextf): + elm = head[headf] + while elm != 0: + yield elm + elm = elm[field][nextf] + + +def list_foreach(head, field): + """sys/queue.h-style iterator.""" + return _queue_foreach(head, field, "lh_first", "le_next") + + +def tailq_foreach(head, field): + """sys/queue.h-style iterator.""" + return _queue_foreach(head, field, "tqh_first", "tqe_next") + + +def tid_to_gdb_thread(tid): + """Convert a FreeBSD kernel thread ID to a gdb inferior thread.""" + for thread in gdb.inferiors()[0].threads(): + if thread.ptid[2] == tid: + return thread + else: + return None + + +def tdfind(tid, pid=-1): + """Convert a FreeBSD kernel thread ID to a struct thread pointer.""" + td = tdfind.cached_threads.get(int(tid)) + if td: + return td + + for p in list_foreach(symval("allproc"), "p_list"): + if pid != -1 and pid != p['p_pid']: + continue + for td in tailq_foreach(p['p_threads'], "td_plist"): + ntid = td['td_tid'] + tdfind.cached_threads[int(ntid)] = td + if ntid == tid: + return td +tdfind.cached_threads = dict() diff --git a/libexec/kgdb/vnet.py b/libexec/kgdb/vnet.py new file mode 100644 --- /dev/null +++ b/libexec/kgdb/vnet.py @@ -0,0 +1,40 @@ +#- +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2025 Mark Johnston +# + +import gdb +from freebsd import * + +class vnet(gdb.Function): + """ + Register a function to look up VNET variables by name. + + To look at the value of a VNET variable V_foo, print $V("foo"). + The currently selected thread's VNET is used by default, but can + be optionally specified as a second parameter, e.g., $V("foo", vnet0). + """ + def __init__(self): + super(vnet, self).__init__("V") + + def invoke(self, sym, vnet=None): + sym = sym.string() + if sym.startswith("V_"): + sym = sym[len("V_"):] + if gdb.lookup_symbol("sysctl___kern_features_vimage")[0] is None: + return symval(sym) + + if vnet is None: + vnet = tdfind(gdb.selected_thread().ptid[2])['td_vnet'] + if not vnet: + # If curthread->td_vnet == NULL, vnet0 is the current vnet. + vnet = symval("vnet0") + base = vnet['vnet_data_base'] + entry = symval("vnet_entry_" + sym) + entry_addr = entry.address.cast(gdb.lookup_type("uintptr_t")) + ptr = gdb.Value(base + entry_addr).cast(entry.type.pointer()) + return ptr.dereference() + + +vnet() diff --git a/sys/conf/kern.post.mk b/sys/conf/kern.post.mk --- a/sys/conf/kern.post.mk +++ b/sys/conf/kern.post.mk @@ -428,6 +428,8 @@ .if defined(DEBUG) && !defined(INSTALL_NODEBUG) && ${MK_KERNEL_SYMBOLS} != "no" mkdir -p ${DESTDIR}${KERN_DEBUGDIR}${KODIR} ${INSTALL} -p -m ${KMODMODE} -o ${KMODOWN} -g ${KMODGRP} ${KERNEL_KO}.debug ${DESTDIR}${KERN_DEBUGDIR}${KODIR}/ + ${INSTALL} -p -m ${KMODMODE} -o ${KMODOWN} -g ${KMODGRP} \ + $S/tools/kernel-gdb.py ${DESTDIR}${KERN_DEBUGDIR}${KODIR}/${KERNEL_KO}-gdb.py .endif .if defined(KERNEL_EXTRA_INSTALL) ${INSTALL} -p -m ${KMODMODE} -o ${KMODOWN} -g ${KMODGRP} ${KERNEL_EXTRA_INSTALL} ${DESTDIR}${KODIR}/ diff --git a/sys/tools/kernel-gdb.py b/sys/tools/kernel-gdb.py new file mode 100644 --- /dev/null +++ b/sys/tools/kernel-gdb.py @@ -0,0 +1,12 @@ +#- +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2025 Mark Johnston +# + +import sys +sys.path.append("/usr/libexec/kgdb") + +# Import FreeBSD kernel debugging commands and modules below. +import acttrace +import vnet diff --git a/usr.sbin/crashinfo/crashinfo.sh b/usr.sbin/crashinfo/crashinfo.sh --- a/usr.sbin/crashinfo/crashinfo.sh +++ b/usr.sbin/crashinfo/crashinfo.sh @@ -220,7 +220,6 @@ scriptdir=/usr/libexec/kgdb echo "bt -full" >> $file - echo "source ${scriptdir}/acttrace.py" >> $file echo "acttrace" >> $file echo "quit" >> $file ${GDB%gdb}kgdb -q $KERNEL $VMCORE < $file