Skip to main content

weargdb

A pip-installable, project-agnostic GDB extension framework for embedded debugging.

License: Apache 2.0 Python

How it works • Install • Commands • Usage • Extend • Troubleshooting

After installation, a single py import weargdb (or source gdbinit.py from a source checkout) inside GDB makes every bundled command available at the (gdb) prompt. The commands read symbols, sections, and memory straight out of the loaded ELF or coredump using GDB's Python API — handy for inspecting NuttX/Vela firmware crash dumps, but tied to nothing specific, so they work against any ELF.

How it works

GDB ships with an embedded Python interpreter and exposes a built-in gdb module to it. A package becomes a "GDB extension" (rather than an ordinary command-line tool) when it does two things:

  1. import gdb — only resolvable inside GDB's embedded Python.
  2. Subclass gdb.Command and instantiate the subclass — instantiation is what registers the command into GDB's command table.

weargdb does both on import, so py import weargdb is all you need.

[!NOTE] weargdb cannot be imported by the plain system Python — there is no gdb module there. Importing it outside GDB raises a clear ImportError telling you to run it inside GDB. This is expected, not a bug.

Two ways to load it: import and source

GDB offers two ways to run a Python file. weargdb is a package (its __init__.py does from .commands import register_all, and the command modules do relative imports like from .common import hexdump), so the two entry points play different roles:

Command GDB runs the file as When to use
py import weargdb a module (package import) after pip install — weargdb is on GDB's Python path
source gdbinit.py a top-level script a source checkout, no install needed

A source-d file runs as a top-level script (__name__ == "__main__"), so a bare source src/weargdb/__init__.py would fail — its relative imports have no parent package. gdbinit.py is the shim that bridges this: it prepends src/ to sys.path and then does a normal package import weargdb, which resolves the relative imports correctly. So:

(gdb) source /path/to/weargdb/gdbinit.py     # works from a source tree

does the same job as py import weargdb does after an install. Both end up running register_all(), which registers every command into GDB.

Installation

# From a local checkout (editable -- code changes take effect on next GDB start)
pip install -e .

# Or a regular install
pip install .

The gdb module is provided by GDB at runtime and is intentionally not a PyPI dependency, so pip never tries to fetch it.

[!IMPORTANT] pip install puts weargdb on the system Python's path. GDB's embedded Python must share that path for py import weargdb to resolve. This works out of the box when GDB is linked against the same Python that ran pip. If py import weargdb reports No module named 'weargdb', either use source gdbinit.py from the source checkout (no path sharing needed) or see Troubleshooting.

Commands

Command What it does Needs a core?
wear_hello [args] Demo command that echoes its arguments No
wear_ver Print the weargdb extension version No
wear_sym <name> Resolve a symbol to its type and address No (reads ELF)
wear_sections List ELF sections with load addresses and sizes No (reads ELF)
wear_arch Report the target architecture and ARM core name No (reads ELF)
wear_cflags [file] Compiler version + compile flags; grouped overview, or per-file flags when given a source-path substring No (reads ELF)
wear_config [name] [-a] Query CONFIG_* build options and their values, recovered from DWARF (-g3); count when bare, matching options when given a name, or every option with -a No (reads ELF)
wear_dump <expr> [nbytes] Hex-dump raw memory at a C expression's address Yes (reads memory)
wear_struct <expr> Pretty-print a struct/union field by field; byte arrays as hex + ASCII Yes (reads memory)
wear_buildinfo Print a build-info string baked into the firmware No (reads .rodata)
wear_rom [path] List the etc romfs baked into the ELF, or print one file's contents when given a path No (reads .rodata)

Usage

$ gdb-multiarch -q
(gdb) py import weargdb
[weargdb] loaded 10 GDB commands successfully

(gdb) wear_ver
weargdb GDB extension v0.0.5

(gdb) help user-defined          # lists every command weargdb registered

