diff --git a/sys/tools/gdb/acttrace.py b/sys/tools/gdb/acttrace.py index fdd18a4833cd..da79fda59da1 100644 --- a/sys/tools/gdb/acttrace.py +++ b/sys/tools/gdb/acttrace.py @@ -1,46 +1,48 @@ # # Copyright (c) 2022 The FreeBSD Foundation # # This software was developed by Mark Johnston under sponsorship from the # FreeBSD Foundation. # # SPDX-License-Identifier: BSD-2-Clause # import gdb from freebsd import * from pcpu import * + class acttrace(gdb.Command): """ Print 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) def invoke(self, arg, from_tty): # Save the current thread so that we can switch back after. curthread = gdb.selected_thread() for pcpu in pcpu_foreach(): td = pcpu['pc_curthread'] tid = td['td_tid'] gdb_thread = tid_to_gdb_thread(tid) if gdb_thread is None: raise gdb.error(f"failed to find GDB thread with TID {tid}") else: gdb_thread.switch() p = td['td_proc'] print("Tracing command {} pid {} tid {} (CPU {})".format( p['p_comm'], p['p_pid'], td['td_tid'], pcpu['pc_cpuid'])) gdb.execute("bt") print() curthread.switch() # Registers the command with gdb, doesn't do anything. acttrace() diff --git a/sys/tools/gdb/freebsd.py b/sys/tools/gdb/freebsd.py index 81ea60373348..f88eef876c7f 100644 --- a/sys/tools/gdb/freebsd.py +++ b/sys/tools/gdb/freebsd.py @@ -1,75 +1,78 @@ # # Copyright (c) 2025 Mark Johnston # # SPDX-License-Identifier: BSD-2-Clause # 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 linker_file_foreach(): """Iterate over loaded linker files.""" return tailq_foreach(symval("linker_files"), "link") def pcpu_foreach(): mp_maxid = symval("mp_maxid") cpuid_to_pcpu = symval("cpuid_to_pcpu") cpu = 0 while cpu <= mp_maxid: pcpu = cpuid_to_pcpu[cpu] if pcpu: yield pcpu cpu = cpu + 1 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/sys/tools/gdb/pcpu.py b/sys/tools/gdb/pcpu.py index 94c451e6eca5..08ae81e5121e 100644 --- a/sys/tools/gdb/pcpu.py +++ b/sys/tools/gdb/pcpu.py @@ -1,77 +1,80 @@ # # Copyright (c) 2025 Mark Johnston # # SPDX-License-Identifier: BSD-2-Clause # import gdb from freebsd import * + class pcpu(gdb.Function): """ A function to look up PCPU and DPCPU fields by name. To look up the value of the PCPU field foo on CPU n, use $PCPU("foo", n). This works for DPCPU fields too. If the CPU ID is omitted, and the currently selected thread is on-CPU, that CPU is used, otherwise an error is raised. """ + def __init__(self): super(pcpu, self).__init__("PCPU") def invoke(self, field, cpuid=-1): if cpuid == -1: cpuid = tdfind(gdb.selected_thread().ptid[2])['td_oncpu'] if cpuid == -1: raise gdb.error("Currently selected thread is off-CPU") if cpuid < 0 or cpuid > symval("mp_maxid"): raise gdb.error(f"Currently selected on invalid CPU {cpuid}") pcpu = symval("cpuid_to_pcpu")[cpuid] # Are we dealing with a PCPU or DPCPU field? field = field.string() for f in gdb.lookup_type("struct pcpu").fields(): if f.name == "pc_" + field: return pcpu["pc_" + field] def uintptr_t(val): return val.cast(gdb.lookup_type("uintptr_t")) # We're dealing with a DPCPU field. This is handled similarly # to VNET symbols, see vnet.py for comments. pcpu_base = pcpu['pc_dynamic'] pcpu_entry = symval("pcpu_entry_" + field) pcpu_entry_addr = uintptr_t(pcpu_entry.address) for lf in linker_file_foreach(): block = gdb.block_for_pc(lf['ops']['cls']['methods'][0]['func']) elf_file_t = gdb.lookup_type("elf_file_t", block).target() ef = lf.cast(elf_file_t) file_type = lf['ops']['cls']['name'].string() if file_type == "elf64": start = uintptr_t(ef['pcpu_start']) if start == 0: continue end = uintptr_t(ef['pcpu_stop']) base = uintptr_t(ef['pcpu_base']) elif file_type == "elf64_obj": for i in range(ef['nprogtab']): pe = ef['progtab'][i] if pe['name'].string() == "set_pcpu": start = uintptr_t(pe['origaddr']) end = start + uintptr_t(pe['size']) base = uintptr_t(pe['addr']) break else: continue else: path = lf['pathname'].string() raise gdb.error(f"{path} has unexpected linker file type {file_type}") if pcpu_entry_addr >= start and pcpu_entry_addr < end: obj = gdb.Value(pcpu_base + pcpu_entry_addr - start + base) return obj.cast(pcpu_entry.type.pointer()).dereference() + # Register with gdb. pcpu() diff --git a/sys/tools/gdb/vnet.py b/sys/tools/gdb/vnet.py index 5f416b2a515a..6175a5d6f551 100644 --- a/sys/tools/gdb/vnet.py +++ b/sys/tools/gdb/vnet.py @@ -1,100 +1,101 @@ # # Copyright (c) 2025 Mark Johnston # # SPDX-License-Identifier: BSD-2-Clause # import gdb -import traceback from freebsd import * + class vnet(gdb.Function): """ 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", ), where is a pointer to a struct vnet (e.g., vnet0 or allprison.tqh_first->pr_vnet) or a string naming a jail. """ + 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) # Look up the VNET's base address. 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") elif vnet.type.is_string_like: vnet = vnet.string() for prison in tailq_foreach(symval("allprison"), "pr_list"): if prison['pr_name'].string() == vnet: vnet = prison['pr_vnet'] break else: raise gdb.error(f"No prison named {vnet}") def uintptr_t(val): return val.cast(gdb.lookup_type("uintptr_t")) # Now the tricky part: compute the address of the symbol relative # to the selected VNET. In the compiled kernel this is done at # load time by applying a magic transformation to relocations # against symbols in the vnet linker set. Here we have to apply # the transformation manually. vnet_data_base = vnet['vnet_data_base'] vnet_entry = symval("vnet_entry_" + sym) vnet_entry_addr = uintptr_t(vnet_entry.address) # First, which kernel module does the symbol belong to? for lf in linker_file_foreach(): # Find the bounds of this linker file's VNET linker set. The # struct containing the bounds depends on the type of the linker # file, and unfortunately both are called elf_file_t. So we use a # PC value from the compilation unit (either link_elf.c or # link_elf_obj.c) to disambiguate. block = gdb.block_for_pc(lf['ops']['cls']['methods'][0]['func']) elf_file_t = gdb.lookup_type("elf_file_t", block).target() ef = lf.cast(elf_file_t) file_type = lf['ops']['cls']['name'].string() if file_type == "elf64": start = uintptr_t(ef['vnet_start']) if start == 0: # This linker file doesn't have a VNET linker set. continue end = uintptr_t(ef['vnet_stop']) base = uintptr_t(ef['vnet_base']) elif file_type == "elf64_obj": for i in range(ef['nprogtab']): pe = ef['progtab'][i] if pe['name'].string() == "set_vnet": start = uintptr_t(pe['origaddr']) end = start + uintptr_t(pe['size']) base = uintptr_t(pe['addr']) break else: # This linker file doesn't have a VNET linker set. continue else: path = lf['pathname'].string() raise gdb.error(f"{path} has unexpected linker file type {file_type}") if vnet_entry_addr >= start and vnet_entry_addr < end: # The symbol belongs to this linker file, so compute the final # address. obj = gdb.Value(vnet_data_base + vnet_entry_addr - start + base) return obj.cast(vnet_entry.type.pointer()).dereference() # Register with gdb. vnet()