Skip to content

Add GDBStub for debugging with LLDB#15

Open
MaartenS11 wants to merge 26 commits intomainfrom
feat/gdbstub
Open

Add GDBStub for debugging with LLDB#15
MaartenS11 wants to merge 26 commits intomainfrom
feat/gdbstub

Conversation

@MaartenS11
Copy link
Copy Markdown
Member

@MaartenS11 MaartenS11 commented Apr 10, 2026

Started working on this on March 29th. Implements a basic gdb stub server allowing LLDB to connect to MIO and debug programs with full source level information. Supports step back, although this feature currently requires a custom version of LLDB. The changes needed in LLDB are currently being upstreamed. Mocking and sliding is currently not part of this, this would require a custom LLDB plugin since LLDB has not multiverse debugging capabilities. Mocking should not be very hard since custom extensions can be added to the gdb stub and LLDB can send them with process plugin packet send.

…e 0 memory to avoid a possible lldb issue

The reply may contain fewer addressable memory units than requested if the server was reading from a trace frame memory and was able to read only part of the region of memory.
… section is now just the full module)

I also am unsure if LLDB actually cares about the section address, seems to not really matter much?
Maybe in the future we might want to support keeping the connection open when detaching so you can keep it running and transfer debugging to a different machine.
…of metadata

We already have this data + it's more accurate than the metadata which restores more snapshots than it should sometimes.

For example:
0000d2 func[4] <main>:
 0000d3: 23 02                      | global.get 2
 0000d5: 23 01                      | global.get 1
 0000d7: 10 00                      | call 0 <env.chip_pin_mode>
 0000d9: 23 03                      | global.get 3
 0000db: 23 01                      | global.get 1
 0000dd: 10 00                      | call 0 <env.chip_pin_mode>
 0000df: 23 04                      | global.get 4
 0000e1: 23 00                      | global.get 0
 0000e3: 10 00                      | call 0 <env.chip_pin_mode>
 0000e5: 02 40                      | block
 0000e7: 03 40                      |   loop
 0000e9: 41 01                      |     i32.const 1
 0000eb: 04 40                      |     if
 0000ed: 23 04                      |       global.get 4
 0000ef: 10 01                      |       call 1 <env.chip_analog_read>
 0000f1: 41 c8 01                   |       i32.const 200
 0000f4: 4a                         |       i32.gt_s
 0000f5: 04 40                      |       if
 0000f7: 23 02                      |         global.get 2
 0000f9: 41 01                      |         i32.const 1
 0000fb: 10 02                      |         call 2 <env.chip_digital_write>
 0000fd: 23 03                      |         global.get 3
 0000ff: 41 00                      |         i32.const 0
 000101: 10 02                      |         call 2 <env.chip_digital_write>
 000103: 05                         |       else
 000104: 23 02                      |         global.get 2
 000106: 41 00                      |         i32.const 0
 000108: 10 02                      |         call 2 <env.chip_digital_write>
 00010a: 23 03                      |         global.get 3
 00010c: 41 01                      |         i32.const 1
 00010e: 10 02                      |         call 2 <env.chip_digital_write>
 000110: 0b                         |       end
 000111: 41 0a                      |       i32.const 10
 000113: 10 03                      |       call 3 <env.chip_delay>
 000115: 0c 01                      |       br 1
 000117: 0b                         |     end
 000118: 0b                         |   end
 000119: 00                         |   unreachable
 00011a: 0b                         | end
 00011b: 00                         | unreachable
 00011c: 0b                         | end

After pc = 0x000101 you have an else at 0x000103, it then jumps to end and 0x000110. This end is not after this primitive call, but it's still restored if we step back in this scenario. This end is after pc = 0x00010e which is a primitive, but we should only restore at 0x000110 in this particular path. So by using the runtime data instead of the ahead of time metadata we actually restore less snapshots so it's more effcient + no need for metadata.
However, LLDB does not have a stepback command so it can only be used by "process plugin packet send sb" or a plugin that adds such command.
WARDuino also has frames for non-functions, for example blocks in wasm.
This allows lldb plugins to step back in the following way:
```python
ci = debugger.GetCommandInterpreter()
res = lldb.SBCommandReturnObject()

ci.HandleCommand('process plugin packet send "QSetStepDir:1"', res)
debugger.HandleCommand("stepi")
ci.HandleCommand('process plugin packet send "QSetStepDir:0"', res)
```
Because the plugin uses `stepi` to perform the actual step operation, lldb will correctly update the state and also use the same UI as regular steps because it's a normal step command.
Otherwise, it seems LLDB just  thinks an exception occured instead of a nice stop for a breakpoint causing it to not discard a range thread plan.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant