bloombloombloom / Bloom

A debug interface for AVR-based embedded systems development on GNU/Linux.
https://bloom.oscillate.io/
Other
64 stars 3 forks source link

Support range stepping #73

Closed navnavnav closed 9 months ago

navnavnav commented 1 year ago

Currently, when stepping over a line of code, GDB will issue commands to step a single instruction at a time. This means there's a lot of back-and-forth with Bloom and GDB, which can have a significant impact on the time it takes to step over the whole line.

GDB supports range stepping, via the vCont command packet.

More to follow.

navnavnav commented 1 year ago

So I spent some time looking into this.

GDB is supposed to use breakpoints to greatly improve the speed of stepping over instruction-heavy lines. However, I've found that it's not doing this when used in conjunction with Bloom. This appears to be due to an assumption made in GDB, that software breakpoints cannot be installed on flash memory. But this doesn't hold for Bloom - the Microchip EDBG debug tools are able to update the target's flash memory to install and remove software breakpoints.

When GDB requests a memory map from Bloom, we correctly include the flash memory region in said map, with type==flash. Because of this (and the assumption mentioned above), GDB will always attempt to use hardware breakpoints, which Bloom doesn't support. The reason this has worked is because Bloom ignores the breakpoint type in the SetBreakpoint GDB command - it just assumes GDB always asks for software breakpoints.

So GDB operates under the assumption that it can only use hardware breakpoints with Bloom. I suspect this is why it's not making use of internal breakpoints to reduce the time it takes to step over instruction-heavy lines.

To verify this, I modified the memory map in Bloom, so that it would present the flash memory as standard RAM. I then attempted to step over a busy wait (_delay_ms(1000)) function call, to see if GDB's behaviour would change - it did. GDB used software breakpoints and attempted to make use of internal breakpoints to speed up the stepping. However, it did a poor job. The internal breakpoints were poorly chosen (outside of the stepping range, with no clear path to any of them) and did not make any noticeable difference to stepping time.

Range-stepping to the rescue? I'm afraid not. I have implemented support for the vCont packet, and range-stepping. After a lot of testing, I've found that GDB makes no attempt to identify potential program flow changes in the range of instructions being stepped over. This means that stepping over a range is not as simple as running to a particular address in program memory, as there could be an instruction within that range that changes the program flow and prevents the program from ever reaching the "to" address. Some of the older debug tools from Atmel (like the JTAG ICE) appear to have the ability to halt target execution when a flow-changing instruction is reached. But it looks like the newer EDBG tools do not posses this function.

I need some more time to think about this.

I'm not sure what I'm going to do about the hardware breakpoint issue - Bloom is misleading GDB by accepting requests to set hardware breakpoints, but I'm not sure what else we can do in this situation. If Bloom outright rejects hardware breakpoint requests, GDB will simply report an error to the user and refuse to proceed. If I update the memory map to present the target's flash memory as RAM, that will solve the breakpoint issue but cause other issues with writing to program memory (when the user invokes the load command), because GDB will no longer make use of the vFlashX command packets.

navnavnav commented 1 year ago

I spoke to some very helpful people in the GDB IRC channel today.

So it turns out the purpose for range-stepping was to reduce the back-and-forth with GDB and the stub (Bloom), but not to reduce the number of steps needed on the target. So range-stepping would typically be implemented by single-stepping on the target, but not reporting any stops to GDB unless the PC goes out of the specified range.

In other words: A typical implementation of range-stepping won't make any significant difference to stepping times.

However, there is no requirement to implement range-stepping in this way. We could read the program memory ourselves, analyse the instructions within the range and identify the ones that change program flow, and then install software breakpoints at each of those instructions. But this is a substantial amount of work.

I will probably end up doing this at some point, but not sure when. I'm going to leave this ticket open. Will post any further updates here.

navnavnav commented 1 year ago

Given the amount of work required for this ticket, I've decided to move it into 'in consideration'.

The ticket will remain open but work will only begin when there is more demand for it. Demand from a handful of users is not enough to justify the time.

navnavnav commented 9 months ago

Development work for this is complete. Bloom now supports range stepping, which comes alongside support for hardware breakpoints and program memory caching.

However, this hasn't resulted in the benefits I had initially hoped for.

Stepping over calls to inlined instruction-heavy functions are still awfully slow, because GDB doesn't employ range stepping for these. Why? I don't know. I tried looking into it, but eventually gave up. If you've seen the GDB codebase, you'll know why.

The range stepping implementation uses breakpoints to intercept any instruction that can take the target outside of the requested range. This works very well, but can have a negative impact on performance, on some debug tools, where installing software breakpoints can take over a second. For example, the MPLAB Snap, with a JTAG target, takes over a second to install software breakpoints. So if we've run out of hardware breakpoints (or they've been disabled), range stepping will actually slow down stepping.

For the reasons mentioned above, I've decided to disable range stepping by default. It can be enabled by setting the rangeStepping server param to true:

server:
  name: "avr-gdb-rsp"
  ipAddress: "127.0.0.1"
  port: 1442
  rangeStepping: true

The work has been merged into develop and will be included in the v1.0.0 release. Closing this now.

navnavnav commented 9 months ago

After some more testing, I figured out the cause to that issue RE GDB failing to range-step over inlined function calls. It was due to a lack of debugging information. The problem only occurs when you compile with anything but -Og. Even -O0 will result in the required line number data being stripped. Users should be using -Og anyway, when debugging their application.

I've also implemented the reserving of a single hardware breakpoint, to facilitate stepping, similar to what MPLABX does.

Range stepping works a lot smoother now, and I'm confident it will improve stepping performance most of the time. For this reason, I've changed the default state to enabled.