The debugger

 

The first thing to remember, is save often. Save, save, and save again.

 

There are two types of debugging. The simplest way to trace a program is to have print statements. You can make use of the SWI 256 + ASCII to trace your way through a program. Here is a lame and trivial example:

   STMFD   R13!, {R0-R2, R14}
   SWI     256 + ASC("A")
   MOV     R1, #1
   SWI     256 + ASC("B")
   MOV     R2, #2
   SWI     256 + ASC("C")
   MUL     R0, R1, R2
   SWI     256 + ASC("D")
   LDMFD   R13!, {R0-R2, PC}
Here, we are simply multiplying two registers. When run, it will print ABCD to the screen. Applied to a larger program, this method can help you follow code.

You can also drop in SWI 256 + 7 if you would rather a beep, but use this sparingly as a beep in a stuck loop is annoying - to say the least!

 

However, sooner or later you'll be wanting to use the debugger built into RISC OS.

You are supposed to officially enter the debugger using the *Debug command, but as far as I can ascertain this just outputs 'Debug' before the * prompt. All the debugger commands can be called from any OS_CLI handler (the command line, OSCLI, SYS OS_CLI, etc...).

The debugger accepts two types of parameter. You can either pass a memory address (a hex number, with or without the ampersand), or a register name, like R12. Often two addresses may be given, the first address being the start and the second address being the end. The end address may be absolute, or an offset. The offset is denoted by a + before the value.
You can use a negative offset, but it doesn't quite work correctly.