[!NOTE] Loading weargdb runs set print pretty on once, so GDB lays out structs and arrays on multiple indented lines (this is how the struct-dumping commands stay readable). It mirrors the startup set tweaks pynuttx applies. If you prefer the compact one-line layout, run set print pretty off after loading.

Inspecting the loaded ELF

These commands read the ELF that GDB has loaded (gdb a.out or (gdb) file a.out) — no running inferior or coredump required:

(gdb) wear_sections            # list ELF sections with load addresses + sizes

(gdb) wear_arch                # architecture, from the ELF header + .ARM.attributes
Class:      ELF32
Endianness: little
Machine:    ARM (e_machine=40)
ARM attributes (from .ARM.attributes):
    CPU_name: Cortex-M55
    CPU_arch: 14
    FP_arch: 6

(gdb) wear_cflags              # no arg: overview, grouped by distinct flag set
Compiler: GCC: (GNU) 12.2.0

28 distinct flag sets across 5205 files:

[1] 3697 files | GNU C17 13.2.1 20231009 ... -Os ... -ffunction-sections ...
    ../../nuttx/openamp/libmetal/lib/device.c
    ../../nuttx/openamp/libmetal/lib/init.c
    ... (+3692 more)
...
tip: wear_cflags <file>  -> exact flags for one source file

(gdb) wear_cflags arm_cache    # a source-path substring: flags for that file
1 match(es) for 'arm_cache':

../../nuttx/arch/arm/src/armv8-m/arm_cache.c
    GNU C17 13.2.1 20231009 -mtune=cortex-m55 ... -Os ... --param=min-pagesize=0
    comp_dir: /home/work/data/miui_codes/build_home_rom/cmake_out/p62lte_ap

(gdb) wear_sym nx_start        # function symbol -> its type and address
nx_start: type=void (void), address=0x...

(gdb) wear_sym g_some_global   # data symbol -> type, address, and ELF value
g_some_global: type=int, address=0x...
  value = 0

[!NOTE] The exact ARM core name (e.g. Cortex-M55) comes from the .ARM.attributes section, which ARM GCC/Clang emit by default. wear_cflags with no argument prints the whole-image command line when the firmware was built with -frecord-gcc-switches (section .GCC.command.line); otherwise it walks the DWARF compile units (-g) and groups the source files by the flag set they were built with. Pass a source-path substring (e.g. wear_cflags arm_cache) to print the exact flags and comp_dir for the matching compile unit(s) — handy for confirming one file's optimization level or compiler version. If neither is recorded, it says so.

[!WARNING] For a data symbol, wear_sym prints the initializer baked into the ELF's .data/.rodata — not the runtime value. To see the value at crash time, load a coredump first (target core x.core / target nxstub), then query the symbol.

Checking which CONFIG_* options a build was compiled with

wear_config answers "was this option enabled, and to what value?" straight from the ELF — no need to find the original defconfig. It works because a firmware built with -g3 records every #define in DWARF; wear_config reads those out of .debug_str.

(gdb) wear_config                         # how many options are recorded
2937 CONFIG_ options recorded in this ELF.
tip: wear_config <name>  -> matching options + values; wear_config -a  -> all of them

(gdb) wear_config -a                       # every option, sorted (long! ~2937 lines)
CONFIG_A7_SRAM_SIZE=0x80000
CONFIG_ADC=1
...
2937 CONFIG_ options.

(gdb) wear_config MM_RECORD                # substring -> all matching options
4 option(s) matching 'MM_RECORD':
    CONFIG_MM_RECORD_PID=1
    CONFIG_MM_RECORD_STACK=1
    CONFIG_MM_RECORD_STACK_DEFAULT=1
    CONFIG_MM_RECORD_STACK_SKIP=3

(gdb) wear_config MM_RECORD_PID            # exact name -> just that one
CONFIG_MM_RECORD_PID=1

(gdb) wear_config CONFIG_LOG_SERVICE_LOG_PART   # CONFIG_ prefix is optional
CONFIG_LOG_SERVICE_LOG_PART="/data"

