GDB python extensions for debugging MicroPython VM internals
  • Python 97%
  • C 2.4%
  • CMake 0.6%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-08-05 21:04:27 -07:00
multicore_demo Add multicore example application and detailed MULTICORE_GUIDE documentation 2026-08-05 20:59:55 -07:00
micropython_gdb.py Support agent-facing JSON serialization outputs across all mp-* GDB commands 2026-08-05 20:57:07 -07:00
multicore_example.py Add multicore example application and detailed MULTICORE_GUIDE documentation 2026-08-05 20:59:55 -07:00
MULTICORE_GUIDE.md Replace absolute file:// links with relative and HTTPS links for Forgejo compatibility 2026-08-05 21:04:27 -07:00
README.md Support agent-facing JSON serialization outputs across all mp-* GDB commands 2026-08-05 20:57:07 -07:00
test_async.py Implement mp-tasks command to inspect asyncio tasks, wake times, wait graphs, and tracebacks 2026-08-05 16:24:56 -07:00

MicroPython GDB Extensions

"Find a way or make one."

This repository is dedicated to building custom GDB Python extensions specifically for the MicroPython VM running on embedded microcontrollers (like the RP2040 and RP2350).


1. The Challenge

When debugging desktop Python (CPython) in GDB, you get access to rich helper macros like py-bt (Python Backtrace), py-print, and py-list. This is because GDB automatically loads Python helper scripts that understand CPython's internal PyObject and PyFrameObject structures.

MicroPython has no such support. If you halt a MicroPython target in GDB:

  1. C-Level Obscurity: You only see the C-level virtual machine execution loop (like mp_execute_bytecode() in py/vm.c).
  2. Tagged Pointers: Python objects are represented as raw, obfuscated mp_obj_t values, which look like meaningless hex addresses or odd integers to GDB.
  3. No Stack Visibility: You cannot see which Python script, function, or line number was active when the core halted.

The goal of this project is to create a GDB Python script (micropython_gdb.py) that implements custom GDB commands (mp-bt, mp-print, etc.) by parsing MicroPython's internal VM structures directly from the target's RAM over JTAG/SWD.


2. Technical Specs & Constraints

To write this extension, we must decode three primary MicroPython VM internal mechanisms in Python using the GDB Python API (import gdb).

A. Decoding Tagged Pointers (mp_obj_t)

MicroPython represents all Python objects as a single word (mp_obj_t, which is a uint32_t on the RP2040/RP2350). To avoid allocating memory on the heap for simple types, MicroPython uses pointer tagging (storing the type metadata in the least significant bits of the address):

  • LSB = x...xxx1 (Bit 0 is 1): The value is a Small Integer (mp_obj_int). The actual integer value is obtained by right-shifting: value = raw_val >> 1.
  • LSB = x...xx10 (Bit 1 is 1, Bit 0 is 0): The value is an Interned String (QSTR) or a symbol token.
  • LSB = x...xxx0 (Bit 0 and 1 are 0): The value is a genuine pointer to an object structure on the GC heap.
    • Dereferencing this pointer reveals an mp_obj_base_t struct, whose first member type points to the object's type description struct (e.g., mp_type_dict, mp_type_list, mp_type_str).

B. Reconstructing the Python Call Stack

To implement mp-bt, we need to find the Python execution frames. MicroPython stores the execution state in a structure called mp_code_state_t (or mp_code_state_active_t).

To reconstruct the traceback, our GDB Python extension will:

  1. Walk the active GDB stack frames (gdb.newest_frame()).
  2. For each frame running mp_execute_bytecode, locate the local variable pointer referencing mp_code_state_t.
  3. Inside mp_code_state_t, read the:
    • ip: The VM's bytecode Instruction Pointer.
    • sp: The VM's temporary evaluation Stack Pointer.
    • fun_bc: The bytecode representation of the running function.

C. Translating Bytecode to Symbols & Line Numbers

Once we have the bytecode pointer (fun_bc) and the current instruction offset (ip), we must decode the MicroPython bytecode header. The header contains:

  • The filename string (accessible via the QSTR table).
  • The function name string.
  • A compressed line-number table mapping bytecode offsets to Python source lines.

Our script will read this table from the target's memory and translate the current raw bytecode offset into a human-readable file name and line number.


3. Targeted Command Specifications

The implemented GDB commands in micropython_gdb.py are:

Command Usage Description
mp-bt mp-bt Walks the active stack, extracts mp_code_state_t frames, and prints a Python-level call stack trace.
mp-print mp-print <var_name> Resolves <var_name> from the current frame's local/global scope dictionary, parses its tagged pointer type, and prints its value (e.g., decodes list/dict elements).
mp-locals mp-locals Lists all Python variables in the current local scope.
mp-heap mp-heap Traverses the MicroPython Garbage Collector heap structures, reporting GC blocks, free memory, and listing allocations by object type.
mp-tasks mp-tasks Traverses the MicroPython asyncio event loop queue and wait graphs, listing active, sleeping, and IO-waiting tasks, along with their coroutine stacks.

4. How to Use

During a debug session, you source the script inside GDB:

