Embedded hardware comes with limitations, and one if it is the given number of hardware breakpoints. Depending on your MCU, 4 or only 2 hardware breakpoints are available, making debugging and stepping in read-only memory (FLASH) a challenge.
Did you know that one can have ‘unlimited’ number of breakpoints in FLASH, with the help of GDB? This is very useful for extended debugging, or if you want to use breakpoints for testing?
Limited Breakpoints
As explained in Software and Hardware Breakpoints, the target can be stopped either by a hardware comparator or a breakpoint instruction in the code. The number of hardware comparator is limited by the hardware, and usually is in the range of 2-4 for an ARM Cortex-M. If I try to set more than the limit, it will not work:
If debugging an application in RAM, the debugger can change the RAM ‘on-the-fly’ and allow an ‘unlimited’ number of breakpoints. SEGGER has implemented ‘unlimited flash breakpoints‘ which modifies the FLASH, but requires a J-Link debug probe or debug firmware. A similar thing can be implemented with GDB and the help of a small script.
I have published the settings and demo project on GitHub (see links at the end of the article), using the NXP LPC845-BRK using a NXP McuLink debug probe.
‘Unlimited’ Breakpoints
On ARM Cortex-M, a breakpoint instruction can be written as
__asm(“bkpt #1”);
The bkpt
instruction has an optional argument which is zero by default. When the target executes such a breakpoint instruction, it raises an exception. If a debugger is attached, the debugger (or GDB) catches the exception, and you see the application halted in the debugger.
So adding above breakpoint instructions in the code will get you any number of breakpoints. The problem with this approach is that the debugger will stop on the breakpoint instruction, and if you press ‘continue’ in the debugger, you will run again on that breakpoint. So we need a way to deal with that situation and allow a continue or custom action.
GDB Catchpoints: SIGTRAP Catching
GDB has a feature where the user can add custom ‘signal handlers’. In case of a breakpoint instruction hit, the GDB will get a ‘SIGTRAP’ signal which we can handle with a script.
This drills down to the following concept:
- Breakpoint instructions are placed in the debug version of the application code and compiled with the application. You can place as many breakpoints as needed.
- When launching GDB, a script is provided to catch the SIGTRAP exception, caused by the breakpoint instruction
- Based on the immediate value of the breakpoint instruction, a custom action can be performed
The possibilities of custom actions are endless: I can print a variable, continue the application (skipping the breakpoint and continue) or keep it stopped and move the program counter after the breakpoint instruction.
Breakpoint Instruction
Let’s implement different kind of breakpoints. Because they shall be only be present in the debug version, I can write macros for it:
#ifdef DEBUG #define BREAK_1 __asm("bkpt #1") #define BREAK_2 __asm("bkpt #2") #else #define BREAK_1 /* empty */ #define BREAK_2 /* empty */ #endif
__asm(“bkpt #1”)
shall stop the target, but move the program counter after the breakpoint instruction. Looking at the disassembly, the code looks like this (opcode 0xbe01
):
To make it easier to enable/disable it, I’m using a macro for it, below with two different bkpt
versions:
With that, I can use it like this in the code to test things:
static int test1(void) { BREAK_1; /* hit breakpoint and move PC to next line */ return 1; } static int test2(void) { BREAK_2; /* hit breakpoint and continue */ return 1; }
SIGTRAP
To catch the breakpoint instruction, I can use catch signal SIGTRAP
in GDB:
catch signal SIGTRAP commands ... end
This catches the signal, and inside that ‘catcher’ I can check for the breakpoint instruction and the number used with it:
# GDB script to catch ARM bkpt instruction catch signal SIGTRAP commands # check if it is a BKPT (0xbe) instruction: if (*(unsigned char*)($pc+1)) == 0xbe # check if it is a BKPT #1 instruction: if (*(unsigned char*)($pc)) == 0x01 # yes: move PC after bkpt instruction set $pc=$pc+2 end end end
The above script gets executed whenever a breakpoint is hit. It checks if it is a ‘BKPT #1’ (Opcode 0xbe01). If so, it increments the PC by 2 to place it after the instruction. That way I can place as many breakpoints I like and the debugger will stop and I can inspect the target.
Now what if I want to skip or ignore some breakpoints. For example if I have breakpoints with ‘BKPT #2’ in my code, I can change the script to ignore them, without a need to recompile my code:
catch signal SIGTRAP commands # check if it is a BKPT (0xbe) instruction: if (*(unsigned char*)($pc+1)) == 0xbe # check if it is a BKPT #1 instruction: if (*(unsigned char*)($pc)) == 0x01 # yes: move PC after bkpt instruction set $pc=$pc+2 else # in case of BKPT #2, skip it and continue if (*(unsigned char*)($pc)) == 0x02 set $pc=$pc+2 continue end end end end
With this, I can have as many ‘software’ breakpoint in FLASH and deal with it using or changing my GDB script. If I want to handle one specific breakpoint instruction, I could use the code location (PC) as decision criteria too.
GDB Script and Debugging
The last missing piece is to tell GDB about that script. One way is to pass the script the the GDB launch configuration. In my case below, I have placed the .gdbinit file in the project root:
With the script loaded, the debugger stops correctly on “bkpt 1” and skips the “bkpt 2”, as expected:
Summary
Using gdb with a script to catch and handle breakpoint instructions gives me an unlimited amount of breakpoints. As a plus, I can perform specific actions by breakpoint type (number) or location (program counter). This is especially useful for automated testing or bug hunting where I need to place breakpoints in many locations, exceeding the number of hardware breakpoints.
Happy Breaking 😊
Links
- Project on GitHub: https://github.com/ErichStyger/mcuoneclipse/tree/master/Examples/MCUXpresso/LPC845-BRK/LPC845-BRK_BKPT
- Debugging ARM Cortex-M Hard Faults with GDB Custom Command
- GDB Signals: https://sourceware.org/gdb/onlinedocs/gdb/Signals.html
- Changing the Startup: Custom initial PC and SP Register Setting with the Debugger
- Software and Hardware Breakpoints
Hi Erich
Nice tip as usual, but doesn’t it conflict somewhat with your views on debug versus release builds here?
😉
LikeLike
Good point :-). Yes and no: yes, with the BKPT instructions the binary is different, and you cannot keep them into the non-debug version. No, because the runtime behaviour is somewhat similar to what you have with J-Link if you use more than the available hardware breakpoint. The ‘skipping’ part is the same as if you do ‘skip-points’ with the debugger. And the main part of that referenced article is about different compiler options (e.g. developing/debugging with non-optimized code, and then optimize it just for release.
LikeLike