Debuffer Commands

This is the example-oriented introduction to the debuffer commands. A shorter summary is also available.

You FORTH Love If Honk Then

The most important thing to keep in mind when working with the debuffer is that the command language is a full FORTH interpreter. If you are not familiar with FORTH this may seem like an unnecessary complexity. If so, I apologize - I'm just twisted. To help ease the pain you can find a short introduction to the language here and find out more about the particular FORTH in the debuffer here.

The primary impact of this is that you type the arguments to a command first. All commands are documented in the summary and in the online help with a stack-effect comment. Briefly, these comments take the form of ( arg1 arg2 -- result ), meaning that the command expects to find two arguments on the stack and returns one result on the stack. For example, the disassemble command is documented as:

dis ( addr count -- )
"dis" expects to find a start address and a count of instructions to disassemble on the stack as its arguments. It is used:
pc @ 10 dis
"pc @" puts the contents of the program counter on the stack, "10" puts a literal 10 on the stack, and "dis" invokes the disassembly. If you do not specify enough arguments for a command you will be rewarded with a "stack underflow" message. Not to worry - just hit the enter key and you will be back at your prompt and can try again. More examples of stack-effect comments can be found in the FORTH intro.

If you are familiar with FORTH, you will quickly appreciate the power of a fully scriptable debugger.

Number Base

FORTHs default to using base-10 for input and output, though they can support any base from 2 to 36. While I typically debug in base-16, I chose to leave the default unchanged. It is sometimes convenient to read numbers in hex, even while being able to input numbers in decimal, so some of the display commands do temporarily change to base-16. This makes the output columns line up nicely and everything looks very pretty.

If you do want to work in hexadecimal, just type the command "hex". Also, regardless of your defined base, numbers entered with a preceeding 0x will be interpreted as hex.

Register Commands

The debuffer maps all emulator registers to FORTH variables. You can inspect and manipulate register values using standard FORTH syntax and any changes you make are transparently propagated to the emulator.

The 32-bit registers available to you are: d0..d7, a0..a6, fp (an alias for a6), usp, ssp, and pc. There is one 16-bit register: sr. If you are new to m68k assembly, there is a short introduction to the registers below.

