- Python 97%
- C 2.4%
- CMake 0.6%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| multicore_demo | ||
| micropython_gdb.py | ||
| multicore_example.py | ||
| MULTICORE_GUIDE.md | ||
| README.md | ||
| test_async.py | ||
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:
- C-Level Obscurity: You only see the C-level virtual machine execution loop (like
mp_execute_bytecode()inpy/vm.c). - Tagged Pointers: Python objects are represented as raw, obfuscated
mp_obj_tvalues, which look like meaningless hex addresses or odd integers to GDB. - 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_tstruct, whose first membertypepoints to the object's type description struct (e.g.,mp_type_dict,mp_type_list,mp_type_str).
- Dereferencing this pointer reveals an
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:
- Walk the active GDB stack frames (
gdb.newest_frame()). - For each frame running
mp_execute_bytecode, locate the local variable pointer referencingmp_code_state_t. - 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 ofclk_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 inmp_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.