Metadata-Version: 2.4
Name: weargdb
Version: 0.0.1
Summary: A pip-installable GDB extension that registers custom GDB commands
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,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 GDB extension that registers custom GDB commands 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` 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.

## Why `import`, not `source`

GDB offers two ways to run a Python file, and for a package like `weargdb` they
are **not** interchangeable:

| Command | GDB runs the file as | `__name__` becomes |
|---|---|---|
| `py import weargdb` | a **module** (package import) | `"weargdb"` |
| `source path/to/file.py` | a **top-level script** | `"__main__"` |

`weargdb` is a *package*: its `__init__.py` does `from .commands import
register_all`, and `commands.py` does `from . import __version__`. These are
**relative imports** (note the leading dot), and a relative import only resolves
when the file is loaded as part of a package — that is, via `import`. If you
instead `source src/weargdb/__init__.py`, Python runs it as a standalone script
with no parent package, and the relative import fails immediately:

```text
ImportError: attempted relative import with no known parent package
```

That is why **`source` cannot load `weargdb`** — you must `import` it. A single
`py import weargdb` does both jobs at once: it resolves the relative imports
*and* runs `register_all()`, which registers every command into GDB.

`source` is only the right tool for a self-contained single file with no
relative imports. `weargdb` is deliberately a package — one import entry point,
a clean `_COMMANDS` registry, and room to grow into submodules.

## 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'`, 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_dump <expr> [nbytes]` | Hex-dump raw memory at a C expression's address | Yes (reads memory) |
| `wear_buildinfo` | Print a build-info string baked into the firmware | No (reads `.rodata`) |

## Usage

```text
$ gdb-multiarch -q
(gdb) py import weargdb
[weargdb] loaded GDB commands: wear_hello, wear_ver, wear_sym, wear_sections, wear_dump, wear_buildinfo

(gdb) wear_ver
weargdb GDB extension v0.0.1

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

### 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_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
```

> [!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
```

It chains four of the most-used GDB Python APIs end to end:
`gdb.string_to_argv` (split args), `gdb.parse_and_eval` (expr -> `gdb.Value`),
`int(value)` / `value.address` (get the address), and
`gdb.selected_inferior().read_memory` (read raw bytes).

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

### Load automatically on every GDB start

Add to `~/.gdbinit`:

```text
python
import weargdb
end
```

## Adding your own command

Edit `src/weargdb/commands.py`:

```python
class MyCmd(gdb.Command):
    """my_cmd -- one-line description."""

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

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

Then append `MyCmd` to the `_COMMANDS` tuple at the bottom of the file —
`register_all()` instantiates every entry in the tuple on import.

> [!TIP]
> After editing, the simplest way to pick up the change is to **restart GDB**.
> Within a running session, re-running `py import weargdb` is a no-op because
> Python caches the module. To force a reload without restarting, clear the
> cache first:
>
> ```text
> (gdb) python import sys; [sys.modules.pop(m) for m in list(sys.modules) if m.startswith("weargdb")]
> (gdb) py import weargdb
> ```
>
> `source src/weargdb/commands.py` does **not** work — it runs the file as a
> script, so the relative `from . import __version__` fails. See
> [Why `import`, not `source`](#why-import-not-source).

### 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
├── src/
│   └── weargdb/
│       ├── __init__.py     # imports gdb, calls register_all() on import
│       └── commands.py     # gdb.Command subclasses + the _COMMANDS registry
└── tests/
    └── test_hexdump.py     # pure-Python tests (stub the gdb module)
```

## Testing

The commands depend on GDB's embedded `gdb` module, so the gdb-independent logic
(the hex-dump formatter) is what gets unit-tested under a plain `pytest` run; the
test injects a stub `gdb` module before importing the package.

```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 | Find GDB's Python with `gdb -ex "py import sys; print(sys.path)"`, then `pip install` into that interpreter, or prepend the install dir to `sys.path` in `~/.gdbinit` before `import weargdb`. |
| `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 clear the cache then re-import (see [Adding your own command](#adding-your-own-command)). `source` does not work — `weargdb` is a package. |