As an example, you can view the contents of the d0 register with the command "d0 @ ." (do not include the quotes) or "d0 ?" (? is defined to mean "@ .". "d0" puts the address of the d0 variable on the stack, "@" (fetch) gets the contents of that variable, and "." prints it to the screen. (The exception is the status register, sr, which you fetch with the word "w@" because it is a 16-bit register.)

Similarly, you can set the contents of the d0 register. You can set d0 to 0 (zero) with the command "0 d0 !" (again, no quotes). As before, "d0" puts the address of the d0 variable on the stack, but "!" (store) sets the contents of that variable. (To set the value of the status register use the word "w!".)

One more example is in order. The debuffer can run scripts that inspect, act on, or modify the values of registers:

: check0 d0 @ 0= if ." Routine _foo returned FALSE." 1 d0 ! then ;
This defines a word (check-foo) that inspects the value of d0 and, if it is 0, prints a message and sets the value to 1. This type of thing will come up again in the section on breakpoints, as it can be used to do fun things like setting watchpoints and conditional breakpoints.

That's all well and good, you say, but it seems like an awful lot of keystrokes for a really common operation like printing the value of a register. It is. The debuffer defines a number of softwords to make common operations easier to do. In the example above, you would really just type ".d0" (print d0) to see the contents of the d0 register.

The softwords for examining the registers are: .d0, .d1, .d2, .d3, .d4, .d5, .d6, .d7, .a0, .a1, .a2, .a3, .a4, .a5, .a6, .fp, .usp, .ssp, .pc, and .sr. Notice that you don't have to remember that sr is a special case (16-bits). There is also a word .regs that dumps all the registers. Since these words are all defined in FORTH you can change them without recompiling the application - even at runtime - to customize their behavior however you would like. For instance, you might want to reformat the output of .regs - it's your choice.

Examining and Modifying Memory

The debuffer defines three primitive words for examining the contents of memory locations in the emulator: @byte, @word, and @long. Each of these takes an address as its argument and returns the contents of the 8-, 16-, or 32-bit value at that address. Easy enough, but this is FORTH, so the argument (the address) comes first.

So, let's say we want to see the contents of the double-word eight bytes above the frame pointer. (This would be an argument passed to a subroutine. Learn more about the emulator stack below.) We can do this as follows:

fp @ 8 + ?long
"fp @" fetches the value of the frame pointer (the a6 register), "8 +" adds eight, and "?long" prints the 32-bit value at that location. ?byte, ?word, and ?long are all softwords.

Similarly, there are three primitive words for modifying the contents of memory locations in the emulator: !byte, !word, and !long. Each of these takes a value and an address as arguments.

-1 fp @ 4 - !long
This sets the value of a 32-bit local variable to -1.

You can examine blocks of memory easily with the commands: da (dump ascii), db (dump bytes), dd (dump double-words), and dw (dump words). Each of these takes an address and a count as its arguments and produces an ascii or hexadecimal dump of the memory. For example:

a0 @ 20 db
a0 @ 20 da
Each of these commands displays the twenty bytes pointed to by the a0 register. You might choose da over db if you believe that a0 points to a string and want to see it in a readable format. "a0 @ 20 dw" dumps the twenty words (forty bytes) pointed to by a0.

da, dd, and dw are all defined in FORTH so you can customize their display quite easily. db is not currently defined in FORTH but that's only because it was easier in C++. You are still free to redefine db in FORTH and your definition will take precedence over the built-in.

The command "dis" also takes as its arguments an address and a count, but instead of a raw dump dis produces a listing of the next count instructions starting at the given address. So "pc @ 20 dis" disassembles the next 20 instructions starting with the current program counter.

"bt" (backtrace, not to be confused with all those other "b*" commands in the next section) displays a complete call history by walking the stack. bt is implemented entirely in FORTH. bt uses a debuffer primitive "ln" (list name) that prints the routine name associated with the address specified as its argument.

Breakpoints

The command to set a breakpoint is "bp". bp takes as its argument the address of the breakpoint to set. For example, you could break on exit from the current subroutine with the command:
fp @ 4 + @long bp
This gets the caller's return address off the stack and uses it to set a breakpoint. (For this example, you could also use the predefined command "out" discussed in the next section.)

At this time, the emulator supports a maximum of five user-defined breakpoints. The debuffer will not let you try to define more than that. You can see what breakpoints you have set with the command "bl", which takes no arguments. bl lists each breakpoint with its breakpoint id in the first column. The breakpoint id is used as the argument to the commands "bc" (clear breakpoint), "bd" (disable breakpoint), and "be" (enable breakpoint).

bp takes as an optional argument a text string that can contain any command or sequence of commands you wish to execute when that breakpoint is hit. Continuing our earlier example:

fp @ 4 + @long bp .d0
This command will break on exit from the current routine and print its return value. We could use this feature to set a conditional breakpoint and only break if that return value is 0.
: break0 @ if go then ;
fp @ 4 + @long bp d0 break0
go instructs the debuffer to resume execution. Alternatively, we could set a watchpoint that displayed the value of d0 without stopping.
fp @ 4 + @long bp .d0 go
Or, we could ensure that the routine always returns 1.
fp @ 4 + @long bp 1 d0 ! go

Controlling Execution

The debuffer commands for controlling target execution are: "go", "next", "out", "step", "to", and "wait". One last option for controlling execution is to install a step-spy. By passing an address and a value to the command "spy" you can ask the emulator to break if ever the double-word at the given address takes on the given value. If you change your mind, un-install the step-spy with the word "nospy". You are only allowed one spy at a time.

Softwords

Softwords generically refers the the debuffer functionality written in FORTH as opposed to C++. You do not need to recompile, or even re-run, in order to customize this functionality. The file debuffer.fr contains the definitions of the softwords currently in the debuffer.

Many softwords are described elsewhere in this document. This section is intended to cover those miscellaneous words that are not documented elsewhere.

Registers

Deep in the guts of microprocessor based computers...

Any CPU will have some number of internal memory locations. Whereas memory outside the CPU is identified by an address, the memory in most CPUs is identified by name. These named memory locations are collectively called registers.

The contents of some registers are not arbitrary. That is to say, the contents of these registers are interpreted in specific ways either by the hardware, the operating system, or the programming language, so that these registers have some meaning.

The Motorola 68328 DragonBall microprocessor at the heart of your Palm computer has the following registers: d0, d1, d2, d3, d4, d5, d6, d7, a0, a1, a2, a3, a4, a5, a6, usp, ssp, pc, and sr. It interprets these registers in the following ways:

Registers d0..d7 are collectively data registers while registers a0..a6 are address registers. Some operations are allowed on data registers that are not allowed on address registers, and vice-versa.

Register usp is the user stack pointer. It contains the address of the top of the stack when the CPU is running in user mode. It does not seem to be used by PalmOS.

Register ssp is the system, or supervisor, stack pointer. It contains the address of the top of the stack when the CPU is not in user mode, which seems to be all the time under PalmOS.

Register pc is the program counter It contains the address of the next instruction to be executed by the CPU.

Register sr is the status register. The individual bits within the status register are assigned meanings and indicate (what else?) status information. Specifically:

Bits 14, 12, 11, and 7 to 5 are reserved. For more information, please see the DragonBall documentation page.

Some of the data and address registers have conventional meanings under PalmOS that are not enforced by the hardware:

Lastly, data registers d0..d2 and a0..a1 are usually considered "temporary" in that called routines are under no obligation to preserve their values. If you want to keep one of these registers across a call, save it on the stack. Conversely, any other registers that you modify you should save and restore before returning.

It is important to note that these last few are only conventions and are not always followed. For instance, optimized routines may not setup an explicit stack frame, or one programming language might choose to always return values on the stack.

The Stack

The call stack maintains a lot of information about the history and context of execution. It is used to hold arguments to subroutines, return values, local variables, and return addresses. It is also used as temporary storage for registers when a called routine needs to overwrite one of the "non-temporary" registers.

The call stack is not to be confused with the argument stack in FORTH. FORTH is a stack-based language and maintains its own separate stack. You can, however, gain access to the call stack from within FORTH.

Routines frequently use a stack frame to provide easy access to arguments, return values, and local variables. In a stack frame, the register a6 is used to point to the base of the frame. The 32-bit value at this location is conventionally (and conveniently!) the old value of the a6 register. In other words, the first value in my stack frame is a pointer to my caller's stack frame. The next value, at "a6 @ 4 +" is the return address. This points to the next instruction to be executed in my caller's routine, upon completion of my routine. Above this, at "a6 @ 8 +" and higher, are the arguments to my routine.

At negative offsets from my stack frame are my local variables. So I might use "a6 @ 4 -" to get to one local variable, and "a6 @ 8 -" to get to another.


Last modified: Jan 9, 1999
sessoms@pagesz.net