(gdb) wear_config MM_RECORD_BACKTRACE      # option exists but is not set here
CONFIG_MM_RECORD_BACKTRACE: not found -- not set / disabled in this build (or the name is misspelled).

[!NOTE] A boolean X=y in the defconfig becomes #define X 1, so an enabled option reads back as =1; numeric and string options keep their literal value (=4096, ="/data"). An option that is not set has no #define at all, so it simply does not appear — wear_config reports that as not found, which is the answer: disabled. Values not written in the defconfig but left at their Kconfig default (e.g. CONFIG_MM_RECORD_STACK_SKIP=3 above) are still recorded, because the build's generated config.h carries them.

[!WARNING] wear_config needs the macros in DWARF, which only -g3 emits (it adds the .debug_macro section). A -g/-g2 or stripped build records no #defines, so wear_config finds nothing and says so. Check with wear_sections | grep debug_macro if in doubt.

Dumping memory at a symbol or address

wear_dump <expr> [nbytes] evaluates a C expression to an address and hex-dumps the bytes there (default 64). Unlike the ELF-only commands above, this reads live/core memory, so it needs a running inferior or a loaded coredump:

(gdb) wear_dump &g_some_global 32    # dump 32 bytes at the variable's address
0x20001000  01 00 00 00 2a 00 00 00 ...                       ....*...
(gdb) wear_dump 0x20001000           # a bare address works too (default 64 B)
(gdb) wear_dump g_tcb->stack_alloc   # any C expression GDB can evaluate

Argument parsing is handled by the shared ArgCommand base (argparse on top of gdb.string_to_argv), so the command body chains the three GDB Python APIs that do the real work: gdb.parse_and_eval (expr -> gdb.Value), int(value) / value.address (get the address), and gdb.selected_inferior().read_memory (read raw bytes).

Pretty-printing a struct field by field

wear_struct <expr> is the typed counterpart to wear_dump: instead of a flat wall of bytes, it walks the type of the expression and prints each field with its offset, name and value. The layout is read straight from the ELF's DWARF, so nothing about any specific struct is hard-coded — feed it any struct or union the loaded ELF describes. A pointer-to-struct is dereferenced automatically, so wear_struct p and wear_struct *p behave alike. Like wear_dump, it reads live/core memory, so it needs a coredump or running inferior.

The one thing GDB's own print gets wrong is a uint8_t[] field: it mangles the bytes into a truncated C string. wear_struct renders every 1-byte array as a hex + ASCII view instead — so binary fields (an EID) and text fields (an IMEI) are both readable at a glance:

(gdb) wear_struct lpa_nv
lpa_nv: lpa_nv_t @ 0x185aaffe (123 bytes)
  +0x000 eid (uint8_t [16]):
0x185aaffe  89 03 30 23 42 61 00 00 00 00 05 41 16 87 17 22  ..0#Ba.....A..."
  +0x010 imei (uint8_t [16]):
0x185ab00e  38 36 31 30 33 39 30 38 30 30 33 34 31 33 31 00  861039080034131.
  +0x07a status (uint8_t) = 0 '\000'

It accepts any expression GDB can evaluate (wear_struct *some_ptr, wear_struct g_tcb->xcp); a non-struct expression is rejected with a hint to use wear_dump/print instead. The gdb-independent part — deciding whether a field is a 1-byte array worth hex-dumping (_is_byte_array) — is unit-tested in tests/test_struct.py; the byte arrays themselves reuse the same hexdump helper as wear_dump.

Reading a build-info string baked into the firmware

wear_buildinfo reads a single global string the firmware exports at compile time, the same way NuttX's own uname reads g_version out of the ELF. The C side and this command are coupled only by the symbol name g_build_info — keep them in sync. Because the string lives in .rodata, a bare ELF is enough (no coredump needed):

(gdb) wear_buildinfo
Jun  1 2026 12:00:00 bt

To export the symbol, define one global string in your firmware (compile-time values, no runtime code):

