It is the 1745th of March 2020 (aka the 9th of December 2024)
You are 18.97.14.90,
pleased to meet you!
mailto:blog-at-heyrick-dot-eu
Custom breakpoint handler code
A few days ago, somebody on the ROOL forum asked if there was a way to interrupt an executing program to display what the register values were at that point. As it turns out, RISC OS does have a SWI that would help here - OS_BreakPt. Upon executing that SWI, the complete user mode state of the processor will be saved and the breakpoint handler invoked.
The standard system breakpoint handler says, simply, "Stopped at break point at &xxxxxxxx" and then quits the program.
What would be far nicer is to have a simple breakpoint handler that would show the register contents, and then ask if you want to continue running the program, or quit it.
Now, my normal inclination to a request such as that would be to say "Google is your friend". I mean, RISC OS is more than a quarter of a century old, there will be some examples of using OS_BreakPt right? Right?
Well, in the future, this page will serve as an example. ☺
Before we begin
Please note carefully the following:
This only works for USER mode programs.
This has been written using facilities available to the 32 bit processors (CPSR, SPSR, etc). It probably won't run on older 26 bit machines.
This cannot easily be used from BASIC. How to do this at the end of the document, along with reasons why there's not much point in doing so.
Some wrapper code
This little program is some generic wrapper code. This represents "your program", and it indicates the three necessary parts:
Call debug_init to set up the breakpoint handler.
Call debug_finish before you quit to restore the original OS breakpoint handler.
Call the OS_BreakPt SWI whenever you want to stop and see the registers at that point.
Note also that the program uses literals for the SWI numbers, so this will build without any reference to headers or the like.
; TestCode
; ========
;
; Stupid test program to test custom OS_BreakPt handler
;
; Rick Murray, 2016/08/04
;
AREA |My$Code|, CODE, A32bit
IMPORT debug_init ; in the other code file ;-)
IMPORT debug_finish
ENTRY
SWI &10 ; OS_GetEnv
MOV R13, R1 ; set stack pointer
BL debug_init ; set up breakpoint handler code
; set some registers so they can be seen in the debug dump
MOV R0, #&00
MOV R1, #&11
MOV R2, #&22
MOV R3, #&33
MOV R4, #&44
MOV R5, #&55
MOV R6, #&66
MOV R7, #&77
; only bother with R0-R7
; Okay, let's blow it up
SWI &17 ; invoke breakpoint
; Do it again - to see that the registers are the same
SWI &17 ; invoke breakpoint
; Output something
ADR R0, my_message
SWI &2 ; OS_Write0
SWI &3 ; OS_NewLine
BL debug_finish ; undo debug stuff
SWI &11 ; OS_Exit
; *** EXIT ***
my_message
= "Hasta la vista, BABY.", 0
ALIGN
END
If you are somebody who prefers to work using C code, then here is an example C program:
#include <stdio.h>
extern void debug_init(void);
extern void debug_finish(void);
int main(void)
{
int a = 1;
int b = 2;
int c = 0;
int d = 0;
debug_init(); // set up breakpoint handler
c = a * b;
d = c << b;
printf("The values of a-d are %d, %d, %d, %d.\n", a, b, c, d);
// invoke breakpoint
{
SWI 0x17, {}, {}, {} // OS_BreakPt, uses and corrupts nothing
}
printf("The values of a-d are %d, %d, %d, %d.\n", a, b, c, d);
// invoke breakpoint
__asm
{
SWI 0x17, {}, {}, {} // OS_BreakPt, uses and corrupts nothing
}
printf"(Done.\n");
debug_finish();
return 0;
}
Here is an annotated version of the breakpoint handler code.
First up is the usual program header blurb.
; DebugTest
; =========
;
; Test using OS_BreakPt for debugging.
;
; Rick Murray, 2016/08/04
;
; Open Source software - this code has been released under the EUPL v1.1 (only).
; https://joinup.ec.europa.eu/community/eupl/og_page/european-union-public-licence-eupl-v11
;
; Special note: The EUPL licence applies ONLY to this specific code. It may therefore be
; used in and with software under other licences[*] without requiring any
; modification to the licence of these other parts.
;
; * - EUPL code can be converted to GPL v2 to work around the lack of
; compatibility and openness of the GPL.
; It is not, however, compatible with the even-less-open GPL v3.
;
The special note is important. I am choosing to release my code as EUPL, however unlike certain other licences (<cough>GPL</cough>), this does not mean everything it touches will be tainted by this licence. You can use this in commercial software. You can even use this with GPL v2 software as the EUPL permits that version of the code (not every version regardless of what FSF thinks) to be 'relicenced' as GPL in order to allow it to inter-operate with restrictive intentionally incompatible licences such as the GPL. This is what open source is supposed to be about.
Note that the EUPL is not directly compatible with the GPLv3. GNU helpfully provide a workaround by using a third licence as an intermediary. However I'm inclined to say that if you feel the need to go to such lengths, you really ought to stop and ask yourself exactly what your definition of open source is and whether you want your "freedom" to be open and inclusive, or closed and restrictive.
Okay, end of soapbox rant. ☺
But it is worth taking the time to say that, because had I decided to, say, licence this as GPL, it would be a huge red flag to anybody wishing to include this code within their own projects...
A standard AREA definition, and then a declaration of the two functions that we EXPORT to be available to other parts of the source code:
AREA |My$Code|, CODE, A32bit
EXPORT debug_init ; our host program needs this
EXPORT debug_finish
Now for the debug_init function. What we are doing here is to use the SWI OS_BreakCtrl to read the address of the register save block and the current breakpoint handler, and to install our own breakpoint handler in its place.
The register save block is 17*4 words (68 bytes), which represents R0-R15 in order, and then the CPSR following. However in order to make things simpler, we just use the OS-supplied register save block.
Technically, the OS_BreakCtrl SWI has been deprecated (since forever). One is supposed to use OS_ChangeEnvironment instead; however I note in the RISC OS source code that RISC OS itself uses OS_BreakCtrl. ☺
debug_init
; set up debug stuff
MOV R0, #0 ; use existing register save block
ADR R1, debug_break ; our handler
SWI &20018 ; XOS_BreakCtrl (deprecated! ;-) )
ADR R2, debug_saveblock
STR R0, [R2, #0] ; remember where the save block is
STR R1, [R2, #4] ; remember the previous control routine
MOV PC, R14
The inverse of the above is to undo the custom breakpoint handler and restore the default one. This is performed by calling the same SWI and giving it the values that we remembered from before:
debug_finish
; revert to previous
ADR R2, debug_saveblock
LDR R0, [R2, #0] ; retrieve original saveblock
LDR R1, [R2, #4] ; retrieve original control routine
SWI &20018 ; XOS_BreakCtrl
MOV PC, R14
Here follows some workspace. Two words for remembering the register save block address and previous handler; and three words for translating numeric values to printable numbers:
debug_saveblock
DCD 0 ; to remember saveblock address
DCD 0 ; to remember control routine
debug_wordbuffer
DCD 0 ; space for 11 characters plus terminull
DCD 0
DCD 0
Here starts the breakpoint handler. We are entered in SVC mode (so we have a private R14 to play with). We do not return, so all registers (save R13) are trashable. We exit either by calling OS_Exit (to quit the application) or by restoring state from the register save block.
In the function, we treat R0-R3 as working registers (akin to APCS), R4 is the register save block pointer, and R5 is a counter for stepping through the registers.
debug_break
; Called upon a breakpoint
; USER mode registers are saved at debug_saveblock address,
; and we are entered in SVC mode.
; R0-R3 : Scratch
; R4 : Register save pointer
; R5 : Counter
; Pick up address of saved registers
ADR R4, debug_saveblock
LDR R4, [R4]
Now that we have a pointer to our saved registers, the first thing to do is pick up the saved value of PC so we can report where the breakpoint occurred. This bit of code does that:
As you can see, we have some help from the SWI OS_ConvertHex8. This means that we have so far printed out the message "Breakpoint at &xxxxxxxx".
The next task to do is step through all of the registers, printing out their contents in both hex and denary. To begin, however, we need to nicely output the register number:
; Now dump the registers
MOV R5, #0 ; Start from R0
debug_dumploop
; Output "Rxx = &"
SWI 256 + 'R' ; Output "R"
ADD R0, R5, #48 ; R0 is now a number
CMP R5, #10 ; Register >= 10?
SUBGE R0, R0, #10 ; ...bring it back to 0-9 range
SWIGE 256 + '1' ; ...and prefix a '1'
SWI &0 ; OS_WriteC
CMP R5, #10
SWILT 256 + ' ' ; Add a space if register < 10
ADR R0, reg_msg ; " = &"
SWI &2 ; OS_Write0
The complication here is twofold. Firstly, we output the register number by writing out the character for the number. There is no "10" or "11" etc, so we need to instead output a "1" followed by the second digit as appropriate. Secondly, we need to insert an additional space character if the register only has one digit, so everything lines up nicely. It's two lines of code, no excuse for amateurish results.
Now write the register in hex, this is just like we did above for PC:
; Read the register and output it, in the form:
; xxxxxxxx (yyyyyy)
; [hex] [denary]
MOV R1, R5, LSL#2 ; R1 = counter, shifted to word offset
LDR R3, [R4, R1] ; R3 = savedregs + counter offset
MOV R0, R3
ADR R1, debug_wordbuffer
MOV R2, #11
SWI &D4 ; OS_ConvertHex8
SWI &2 ; OS_Write0
Output a space, an open bracket, the value in denary (using a slightly different SWI), then a closing bracket. I had thought of using a string, but ultimately the simplest option was to just output those characters directly.
Of note here is the use of OS_ConvertCardinal. This treats the register value as unsigned, so will never result in a negative number.
Now loop around until al of the registers (R0-R14) have been displayed.
ADD R5, R5, #1 ; next register
CMP R5, #15 ; done beyond R14?
BLT debug_dumploop ; if not, go around again
The final part of our output is to pick up the saved CPSR and report on the status of the flags. The flags reported are Negative, Zero, Carry, and oVerflow (bits 31 to 28 respectively).
The test is performed using the TST instruction. It might seem odd that an EQ result afterwards means NO match, but if you consider the instruction itself performs an AND comparison between the specified register and the value we are checking for. Therefore zero (EQ) means the AND result was zero (no match) whereas non-zero (NE) means there was a matching bit. We set the character to output (in R0) to the uppercase letter. If the comparison is zero (no match), 32 is added to force the character to be its lower case form. Thus, upper case means the flag is set, lower case means it is unset.
; Now we want to read the stored CPSR and report the flags
ADR R0, flags_msg
SWI &2 ; OS_Write0
LDR R1, [R4, #(16*4)]
TST R1, #(1<<31) ; Test N bit
MOV R0, #'N'
ADDEQ R0, R0, #32 ; if unset, make lower case
SWI &0 ; OS_WriteC
TST R1, #(1<<30) ; Test Z bit
MOV R0, #'Z'
ADDEQ R0, R0, #32
SWI &0
TST R1, #(1<<29) ; Test C bit
MOV R0, #'C'
ADDEQ R0, R0, #32
SWI &0
TST R1, #(1<<28) ; Test V bit
MOV R0, #'V'
ADDEQ R0, R0, #32
SWI &0
SWI &3 ; OS_NewLine
We don't bother to report the processor mode. As I said, this stuff is only intended to work in USR32 mode, so that is just assumed to be the case.
Other flags you might want to add: Q (sticky overflow) bit 27, IRQ disabled bit 7, FIQ disabled bit 6. There are others on later ARM processors, refer to a datasheet for specifics.
The version of this program that I made available this morning did the following:
TST R0, #(1<<28) ; Test V bit
SWIEQ 256 + 'v'
SWINE 256 + 'V'
However it is not possible to rely upon flags being preserved across SWI calls. The code worked fine in testing, but this may be luck more than anything else. The method given now will work.
Now the output has been done, ask the user if they want to continue running the program, or quit. We say the options are [Q] and [C] but in reality we only continue if the user replies with 'C' or 'c' - anything else is assumed to be Quit.
; If we're here, R0-R14 have been dumped. Ask the user what
; to do.
ADR R0, abort_retry_ignore
SWI &2 ; OS_Write0
SWI &3 ; OS_NewLine
SWI &4 ; OS_ReadC
; 'C' means continue, anything else means exit.
CMP R0, #'C' ; Was it a 'C'?
CMPNE R0, #'c' ; Or was it a 'c'?
BNE debug_exit ; If not, EXIT.
As we will have branched to quit, the following is the continuation code. You will note the use of some things (LDM with ^ and MOVS PC) that are not supposed to be used in 32 bit code. These instructions do, in fact, exist, but their behaviour is different to the older versions.
The first task is to pick up the user mode status register (CPSR) and write it to the SVC mode's saved PSR (SPSR).
Next, we point our private copy of R14 to point to the saved register block, and we then load R0-R14 into the user mode register block (hence the ^).
Then we load the saved copy of PC into R14, and add four (so it will point to the instruction following the one that raised the breakpoint).
Finally, we use MOVS to push R14 into PC and resume execution of the program. The use of MOVS in this manner works because we have a private SPSR, and the instruction thus copies the SPSR into the CPSR at the same time. This behaves like the MOVS of old in restoring execution and processor state/mode.
After this, our program resumes unaware that anything has happened.
; If we're here, the user wants to resume with the program
LDR R0, [R4, #(16*4)] ; pick up the CPSR
MSR SPSR_cxsf, R0 ; store it in SPSR_svc
MOV R14, R4 ; Set R14_svc to point to register save block
LDMIA R14, {R0-R14}^ ; Restore all USER MODE registers
MOV R0, R0 ; objasm whinges
LDR R14, [R14, #(15*4)] ; R14_svc is now USER MODE PC
ADD R14, R14, #4 ; Point to the *following* instruction!
MOVS PC, R14 ; MOVS here will copy SPSR to CPSR
; we're back to our program now...
The exit clause is here. We force USR32 mode (no longer in SVC mode), undo the breakpoint handler, and then call OS_Exit to terminate the application.
debug_exit
; We come here to EXIT the program
MSR CPSR_c, #16 ; drop to USER32 mode (from SVC)
BL debug_finish ; restore previous breakpoint handler
SWI &11 ; OS_Exit
; ** Done **
Some strings finish our code.
brk_msg
= "Breakpoint at &", 0
ALIGN
reg_msg
= " = &", 0
ALIGN
flags_msg
= "Flags: ", 0
ALIGN
abort_retry_ignore
= "[C]ontinue or [Q]uit?", 0
ALIGN
END
The breakpoint handler - complete
Here's a non-annotated version:
; DebugTest
; =========
;
; Test using OS_BreakPt for debugging.
;
; Rick Murray, 2016/08/04
;
; Open Source software - this code has been released under the EUPL v1.1 (only).
; https://joinup.ec.europa.eu/community/eupl/og_page/european-union-public-licence-eupl-v11
;
; Special note: The EUPL licence applies ONLY to this specific code. It may therefore be
; used in and with software under other licences[*] without requiring any
; modification to the licence of these other parts.
;
; * - EUPL code can be converted to GPL v2 to work around the lack of
; compatibility and openness of the GPL.
; It is not, however, compatible with the even-less-open GPL v3.
;
AREA |My$Code|, CODE, A32bit
EXPORT debug_init ; our host program needs this
EXPORT debug_finish
debug_init
; set up debug stuff
MOV R0, #0 ; use existing register save block
ADR R1, debug_break ; our handler
SWI &20018 ; XOS_BreakCtrl (deprecated! ;-) )
ADR R2, debug_saveblock
STR R0, [R2, #0] ; remember where the save block is
STR R1, [R2, #4] ; remember the previous control routine
MOV PC, R14
debug_finish
; revert to previous
ADR R2, debug_saveblock
LDR R0, [R2, #0] ; retrieve original saveblock
LDR R1, [R2, #4] ; retrieve original control routine
SWI &20018 ; XOS_BreakCtrl
MOV PC, R14
debug_saveblock
DCD 0 ; to remember saveblock address
DCD 0 ; to remember control routine
debug_wordbuffer
DCD 0 ; space for 8 characters plus terminull
DCD 0
DCD 0
debug_break
; Called upon a breakpoint
; USER mode registers are saved at debug_saveblock address,
; and we are entered in SVC mode.
; R0-R3 : Scratch
; R4 : Register save pointer
; R5 : Counter
; Pick up address of saved registers
ADR R4, debug_saveblock
LDR R4, [R4]
; First, report where the break point happened
ADR R0, brk_msg
SWI &2 ; OS_Write0
LDR R0, [R4, #(15*4)] ; R15 aka PC
ADR R1, debug_wordbuffer
MOV R2, #11
SWI &D4 ; OS_ConvertHex8
SWI &2 ; OS_Write0
SWI &3 ; OS_NewLine
; Now dump the registers
MOV R5, #0 ; Start from R0
debug_dumploop
; Output "Rxx = &"
SWI 256 + 'R' ; Output "R"
ADD R0, R5, #48 ; R0 is now a number
CMP R5, #10 ; Register >= 10?
SUBGE R0, R0, #10 ; ...bring it back to 0-9 range
SWIGE 256 + '1' ; ...and prefix a '1'
SWI &0 ; OS_WriteC
CMP R5, #10
SWILT 256 + ' ' ; Add a space if register < 10
ADR R0, reg_msg ; " = &"
SWI &2 ; OS_Write0
; Read the register and output it, in the form:
; xxxxxxxx (yyyyyy)
; [hex] [denary]
MOV R1, R5, LSL#2 ; R1 = counter, shifted to word offset
LDR R3, [R4, R1] ; R3 = savedregs + counter offset
MOV R0, R3
ADR R1, debug_wordbuffer
MOV R2, #11
SWI &D4 ; OS_ConvertHex8
SWI &2 ; OS_Write0
SWI 256 + ' '
SWI 256 + '('
MOV R0, R3 ; pick up the register value again
ADR R1, debug_wordbuffer
MOV R2, #11
SWI &D8 ; OS_ConvertCardinal4
SWI &2 ; OS_Write0
SWI 256 + ')'
SWI &3 ; OS_NewLine
ADD R5, R5, #1 ; next register
CMP R5, #15 ; done beyond R14?
BLT debug_dumploop ; if not, go around again
; Now we want to read the stored CPSR and report the flags
ADR R0, flags_msg
SWI &2 ; OS_Write0
LDR R1, [R4, #(16*4)]
TST R1, #(1<<31) ; Test N bit
MOV R0, #'N'
ADDEQ R0, R0, #32 ; if unset, make lower case
SWI &0 ; OS_WriteC
TST R1, #(1<<30) ; Test Z bit
MOV R0, #'Z'
ADDEQ R0, R0, #32
SWI &0
TST R1, #(1<<29) ; Test C bit
MOV R0, #'C'
ADDEQ R0, R0, #32
SWI &0
TST R1, #(1<<28) ; Test V bit
MOV R0, #'V'
ADDEQ R0, R0, #32
SWI &0
SWI &3 ; OS_NewLine
; If we're here, R0-R14 have been dumped. Ask the user what
; to do.
ADR R0, abort_retry_ignore
SWI &2 ; OS_Write0
SWI &3 ; OS_NewLine
SWI &4 ; OS_ReadC
; 'C' means continue, anything else means exit.
CMP R0, #'C' ; Was it a 'C'?
CMPNE R0, #'c' ; Or was it a 'c'?
BNE debug_exit ; If not, EXIT.
; If we're here, the user wants to resume with the program
LDR R0, [R4, #(16*4)] ; pick up the CPSR
MSR SPSR_cxsf, R0 ; store it in SPSR_svc
MOV R14, R4 ; Set R14_svc to point to register save block
LDMIA R14, {R0-R14}^ ; Restore all USER MODE registers
MOV R0, R0 ; objasm whinges
LDR R14, [R14, #(15*4)] ; R14_svc is now USER MODE PC
ADD R14, R14, #4 ; Point to the *following* instruction!
MOVS PC, R14 ; MOVS here will copy SPSR to CPSR
; we're back to our program now...
debug_exit
; We come here to EXIT the program
MSR CPSR_c, #16 ; drop to USER32 mode (from SVC)
BL debug_finish ; restore previous breakpoint handler
SWI &11 ; OS_Exit
; ** Done **
brk_msg
= "Breakpoint at &", 0
ALIGN
reg_msg
= " = &", 0
ALIGN
flags_msg
= "Flags: ", 0
ALIGN
abort_retry_ignore
= "[C]ontinue or [Q]uit?", 0
ALIGN
END
Using this from BASIC
For those rare times you may find yourself doing register-level debugging in BASIC...
Modify the breakpoint handler code to begin as follows:
AREA |My$Code|, CODE, A32bit
EXPORT debug_init ; our host program needs this
EXPORT debug_finish
ENTRY
B debug_throw
B debug_init
B debug_finish
debug_throw
; Make OS_BreakPt become
SWI &17 ; OS_BreakPt
MOV PC, R14
debug_init
The two EXPORTs are not needed, you can remove them if you want. It doesn't matter.
Now assemble this as an object file, and drag that object file to link with the Binary option chosen. Save the data file offered.
Now, from BASIC...
REM >BreakTestB
REM
REM Using the breakpoint handler in BASIC (OMG!)
REM
REM Rick Murray, 2016/08/05
REM
REM *NOTE* Assumes "debugcode" is in the CSD.
DIM code% 511
SYS "OS_File", 16, "debugcode", code%, 0 : REM *LOAD it
REM Set up the breakpoint handler
CALL code%+4
a% = 1
b% = 2
c% = a% * b%
PRINT a%, b%, c%
CALL code%
PRINT a%, b%, c%
SYS "OS_BreakPt"
REM Remove the breakpoint handler
CALL code%+8
PRINT "Done."
END
Of course, calling SYS in BASIC will likely leave you with most of the registers as zero (as BASIC copies over the registers you pass on entry). Even trying to sidestep this to CALL some assembler to call the SWI won't have much effect, as BASIC doesn't work like a regular program and its environment isn't one that poking around at register level would be much use for.
You can see in these examples. The first register dump was the CALL version. This, by rights, should leave most of the registers as-is, yet R0-R7 are all '1'. The SYS version follows, and you can see it has set R0-R9 to zero (as nothing was specified, so BASIC assumes zero).
Frankly, asides from working with assembler code, you won't get much use out of the breakpoint code in BASIC. But, hey, somebody was bound to ask, right? ☺
This is, incidently, the beginnings of how a debugger works. A debugger would likely save a copy of the program code somewhere for its own private use. Then certain instructions (branches, memory access, etc) would be replaced by breakpoint calls.
When the breakpoint is encountered, the value of PC allows the debugger to look to see what instruction was supposed to have been executed. This instruction could then be faked in a sanitised fashion (such as trapping errant memory accesses), and then execution would resume until the next breakpoint, at which point the whole process starts all over again.
Your comments:
Please note that while I check this page every so often, I am not able to control what users write; therefore I disclaim all liability for unpleasant and/or infringing and/or defamatory material. Undesired content will be removed as soon as it is noticed. By leaving a comment, you agree not to post material that is illegal or in bad taste, and you should be aware that the time and your IP address are both recorded, should it be necessary to find out who you are. Oh, and don't bother trying to inline HTML. I'm not that stupid! ☺ ADDING COMMENTS DOES NOT WORK IF READING TRANSLATED VERSIONS.
You can now follow comment additions with the comment RSS feed. This is distinct from the b.log RSS feed, so you can subscribe to one or both as you wish.
David Pilling, 12th August 2016, 01:25
Hmm my comment vanished - I wondered if you could take an open source memory manager and create a better OS_Heap module. I once suggested to Pace how to improve C malloc - something like merging neighbouring free'd blocks. Which they may have done. Lots of interesting things one can do like adding guard bytes to block and see if they are corrupted.
, 30th April 2018, 19:41
Bonjour, Thanks for all your tutor. I am testing a basic program with assembler code. I use your debuger code, nice, to trace the registers before the call to swi. OMAPxxx.Castle.RiscOs.sources.HwSupport.buffers.test.Basher I am trying to test buffer but this example is 26 bits. Note: I have tested your basic programme: call code% works whith a%, b% , c% all caps.
This web page is licenced for your personal, private, non-commercial use only. No automated processing by advertising systems is permitted.
RIPA notice: No consent is given for interception of page transmission.