(gdb) source /path/to/micropython-gdb/micropython_gdb.py
MicroPython GDB extensions loaded. Commands available: mp-bt, mp-print, mp-locals, mp-heap, mp-tasks

Agent Usage & JSON Mode

To make these tools ergonomic for AI coding assistants and automation scripts, all command implementations (mp-bt, mp-print, mp-locals, mp-heap, and mp-tasks) support a trailing json argument (or -json / --json).

When specified, the output is returned as machine-parseable JSON instead of human-oriented text:

(gdb) mp-locals json
{
  "a": "42",
  "b": "hello"
}

Examples of Implemented Commands

1. Traceback (mp-bt)

Prints a Python-level backtrace, displaying function names, argument names/values, filenames, and line numbers:

(gdb) mp-bt
MicroPython Traceback (most recent call first):
  #0 0x2000a216 in bar(a=42, b=hello) at <stdin>:6
  #1 0x2000a22e in foo(x=42) at <stdin>:10
  #2 0x2000a259 in <module>() at <stdin>:12

2. Local Scope (mp-locals)

Lists all variables defined in the active local scope, including parameters and non-parameter local variables (which have discarded names and are listed as localN):

(gdb) mp-locals
MicroPython Local Variables:
  a = 42
  b = hello

3. Print Variable (mp-print)

Resolves a variable by name in the local, global, or module scope and prints its formatted Python representation (supports lists, dicts, tuples, strings, integers, and boolean/None values):

(gdb) mp-print a
a (local) = 42
(gdb) mp-print b
b (local) = hello

4. Garbage Collector Heap (mp-heap)

Traverses the MicroPython allocation table and GC pool, printing overall occupancy statistics and categorizing active heap objects by their Python type name:

(gdb) mp-heap
Reading GC allocation table (7567 bytes) from 0x20005fa8...
MicroPython GC Heap Statistics:
  Total Heap size: 484288 bytes (30268 blocks of 16 bytes)
  Allocated space: 5312 bytes (332 blocks, 1.1%)
    - Head blocks: 95
    - Mark blocks: 0
    - Tail blocks: 237
  Free space:      478976 bytes (29936 blocks, 98.9%)
  Total allocations: 95 blocks

Resolving object types for active allocations...

Allocations by Object Type:
  - function: 33 allocations
  - tuple: 9 allocations
  - dict: 5 allocations
  - type: 2 allocations
  - module: 1 allocations
  - list: 1 allocations
  - VfsLfs2: 1 allocations
  - str: 1 allocations
  - <raw data / other>: 42 allocations

5. Event Loop & Task Inspector (mp-tasks)

Crawls the asyncio event loop _task_queue (pairing heap) and _io_queue (stream poll dicts). It also recursively resolves wait relationships to capture parent tasks suspended awaiting child tasks (such as main() awaiting a background worker). Each task is assigned a sequential identifier and labeled with its entry-point coroutine function and RAM address:

(gdb) mp-tasks
MicroPython Asyncio Event Loop State:
  Current active task: 0x2000ac90

Found 3 task(s):
* Task #1 (task_one) [at 0x2000ac30]: SLEEPING (wakeup in 10156 ms)
    Coroutine Stack Trace:
      task_one(id=101, delay=15000) at <stdin>:5
* Task #2 (main) [at 0x200091a0]: AWAITING Task #1 (task_one)
    Coroutine Stack Trace:
      main() at <stdin>:18
* Task #3 (task_two) [at 0x2000ac90]: RUNNING
    Coroutine Stack Trace:
      task_two(x=42) at <stdin>:1

5. Future Commands & Helpers to Consider

For developers looking to extend the capabilities of micropython-gdb, here are several powerful commands that would be highly valuable:

A. Hardware & Peripheral Integration

  • mp-pins / mp-gpios: Inspects the microcontrollers' GPIO control registers and outputs a clean Markdown table summarizing the state of all pins (e.g., Pin Number, Direction [In/Out], Drive Strength, Pull-Up/Down status, and current Hardware Function selector).
  • mp-clocks: Reads system clock control registers (on RP2040/RP2350) and reports the active frequencies of clk_sys, clk_peri, clk_adc, and PLL locking statuses.

B. MicroPython Runtime & Internals

  • mp-scheduler: Dumps the MicroPython scheduler queue (mp_state_ctx.vm.sched_queue). This queue schedules callbacks running outside the coroutine task loop (like soft interrupt handlers or timers).
  • mp-modules: Lists all imported Python modules currently registered in mp_loaded_modules_dict, showing their names, dictionary addresses, and a count of public variables.
  • mp-find <obj_type>: Scans the GC heap for all instances of a specific Python class (e.g. mp-find MyTask) and lists their memory addresses. Helpful for finding leaked objects without complete traversal.

C. Advanced Debugging & Profiling

  • mp-trace <var_name>: Leverages hardware watchpoints to automatically dump a Python backtrace (mp-bt) whenever the specified Python variable is written to by the VM bytecode interpreter.
  • mp-gc-dump <type_name>: Serializes or dumps a raw memory representation of all GC heap blocks belonging to a given type descriptor, aiding in tracking memory corruption or memory layout fragmentation.