/* Pick the variant from whatever build macro distinguishes your targets. */
#ifdef CONFIG_TELEPHONY
#  define BUILD_VARIANT "esim"
#else
#  define BUILD_VARIANT "bt"
#endif

/* __attribute__((used)) stops LTO from dropping it when nothing references it
   -- otherwise the symbol may be optimized out and the command finds nothing. */
const char g_build_info[] __attribute__((used)) =
    __DATE__ " " __TIME__ " " BUILD_VARIANT;

The command uses gdb.lookup_global_symbol to find the symbol and gdb.Value.string() to read the NUL-terminated char[] as a Python string.

Browsing the etc romfs baked into the firmware

When the firmware is built with CONFIG_ETC_ROMFS=y, NuttX bakes the whole /etc directory into the ELF as a romfs_img[] byte array in .rodata. The wear_rom command parses that -rom1fs- image straight out of the ELF — no coredump needed:

(gdb) wear_rom                        # list the full romfs tree
   SIZE  PATH
    233  /build.prop
    554  /init.d/rcS
  18887  /font_config.json
...

(gdb) wear_rom /init.d/rcS            # print any file's contents (text or binary)
set +e
uname -a > /dev/log
...

(gdb) wear_rom /build.prop            # build.prop is just a file: prints every property
ro.build.version=2022.06.03
...

With no argument wear_rom lists every file; given a path it prints that file, decoding text directly and falling back to a hex-dump for binary content. build.prop is an ordinary file in the listing, so wear_rom /build.prop shows all its properties.

[!NOTE] These read the romfs of the currently loaded ELF. A given file (e.g. key.avb or modem XMLs that the board CMake adds via add_board_rcraws) only shows up if it was packed into that core's etc romfs. To inspect another image's files, load the matching ELF (e.g. the recovery/bl2 ELF).

Load automatically on every GDB start

After a pip install, add to ~/.gdbinit:

python
import weargdb
end

Or, to load straight from a source checkout with no install, point ~/.gdbinit at the shim:

source /path/to/weargdb/gdbinit.py

Adding your own command

Drop a new file in src/weargdb/commands/ — one gdb.Command subclass per file:

# src/weargdb/commands/mycmd.py
import gdb


class WeargdbMyCmd(gdb.Command):
    """wear_mycmd -- one-line description."""

    def __init__(self):
        super().__init__("wear_mycmd", gdb.COMMAND_USER)

    def invoke(self, arg, from_tty):
        gdb.write("hello from wear_mycmd\n")

That's it — there is no list to edit and no registration call to add. register_all() auto-discovers every gdb.Command subclass defined in the commands/ package on import. If two commands need to share logic, put the shared code in src/weargdb/commands/common.py and import it (from .common import ...) rather than importing one command file from another.

[!TIP] After editing, the simplest way to pick up the change is to restart GDB. If you loaded weargdb via source gdbinit.py, just re-source it — the shim clears weargdb's cached modules first, so your edits take effect without a restart. For py import weargdb, Python caches the module, so clear the cache before re-importing:

(gdb) python import sys; [sys.modules.pop(m) for m in list(sys.modules) if m.startswith("weargdb")]
(gdb) py import weargdb

GDB Python API cheat sheet

The APIs the bundled commands use, plus the ones you will most likely reach for when writing your own:

API Purpose
gdb.Command Base class for a custom command; instantiating a subclass registers it
Command.invoke(self, arg, from_tty) Called when the command runs; arg is the raw argument string
gdb.write(s) Print to GDB's output stream (use instead of print())
gdb.string_to_argv(arg) Split an arg string into a list the way GDB does (honours quoting)
gdb.execute(cmd, to_string=True) Run a GDB command; capture its output as a string
gdb.lookup_global_symbol(name) Look up a global symbol in the ELF; returns gdb.Symbol or None
gdb.parse_and_eval(expr) Evaluate any C expression to a gdb.Value (e.g. "g_foo->bar")
gdb.selected_inferior().read_memory(addr, n) Read n raw bytes (needs a live inferior or core)
gdb.Symbol.value() / .type / .is_function A symbol's value (gdb.Value), its type, whether it is a function
gdb.Value.address / int(val) / str(val) The value's address; convert a gdb.Value to Python int / str
gdb.Value.type.code The type's kind, compared against gdb.TYPE_CODE_PTR / _ARRAY / _FUNC / ...
gdb.lookup_type("struct tcb_s") Get a gdb.Type, often used with value.cast(type)
gdb.objfiles() List of loaded object files (e.g. to check whether an ELF is loaded yet)

