Skip to main content

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.

License: Apache 2.0 Python

How it worksInstallCommandsUsageExtendTroubleshooting

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_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 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/.rodatanot 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.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
│           ├── 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 weargdbNo 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

weargdb-0.0.6.tar.gz (41.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

weargdb-0.0.6-py3-none-any.whl (39.0 kB view details)

Uploaded Python 3

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

Hashes for weargdb-0.0.6.tar.gz
Algorithm Hash digest
SHA256 870da8f7615b1a7c6b7e5c74d5cdc1ff301141ed0fa747ae853e0cbcfaf78f7c
MD5 bb88cf3b439647697af5ff2d844f5737
BLAKE2b-256 61b706e5dcb2816b6ba62460e9cd4f3c85fd737a4876b2136be083540c2a3388

See more details on using hashes here.

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

Hashes for weargdb-0.0.6-py3-none-any.whl
Algorithm Hash digest
SHA256 c70069ee542322eb40752aca1976b4bd5ab05939767f0139e4861b15e8e58eb7
MD5 c2f1008b1c2d59cbd363df2fc311b1ef
BLAKE2b-256 7cdf76c8391444ead741694eda25dba89f03ace44f74ac9b56e193133973e3f8

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page