A pip-installable, project-agnostic GDB extension framework for embedded debugging
Project description
weargdb
A pip-installable, project-agnostic GDB extension framework for embedded debugging.
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:
import gdb— only resolvable inside GDB's embedded Python.- Subclass
gdb.Commandand 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]
weargdbcannot be imported by the plain system Python — there is nogdbmodule there. Importing it outside GDB raises a clearImportErrortelling 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 installputsweargdbon the system Python's path. GDB's embedded Python must share that path forpy import weargdbto resolve. This works out of the box when GDB is linked against the same Python that ranpip. Ifpy import weargdbreportsNo module named 'weargdb', either usesource gdbinit.pyfrom 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_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_prop [key] |
Read a build.prop property from the etc romfs (dump all if no key) | No (reads .rodata) |
wear_rom ls |
List every file in the etc romfs baked into the ELF | No (reads .rodata) |
wear_rom cat <path> |
Print one file's contents from the etc romfs | 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 ononce, so GDB lays out structs and arrays on multiple indented lines (this is how the struct-dumping commands stay readable). It mirrors the startupsettweaks pynuttx applies. If you prefer the compact one-line layout, runset print pretty offafter 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.attributessection, which ARM GCC/Clang emit by default.wear_cflagswith 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 andcomp_dirfor 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_symprints 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.
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 (with ls / cat sub-commands) and wear_prop parse that
-rom1fs- image straight out of the ELF — no coredump needed:
(gdb) wear_rom ls # list the full romfs tree
SIZE PATH
233 /build.prop
554 /init.d/rcS
18887 /font_config.json
...
(gdb) wear_rom cat /init.d/rcS # print any file's contents (text or binary)
set +e
uname -a > /dev/log
...
(gdb) wear_prop ro.build.version # shortcut for a single build.prop key
ro.build.version = 2022.06.03
(gdb) wear_prop # or dump every ro.* property
wear_rom is a prefix command — type wear_rom <TAB> to complete its
sub-commands. wear_rom cat prints text files directly and falls back to a
hex-dump for binary content. wear_prop is a convenience layer over build.prop
specifically; for any other file use wear_rom cat.
[!NOTE] These read the romfs of the currently loaded ELF. A given file (e.g.
key.avbor modem XMLs that the board CMake adds viaadd_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-sourceit — the shim clears weargdb's cached modules first, so your edits take effect without a restart. Forpy 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_symbolandparse_and_evalof 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
│ ├── prop.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). |
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file weargdb-0.0.6.tar.gz.
File metadata
- Download URL: weargdb-0.0.6.tar.gz
- Upload date:
- Size: 41.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
870da8f7615b1a7c6b7e5c74d5cdc1ff301141ed0fa747ae853e0cbcfaf78f7c
|
|
| MD5 |
bb88cf3b439647697af5ff2d844f5737
|
|
| BLAKE2b-256 |
61b706e5dcb2816b6ba62460e9cd4f3c85fd737a4876b2136be083540c2a3388
|
File details
Details for the file weargdb-0.0.6-py3-none-any.whl.
File metadata
- Download URL: weargdb-0.0.6-py3-none-any.whl
- Upload date:
- Size: 39.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c70069ee542322eb40752aca1976b4bd5ab05939767f0139e4861b15e8e58eb7
|
|
| MD5 |
c2f1008b1c2d59cbd363df2fc311b1ef
|
|
| BLAKE2b-256 |
7cdf76c8391444ead741694eda25dba89f03ace44f74ac9b56e193133973e3f8
|