[!NOTE] lookup_global_symbol and parse_and_eval of a global work on a bare ELF, but they give the link-time value. Anything that reads memory (read_memory) or runtime state needs a live inferior or a loaded coredump.

Project layout

weargdb/
├── pyproject.toml          # PEP 621 metadata, hatchling backend, no runtime deps
├── README.md
├── LICENSE                 # Apache 2.0
├── gdbinit.py              # `source` entry point for a source checkout (no install)
├── src/
│   └── weargdb/
│       ├── __init__.py     # imports gdb, calls register_all() on import
│       └── commands/       # one gdb.Command subclass per file
│           ├── __init__.py # register_all() -- auto-discovers every command
│           ├── common.py   # shared code (hexdump, romfs parser, ArgCommand base)
│           ├── hello.py
│           ├── ver.py
│           ├── sym.py
│           ├── sections.py
│           ├── dump.py
│           ├── buildinfo.py
│           └── rom.py
└── tests/
    ├── test_hexdump.py     # pure-Python tests (stub the gdb module)
    └── test_argcommand.py  # ArgCommand argparse base tests (stub the gdb module)

Testing

The commands depend on GDB's embedded gdb module, so the gdb-independent logic is what gets unit-tested under a plain pytest run: the hex-dump formatter and the ArgCommand argparse base in commands/common.py. Each test injects a stub gdb module before importing. pyproject.toml points pytest at src/, so the tests always run against the source tree rather than any installed copy.

pip install -e '.[dev]'
pytest

Troubleshooting

Symptom Cause Fix
py import weargdb → No module named 'weargdb' GDB's Python differs from the Python pip installed into Use source gdbinit.py from the source checkout, or find GDB's Python with gdb -ex "py import sys; print(sys.path)" and pip install into that interpreter.
import weargdb from a normal shell python3 fails Expected — the gdb module only exists inside GDB Run inside GDB, not the system Python.
Edited a command but GDB still runs the old one Python cached the module Restart GDB; or re-source gdbinit.py (it clears the cache); or for py import, clear the cache then re-import (see Adding your own command).

Release files for weargdb 0.0.8

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for weargdb 0.0.8
File Size Uploaded
weargdb-0.0.8.tar.gz 51.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for weargdb 0.0.8
File Interpreter ABI Platform
weargdb-0.0.8-py3-none-any.whl Python 3 none any Details

Total release size: 98.5 kB

Release files / weargdb-0.0.8.tar.gz

Download URL weargdb-0.0.8.tar.gz
Size 51.9 kB
Tags Source
SHA-256 checksum
How to use checksums
5277c635b15b1429556d894ff43c4310d393e0db97bfb491fbaaf4e3943efc80
BLAKE2b-256 checksum
How to use checksums
d58dbb83ea5c611aad259570adb788d041271f7199420346326cb1906b2275aa
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.10.12

Release files / weargdb-0.0.8-py3-none-any.whl

Download URL weargdb-0.0.8-py3-none-any.whl
Size 46.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
57da186c0cc854bdf61cf347210c37004fe392c49bacbddefbbe1b125accbd02
BLAKE2b-256 checksum
How to use checksums
75a718f053d277af09d1b65c6430ebdb19d845eb109b7ba84110f40ccf48bad9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.10.12

Release history Release notifications | RSS feed

This release

0.0.8 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page