GNU Debugger, GDB is one of the most popular debuggers today.
It has a lot of features. One of those features that doesn't have a lot discussion are the convenience variables and user-defined commands.
I work with firmware. GDB can be used with processors that are not the host processor where you write and compile your code. However, additional GDB commands must be used to provide this feature.
I found a very good use for both of these neglected features when working on firmware on legacy (old) hardware.
Motorla 68000
Microprocessors today support hardware breakpoints. The microprocessor provides these breakpoints as part of its structure, no additional support or tools required, JTAG enabled and all that rot.But what happens when you have to work with legacy devices. The Motorola 68000 series microprocessor is such an example.
When the M68K was first introduced, the tool to use was an ICE, In-Circuit Emulator. A large connector, the same as the CPU, was inserted into the circuit board instead of the M68K, and an M68K was placed on top of this connector. The ICE device then controlled ALL the pins of the M68K. The ICE also had RAM that mirrored the memory space on the circuit board. In essence, a complete clone.
ICEs were expensive. One could still use an ICE today, if you can find one, and it is still working. :)
A simpler method was developed for the M68K, and later families of Motorola processors (today Freescale), BDM, Background Debug Mode.
BDM is a serial bus dedicated to debugging supported by a simple header.
Using BDM, one can read and write registers, read and write RAM, read from ROM and reflash programs, but it has very limited instruction debugging. This makes debugging complex programs difficult. BDM was a predecessor to JTAG.
Enter the convenience variables and user-defined commands.
GDB Server
To use GDB with any target hardware, one needs a GDB server to translate the GDB commands to those supported by the target hardware, along with sending the commands over a wire from the host to the target and back.This is done in two ways, one is with a software GDB server, and one is with a hardware GDB server. With a software GDB server, a GDB server program is run on the host and it talks to the target via dedicated hardware. With a hardware GDB server, there is a hardware device that communicates with host, via Ethernet or RS-232 and has a built-in GDB server as well as its own set of commands. No software running on the host.
With either choice, you must inform GDB where the GDB server is located.
.GDBINIT
.gdbinit is the default file that GDB reads before it starts debugging your program. If the file doesn't exist, GDB doesn't complain about the missing file, but GDB won't talk to the hardware where you want to debug your program..gdbinit is a file that establishes the environment you want GDB to use with the target. You specify the DDR memory register values. You specify valid memory address ranges. You specify chip selects, etc. Anything that needs to be specified to be able to read and write to registers and memory of the target hardware.
Your program does all this, but GDB needs to talk to the hardware before your program starts.
In addition to configuring the remote target hardware, GDB needs to know where the hardware is.
target remote 192.168.0.10
This would be a typical command line added to .gdbinit, one of the first lines of the file.
One does not have use the file name .gdbinit. The command switch -x <filename> can be used to specify a file other than .gdbinit. On Linux systems, .gdbinit will be a hidden file. On Windows systems it is sometimes difficult to create a file with a period. [Notepad++ helps with this.]
Abatron BDI2000
Abatron, of Switzerland, produced a device, BDI2000, that supports the Motorola BDM and the CPU32 family of Motorola processors.The Abatron BDI2000 is an example of a hardware GDB server.
Abatron BDI2000 User's Manual
[BTW, this device is no longer manufactured. Abatron has announced in 2015 that they will be closing their doors. There are several listings on eBay. ]
The BDI2000 supports two native M68000 commands, ti and tc.
The ti command steps to the next machine instruction.
The tc command runs until there is a change in the flow of instructions. Essentially stop when there is a jump instruction.
The BDI2000 does not disassemble machine instructions. We use GDB to perform this task.
The Motorola 68332 starts at location 0. The first 32-bit value is top of the stack. The second 32-bit value is the address of the first instruction.
Upon 'reset' using the BDI2000, the 68332 program counter is at location 0x4.
How does one use GDB with such hardware?
You say "Can't you just use software breakpoints?". Yes, if the program is running out of RAM. If the program runs out of flash or EEProm, software breakpoints are not possible. A software break point requires the modification of the break location to replace the desired instruction with a BREAK interrupt instruction. Flash does not allow this substitution.
An alternative is to substitute a RAM chip for the Flash chip and download the program with each power on.
But let's say we don't want to change the hardware. We need to test and debug it the way it is.
Today with C/C++ compilers debugging in assembly is only for the most hard core programmers or difficult hardware bugs.
Convenience Variables and User-Defined Commands
Using convenience variables and user-defined commands, we are going to create break point commands, break on memory change, and other commands.The GDB command that allows access to the native BDI2000 commands is monitor or mon for short.
To list the M68K registers, enter
mon rd
GDB has the equivalent
info reg
But mon rd has a very compact presentation. GDB presents each register on a separate line.
mon reset will perform a soft reset of the CPU.
The BDI2000 converts the GDB command si into ti and the ni into ti. The normal n and s commands do not work as they expect native hardware breakpoints or software breakpoints.
We will simulate s and n with user-defined commands. The user-defined commands are added to .gdbinit.
User-Define Commands
The format of a user-defined command is
define <command>
<body of command>
end
document <command>
<documentation>
end
All the user-defined commands are listed with the GDB command help user-defined.
Convenience Variables
A convenience variable is defined as
set <variable> = <expression>
Reset, Read, Info
Let's define three user-defined commands, mreset, mrd, and minfo.
define mreset
mon reset
flushregs
end
document mreset
Send the BDI2000 command 'reset'
'flushregs' is required as GDB does not
know what 'mon reset' did to the target
end
define mrd
mon rd
end
document mrd
Send the BDI2000 command 'rd'. Reads better than 'info reg'
end
define minfo
mon info
end
document minfo
Send the BDI2000 command 'info'
end
User-defined commands are very useful to define repetitive commands.
mrd is a repetitive command to use BDI2000 to output registers instead of GDB, as the output is more compact.
minfo is again another short cut command.
A more useful command is beefcore.
define beefcore
mon mm 0xe00000 0xdeadbeef 0x10000
mon mm 0xe10000 0xdeadbeef 0x10000
mon mm 0xe20000 0xdeadbeef 0x10000
mon mm 0xe30000 0xdeadbeef 0x10000
mon mm 0xe40000 0xdeadbeef 0x10000
mon mm 0xe50000 0xdeadbeef 0x10000
mon mm 0xe60000 0xdeadbeef 0x10000
mon mm 0xe70000 0xdeadbeef 0x10000
end
document beefcore
Fill RAM (0xe00000), 1MB, with 0xdeadbeef
end
Now that we done the easy commands, let's build more complicated ones.
dxi
First let's make a short cut dxi.# dxi
# dxi <count>
# dxi <count> <startaddress>
define dxi
if $argc == 2
x /$arg0i $arg1
end
if $argc == 1
x /$arg0i $pc
end
if $argc == 0
x /20i $pc
end
end
document dxi
Output $arg0 instructions from current PC
If $arg0 is not supplied, output 20 instructions
end
# starts a comment in .gdbinit.
The user-defined command supports if/then/else and while constructs.
Just like a function, arguments can be passed to a user-defined command. $argc is the variable with the number of arguments. Each argument is $argx where x is the argument number starting with zero.
The command dxi has three forms,
dxi
dxi <number>
dxi <number> <address>
$pc is the built-in GDB variable for the current program counter.
With no arguments, dni executes the GDB command x /20i $pc. Output 20 assembly instructions starting at the program counter.
dxi <number> changes the 20 to <number>. Notice this is a string substitution. <number> is not a number in the 'int' sense. It is a string that GDB interprets as a number.
Finally, dxi <number> <address> outputs <number> of assembly instructions starting at <address>.
dxi is a short cut command, but with some variability.
BMEM - Break on Change of Contents of An Address
Below are three command bmemc, bmems, and bmeml. bmemloop is used by all three other commands. This shows GDB user-defined commands support subroutines.Each command executes an si command until the value at the specified address changes a specified number of times.
bmemc examines 8-bit values, bmems examines 16-bit values, and bmeml examines 32-bit values.
The logic of each command is the same.
Now the specific syntax of user-defined commands that is not well documented is shown.
#bmemloop ptr count
#internal function, called by bmemc, bmemw, and bmeml
define bmemloop
if $argc == 2
set $loop = $arg1
set $bmemlooprd = *($arg0)
set $bmemlooporg = *($arg0)
while ($loop > 0)
while ($bmemlooprd==$bmemlooporg)
si
set $bmemlooprd=*($arg0)
end
set $loop = $loop - 1
if $loop == 0
loop_break
end
set $bmemlooporg = *($arg0)
end
end
end
document bmemloop
First argument specified address to watch
Second argument specifies number of times to loop
for each change of the specified address
bmemloop 10000 5 - Loop until address 10000 changes 5 times
end
if $argc>=1
disable disp 1
set $bmemcx=(char*)($arg0)
set $bmemccount = 1
if $argc == 2
set $bmemccount = $arg1
end
bmemloop $bmemcx $bmemccount
enable disp 1
end
end
document bmemc
bmemc runs until the byte at the address specified changes
end
if $argc>=1
disable disp 1
set $bmemwx=(short*)($arg0)
set $bmemwcount = 1
if $argc == 2
set $bmemwcount = $arg1
end
bmemloop $bmemwx $bmemwcount
enable disp 1
end
end
document bmemw
bmemw runs until the short (16-bit) at the address specified changes
end
if $argc>=1
disable disp 1
set $bmemlx=(long*)($arg0)
set $bmemlcount = 1
if $argc == 2
set $bmemlcount = $arg1
end
bmemloop $bmemlx $bmemlcount
enable disp 1
end
end
document bmeml
bmeml runs until the long (32-bit) at the address specified changes
end
Let's discuss some of the syntax.
set $bmemcx=(char*)($arg0)
set $bmemwx=(short*)($arg0)
set $bmemlx=(long*)($arg0)
set $bmemwx=(short*)($arg0)
set $bmemlx=(long*)($arg0)
These three lines each declare a convenience variable. Start with a dollar sign and create a name, start with a letter. The syntax that is not obvious is that the address specified as the first argument, arg0, of the command has to be casted to the appropriate type, a pointer of a specific width.
The syntax is the same as that of C.
The three convenience variables are now pointers.
We can pass the pointer to bmemloop, a subroutine, along with an optional count.
The enable and disable commands are used to suppress GDB output while the bmemloop is running. The si command outputs data each time it is executed.
To compare the value of two addresses we need pointers. bmemloop creates two convenience variables, bmemlooporg, the original value pointed to by arg0 and bmemlooprd, the value pointed to by arg0 after each si command.
bmemw 20000
This command will execute the si command until the 16-bit value at 20000 changes.
bmemc 0x30000 10
This command will execute the si command until the 8-bit value at 0x30000 changes 10 times.
Note that the address is hexadecimal. The cast to a pointer understands hexadecimal notation.
Performance
The commands bmemc, bmemw, and bmeml work, but they are slow. We are using the GDB interpreter to do something, that is today, done by hardware, but it does work.
Break on an Address - brk
Now let's create a break point user-defined command, brk.
# brk
# brk <dest_address>
# brk <dest_address> <count>
# brk <dest_address> <count> <offset>
define brk
if $argc >= 1
disable disp 1
set $brkcount = 1
if $argc >=2
set $brkcount = $arg1
end
set $targ = (long)($arg0)
if $argc == 3
set $targ = $targ + $arg2
end
set $brkx = (unsigned char *)($targ)
print $brkx
if $brkcount > 0
set $loop = 0
while $brkx == $pc
si
end
while ($loop < $brkcount)
while ($brkx != $pc)
si
end
set $loop = $loop +1
if $loop == $brkcount
loop_break
end
si
end
else
print "Loop count cannot be negative"
end
enable disp 1
end
if $argc == 0
si
end
end
document brk
brk <dest_address> [count offset]
Send 'si' commands until <dest_address> reached
If count specified, send 'si' commands until <dest_address> is reached <count> times.
If offset specified, add <offset> to <dest_address> to make break address.
When using <offset>, must specify count.
brk $ReadSwitches 1 0x20 - break at $ReadSwitches+0x20 once.
end
The user-define command brk accepts one to three arguments. The first argument, mandatory, is the address we want to break on. The second optional argument is how many times we want to break before stopping. The third optional argument is an offset to add to the address to specify the break point address.
while ($brkx != $pc)
Execute si commands until the program counter is the same as the specified address.
Again the enable and disable commands are used.
brk is no speed demon, but it works.
The use of convenience variables and user-defined commands creates GDB commands that emulate break point commands.
See this bitbucket repository for all of the user-defined commands in one file.
See this bitbucket repository for all of the user-defined commands in one file.