Metadata-Version: 2.4
Name: weargdb
Version: 0.0.6
Summary: A pip-installable, project-agnostic GDB extension framework for embedded debugging
Project-URL: Homepage, https://github.com/Junbo-Zheng/weargdb
Project-URL: Issues, https://github.com/Junbo-Zheng/weargdb/issues
Author-email: Junbo Zheng <3273070@qq.com>
License: Apache-2.0
License-File: LICENSE
Keywords: debugging,embedded,framework,gdb,gdb-extension
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Debuggers
Requires-Python: >=3.10
Provides-Extra: dev
Requires-Dist: black; extra == 'dev'
Requires-Dist: build; extra == 'dev'
Requires-Dist: pytest; extra == 'dev'
Requires-Dist: twine; extra == 'dev'
Description-Content-Type: text/markdown

<!-- SPDX-License-Identifier: Apache-2.0 -->
<!-- Copyright (c) 2026 Junbo Zheng -->

<div align="center">

# weargdb

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

[![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)
[![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](pyproject.toml)

[How it works](#how-it-works) &bull; [Install](#installation) &bull; [Commands](#commands) &bull; [Usage](#usage) &bull; [Extend](#adding-your-own-command) &bull; [Troubleshooting](#troubleshooting)

</div>

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:

```text
(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

```bash
# 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](#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

```text
$ 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:

```text
(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.

### 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:

```text
(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:

```text
(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):

```text
(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):

```c
/* 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:

```text
(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`:

```text
python
import weargdb
end
```

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

```text
source /path/to/weargdb/gdbinit.py
```

## Adding your own command

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

```python
# 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:
>
> ```text
> (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

```text
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.

```bash
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](#adding-your-own-command)). |