Here is an example of a debug session. This is the 'MemTest' program (try looking on arcade.demon.co.uk's filesearch?), and I want to get at it just as it starts...

*Load %.MemTest 8000
*MemoryI 8000 +A00
  [...snip...]
0000806C : .... : 00000000 : ANDEQ   R0,R0,R0
00008070 : ... : EF000010 : SWI     OS_GetEnv
00008074 : . : E1A0C001 : MOV     R12,R1
00008078 : . : E1A0A00C : MOV     R10,R12
0000807C : .. : EB0000E6 : BL      &0000841C
  [...snip...]
*BreakSet 8078
*Go 8000
Application Space Memory Test: Version 1.00 made on 1st February 1988.

Testing memory from &8000 to &Stopped at break point set at &00008078
Register dump (stored at &021A046C) is:
R0  = 02102AB4 R1  = 000A8000 R2  = 02379FA4 R3  = 03806358
R4  = 021A0564 R5  = 0000000D R6  = 000000FF R7  = 00008000
R8  = 00000000 R9  = 00000000 R10 = 00000067 R11 = 021A0564
R12 = 000A8000 R13 = 0237A534 R14 = 83814024 R15 = 00008078
Mode USR flags set: nzcvif
Debug*Memory R13 -40

Address  :     7 6 5 4     B A 9 8     F E D C     3 2 1 0 :    ASCII Data
0237A4F4 :    00000000    021633FF    02379F94    0237A334 : ....3..7.47.
0237A504 :    00000000    0237A5A0    01C01832    01C02000 : ....7.2... .
0237A514 :    0215C964    00000000    00000001    239E7A80 : d..........z#
0237A524 :    00000000    00000000    00000000    639E7B80 : ............{c
What we have done here is to set a 'breakpoint' at a chosen address. What happens is when a breakpoint is reached, the debugger halts the program, dumps the registers, and returns control to you. Here, you can examine/alter memory, set more breakpoints, and continue as you see fit.

You also see the benefit of a negative offset, you can examine stacked data. But, the deliberate error here is that RISC OS stacks are 'fully descending', they grow backwards from the top. Your data, however, may be stacked upwards. So you have some flexibility to play around, but notice that the offsets don't quite match up. We have asked for R13-&40 to R13. R13 is &237A534 and we got &237A4F4 (correct) to &237A524 (16 bytes off). Even so, it is useful to be able to go both directions.

 

Debugger commands:

*BreakSet <address|register>

This will set a breakpoint. When execution reaches the breakpoint, a break will be triggered, as is shown above. The breakpoint, obviously, should be word-aligned. Also, the breakpoint should be set after the code has been compiled, otherwise it would be overwritten by the compiled code.
Example:
   *BreakSet 8078
   *

 

*BreakList

This lists the breakpoints.
It says 'Old data' in the list because what actually happens is the debugger replaces the instruction that is supposed to be there with a special branch that is picked up by the debugger. In the above example, the branch was to &FE1A044C. Because the debugger is what gives you your memory dumps and disassemblies, it is able to transparently replace the bogus instruction with the one you expected to see.
Example:
   *BreakList
   Address     Old data
   00008078    E1A0A00C
   *

 

*BreakClr [<address|register>]

This removes the specified breakpoint. If no address is given, you will be prompted if you wish to remove all breakpoints.
Example:
   *BreakClr
   Clear all breakpoints? [Y/N]
   All breakpoints cleared.
   *

 

*Continue

This will continue execution after a breakpoint trap.
Example:
   *Continue
   Continue from breakpoint set at &00008078
   Execute out of line? [Y/N]
   <- program continues ->

 

*Memory [B] <address|register> [<address|register>]

This will display the specified range of memory. If no end is given, 256 bytes will be shown. If the 'B' option is given, the display will be shown in bytes otherwise you will be shown words.
Example:
   *Memory 8000

   Address  :     3 2 1 0     7 6 5 4     B A 9 8     F E D C :    ASCII Data
   00008000 :    EF000001    6C707041    74616369    206E6F69 : ...ÏApplication 
   00008010 :    63617053    654D2065    79726F6D    73655420 : Space Memory Tes
   00008020 :    56203A74    69737265    31206E6F    2030302E : t: Version 1.00 
   00008030 :    6564616D    206E6F20    20747331    72626546 : made on 1st Febr
   00008040 :    79726175    38393120    0D0A2E38    65540D0A : uary 1988.....Te
   00008050 :    6E697473    656D2067    79726F6D    6F726620 : sting memory fro
   *

 

*MemoryI <address|register> [<address|register>]

Here, the memory is disassembled. Various versions of the debugger understand various versions of the ARM instruction set. For example the RISC OS 3.10 debugger is unlikely to understand MRS. But since MRS isn't an ARM2/3 instruction, you are unlikely to be executing it on such a machine.
   *MemoryI 8000 +10
   00008000 :  EF000001 : ...Ï : SWI     "OS_WriteS"
   00008004 :  6C707041 : Appl : LDCVSTL CP0,C7,[R0],#-&104 ; =260
   00008008 :  74616369 : icat : STRVCBT R6,[R1],#-&369     ; =873
   0000800C :  206E6F69 : ion  : RSBCS   R6,R14,R9,ROR #30
   *

 

*MemoryA [B] <address|register> [<address|register>]

The MemoryA command allows you to alter the contents of memory. It will go from the start address (to the end address) displaying the value of what is in memory, then a disassembly, then it will ask you for new memory contents.
The following example shows that I am changing the initial SWI "OS_WriteS" to become SWI "OS_Exit". A trivial example, but note that you give instructions in hex, it does not have an assembler. I press Return with blank input to move to the next word, or press ESCape to abort altering memory.
Example:
   *MemoryA 8000
   + 00008000 : ...Ï : EF000001 : SWI     "OS_WriteS"
     Enter new value: EF000011
   + 00008000 : ...Ï : EF000011 : SWI     "OS_Exit"
     Enter new value: 
   + 00008004 : Appl : 6C707041 : LDCVSTL CP0,C7,[R0],#-&104 ; =260
     Enter new value: Escape
   *

 

The *Memory commands can take a third parameter. The regular form is:

  *MemoryI <address> [[+|-]<length or second address>
The extended form is:
  *MemoryI <address> [[+]<offset>] [[+|-]<length or second address>

What this means is that instead of disassembling from 'address' to 'address + length' (or the second address), it will disassemble from 'address + offset' to '(address + offset) + length'. This comes into it's own when playing around in modules.

 

And finally...

 

*InitStore

This fills memory with a specified value (&E6000010), which disassembles as the BRK instruction. Run from a language (ie, BASIC) it will crash big-time.

 

*ShowRegs

This simply shows the status of the registers since the last breakpoint. If no breakpoint has yet occurred, the results will be meaningless.
Example:
   *ShowRegs
   Register dump (stored at &0219D094) is:
   R0  = 02102B04 R1  = 000A8000 R2  = 02340F34 R3  = 03806358
   R4  = 0234154C R5  = 0000000D R6  = 000000FF R7  = 00008000
   R8  = 00000000 R9  = 00000000 R10 = 00000067 R11 = 0234154C
   R12 = 000A8000 R13 = 023414C4 R14 = 00000000 R15 = 00008078
   Mode USR, flags set: nzcvif
   *

 

 

Darren Salt's Debugger Plus

Darren Salt has written an extended debugger that comes highly recommended. It is aware of the ARM processors you're likely to encounter on RISC OS machines, and it has a wide range of flags to alter how it outputs the disassembly. It allows breakpoints to be set on a range of instructions, even when PC is being read from.
Here's the help for the disassembler flags alteration command:

*Help DisassemblerFlags
   ==> Help on keyword DisassemblerFlags
   *DisassemblerFlags allows you to set various MemoryI and Debugger_Disassemble features.
   Available switches:
        -FDwithR13      use FD with R13, eg. STMDB R13 -> STMFD R13
        -APCS           use APCS-R register set
        -LFMstack       use stack notation with LFM & SFM where possible
        -LFS            use LFS and SFS in preference to LFM & SFM
        -QuoteSWIs      put quotes around SWI names
        -UseDCD         use DCD instead of 'Undefined instruction'
        -UseVDU         use VDU x instead of SWI OS_WriteI+x
        -ANDEQasDCD     use DCD instead of 'ANDEQ' and similar
        -UseADRL        use ADRL (ADRX) instead of ADR + ADD/SUB (+ ADD/SUB)
        -UseADRW        use ADRW, LDRW, STRW for R12m & [R12,#m]
        -LongMul        append L to UMUL, SMUL, UMLA, SMLA
        -UseLDRL        use LDRL instead of ADR/ADD/SUB + LDR
        -UseNOP         use NOP for MOV R0,R0 and BRK for DCD &x6000010
        -OldPSR         use old CPSR/SPSR names
        -Wide           disassemble for wide display
        -HSLO           use HS and LO instead of CS and CC
        -Shift          append comments of the form x<]
   *

To see this in action, we'll look at a bit of the DOSmount module, loaded at &8000 for our purposes (obviously inactive!).
A 'normal' disassembly would look something like:

   *MemoryI 8078 +30
   00008078 <  E1A0A00C : .  á : MOV     R10,R12
   0000807C :  E3A00006 : .. ã : MOV     R0,#6
   00008080 :  E3A03F81 : ? ã : MOV     R3,#&0204          ; =516
   00008084 :  EF02001E : ...ï : SWI     XOS_Module
   00008088 :  624F0038 : 8.Ob : ADRVS   R0,&00008058
   0000808C :  748C2000 : . Œt : STRVC   R2,[R12],#0
   00008090 :  E8BD900E : .½è : LDMIA   R13!,{R1-R3,R12,PC}
   00008094 :  E92D5000 : .P-é : STMDB   R13!,{R12,R14}
   00008098 :  E3A00007 : .. ã : MOV     R0,#7
   0000809C :  E1A0200C : .  á : MOV     R2,R12
   000080A0 :  EF02001E : ...Ï : SWI     XOS_Module
   000080A4 :  E8FD9000 : .Œýè : LDMIA   R13!,{R12,PC}^
   *

We can make it nicer by adding 'FD' style stacking and quoted SWIs with the command *DisassemblerFlags -FDwithR13 1 -QuoteSWIs 1, so then we will see:

   *MemoryI 8078 +30
   00008078 <  E1A0A00C : .  á : MOV     R10,R12
   0000807C :  E3A00006 : .. ã : MOV     R0,#6
   00008080 :  E3A03F81 : ? ã : MOV     R3,#&0204          ; =516
   00008084 :  EF02001E : ...ï : SWI     "XOS_Module"
   00008088 :  624F0038 : 8.Ob : ADRVS   R0,&00008058
   0000808C :  748C2000 : . Œt : STRVC   R2,[R12],#0
   00008090 :  E8BD900E : .½è : LDMFD   R13!,{R1-R3,R12,PC}
   00008094 :  E92D5000 : .P-é : STMFD   R13!,{R12,R14}
   00008098 :  E3A00007 : .. ã : MOV     R0,#7
   0000809C :  E1A0200C : .  á : MOV     R2,R12
   000080A0 :  EF02001E : ...Ï : SWI     "XOS_Module"
   000080A4 :  E8FD9000 : .Œýè : LDMFD   R13!,{R12,PC}^
   *

The final example variation is the -APCS option, which would give us:

   *MemoryI 8078 +30
   00008078 <  E1A0A00C : .  á : MOV     sl,ip
   0000807C :  E3A00006 : .. ã : MOV     a1,#6
   00008080 :  E3A03F81 : ? ã : MOV     a4,#&0204          ; =516
   00008084 :  EF02001E : ...ï : SWI     "XOS_Module"
   00008088 :  624F0038 : 8.Ob : ADRVS   a1,&00008058
   0000808C :  748C2000 : . Œt : STRVC   a3,[ip],#0
   00008090 :  E8BD900E : .½è : LDMIA   sp!,{a2-a4,ip,pc}
   00008094 :  E92D5000 : .P-é : STMDB   sp!,{ip,lr}
   00008098 :  E3A00007 : .. ã : MOV     a1,#7
   0000809C :  E1A0200C : .  á : MOV     a3,ip
   000080A0 :  EF02001E : ...Ï : SWI     "XOS_Module"
   000080A4 :  E8FD9000 : .Œýè : LDMIA   sp!,{ip,pc}^
   *

 

 

A trial debugger

Paul Thomson has written a basic interactive debugger in order to assist him in debugging his programs, and he has been kind enough to allow me to distribute it. Please note that it requires a RiscPC (or later) - it won't work on earlier machines.
When activated, the screen is changed to 1024 x 768 (16 colours). This is necessary due to the vast amount of information presented. You will see across the top the contents of the user mode registers, and the contents of the stack pointed to by R13 (a nice addition!). The lower part of the screen shows two columns of disassembly. On the left is the current breakpoint and execution, and on the right is another area of memory - thus you can see memory access instructions and the memory accesses, say, at the same time.

The debugger is incomplete - it only deals with USR mode, SWIs are assumed to return to the following address, there's no support for FP instructions, the text can sometimes overflow (you can see this in the example), but all in all it is a very good start. As an extra thing, Paul is releasing the source code, so you can either make your own extensions/corrections, or you can simply see how it is that debuggers work.

Paul's debugger differs quite a lot from Darren Salt's. Darren's, and Acorn's own, is a non-interactive debugger. You can play with memory, and you get breakpoints. That is expected of all debuggers. Paul's goes a stage further and allows you to single-step. There are no OSCLI commands. The interactive debugger is an 'environment', while the standard debugger is a 'tool'.

Example; GIF 15K
Full image is 1024 x 768; GIF 45K

Click to download Debug v1.00, and as a bonus you'll get a code profiler (also with source).

If you use this software, please let Paul know what you think.

 

What about DDT?

DDT (Desktop Debugging Tool) is Acorn's almost-all-singing-all-dancing utility supplied with the DDE. If you obtained the DDE tools legally, you'll have plenty of in-depth information about the intricacies of this complex debugger. Thus, you'll be better reading that and trying out the supplied examples.

But, before we leave DDT, a caveat: Ever had this?
Your program acts like a baby, it screams and howls and messes itself in hard to tidy-up ways while running 'native', but behaves perfectly under DDT?
This is usually a side effect of the 'sanitised' environment provided by the debugger. All memory is cleared, etc. So you may have a variable with a quick check to abort if it is zero, but sadly you forgot to initialise the variable before use, so the variable is validly non-zero, but it points at who-knows-where?

 


Return to assembler index
Copyright © 2002 Richard Murray