GDB for beginners

  • up
    48%
  • down
    52%

GDB for beginners.

1. Introduction.
2. What is GDB?
3. First steps.
4. Final words.
5. Quick reference.
6. Links.

Introduction.


On the Internet you can find a lot of GDB tutorials; ranging from the simple ones, which give you a basic reference for GDB, up to the big tutorials and documentation (including the official ones). Of course, none of these are written in the context of Amiga OS4, because for the rest of word AmigaOS is very obscure, and as result all those GDB tutorials are oriented towards x86/UNIX, or occasionally PPC/Sparc/Alpha based Unixes (although those still look pretty much the same, as it is still UNIX). In the case of Amiga OS4, the SDK documentation just includes the official GDB docs, which cover just the GDB itself without taking Amiga OS4 into account.

Some of you may remember that GDB on Amiga OS4 worked reasonably well on the early versions of the OS, prior to the first releases of OS4.1, which brought a lot of changes, some of which caused GDB to stop working properly. But with the release of Amiga OS4.1 update 3, GDB once again started to work more-or-less as expected, and lately (with release of Update 4) it is usable again. As result, all of the information in this article are based on the GDB from latest SDK 53.20 (GDB version 6.3, 2005.07.19 build) and Amiga OS4.1 update4.

7/0.RAM Disk:> gdb -v
GNU gdb 6.3 (AmigaOS build 20050719)
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "ppc-amigaos".
 
7/0.RAM Disk:> 

What is GDB?


GDB is not just a simple debugger or disassembler, but a full debug environment that gives you the ability to debug your program in real time, disassemble your code, dump or disassemble memory areas, attach to running processes (e.g. a commodity that is already running), step through source code or assembler instructions, set breakpoints on labels or addresses, display variable values, and just about anything else a serious programmer may need.

So of what use is GDB to a beginner programmer? Why have GDB on Amiga OS at all? Put simply, GDB enables you to determine exactly what is happening inside your program, and help you figure out why it runs into trouble. For example, if you wrote a program, but it doesn't work how you expected it to, and/or it crashes. You can use GDB to set a breakpoint somewhere before the problem, and launch the program. The program will run until it reaches the breakpoint, and then you can check the state of the program, its variables, dump memory buffers, inspect the stack and see exactly what is going on. Then, after you have investigated everything you need to look at, you can continue execution of the program, or step over the code (either at source code or machine code level), to see what is happening.

GDB on Amiga OS4 is also good for any research of work, for example you can find out at which address OS4 loads a relocated ELF, which addresses it uses for kernel areas, what happens when programs calls the system parts of OS and so on. Of course, it is entirely possible to do some non-interactive debugging without GDB, like plain disassembly (with "objdump" for example), but static disassembly is not real time debugging, so you can't check what happens exactly at a given moment of a program's execution.

Sometimes it is useful to be able to use a debugger when you have no documentation to hand, and don't know how some functions behave, or what they return and where. Also, it can sometimes be the case that even the documentation is wrong and the debugger can help you confirm exactly what is wrong to fill out a proper bug-report or for creating a patch/fix.

When using GDB, it is necessary to include debugging information in the binary. This information will you to reference functions and variables by their names, and set breakpoints at named functions, like at "main()" to debug from the start of your program, or function "foo()". It is possible to debug binaries which have no debugging information at all, but then you will not be able to follow the high-level source code (usually C/C++), and will only be able to examine the assembly code. Without debug information you are usually forced to start at the "_start" label, which is where the all real code start (which is in the C/C++ library startup code for C/C++ programs).

GCC has many flags to specify the kind of debugging information to be included in the binary, including -g, -ggdb, -gstabs and -gstabs+. The only flag which is recommended for use on Amiga OS4 is -gstabs. So, whenever you wish to create a binary with debugging information, you should add "-gstabs". Other ones may or may not work, but to avoid any issues just use "-gstabs". The same is true if you want to just use "addr2line" to find the address of a crash from a stack trace.


First steps.

Lets start with the simple Hello World example:

7/0.RAM Disk:> type hello.c
#include <stdio.h>
main()
{
    printf("just hello");
 
    return 0;
}
7/0.RAM Disk:> gcc -gstabs hello.c -o hello
 
7/0.RAM Disk:> gdb -q hello
(gdb) run
Starting program: /RAM Disk/hello 
just hello
Program terminated with signal SIGQUIT, Quit.
The program no longer exists.
(gdb) quit
7/0.RAM Disk:> 


In the above example, we compiled a binary with debug information, and ran the binary inside the debugger. The "-q" (or "--quiet") option on the command line just tells GDB not to print version information on startup.

For basic debugging, you need to know how to set a breakpoint, how to step over the code, and how to view code, variable and memory contents. The most useful commands for beginners are: run, break, disassemble, next, step, stepi and continue. For all the GDB commands you always have some short names, like b for break, r for run, disas for disassembly, si for stepi and so on. Lets cover the most common scenarios:


Breakpoints


Just running a program in the debugger isn't very useful - we need to pause execution and examine the state of the code at a particular point. For that, we use the "breakpoint" command ("break" or "b" for short). For example:

    break _start      : break at beginning of the _start() function
    break 10          : break at line 10 of the current file
    break *0xXXXXXXX  : break at the address

Lets see it in action:

 
7/0.RAM Disk:> gdb -q hello
(gdb) break main
Breakpoint 1 at 0x7f974208: file hello.c, line 3.
(gdb) info break
Num Type           Disp Enb Address    What
1   breakpoint     keep y   0x7f974208 in main at hello.c:3
(gdb) run
Starting program: /RAM Disk/hello 
BS 653c1a68
Current action: 2
 
Breakpoint 1, main () at hello.c:3
3       {
(gdb)   


Here we have set a breakpoint at beginning of program, displayed the current breakpoints (there is only the one we just set), then ran the program. The program execution stopped at the beginning of the main function (this is called "hitting a breakpoint"), and returned to the GDB prompt. Once at the GDB prompt, you can disable or enable any breakpoint (via "enable" and "disable" commands), as well as removing it with the "clear" command.


Step by step


Once you have hit a breakpoint, you can have fine control over the execution of the program, using the following commands:

  next (n) - Execute current statement and move to the next one in the function.
 
  step (s) - The same as next, but with difference that if you are at a function call and you hit next, then the function will execute and return. But if you hit step, then you will step into the first line of the called function.
 
  stepi (si)  - Stepping a single assembly instruction at a time. This is only for experienced developers with knowledge of assembly language.
 
  continue (c) - If you are finished with manual stepping, then you can just type "c" and the program will continue execution until the next breakpoint is reached, or the program reaches the end.

Here is a more in-depth example:

7/0.RAM Disk:> gdb -q hello
(gdb) list
1       #include <stdio.h>
2       main()
3       {
4          printf("just hello");
5          return 0;
6       }
(gdb) break main
Breakpoint 1 at 0x7f974208: file hello.c, line 3.
(gdb) r
Starting program: /RAM Disk/hello 
BS 63afafd8
Current action: 2
 
Breakpoint 1, main () at hello.c:3
3       {
(gdb) step
main () at hello.c:4
4          printf("just hello");
(gdb) step
0x7f97424c in printf ()
(gdb) step
0x7f974254 in __NewlibCall ()
(gdb) step
just hello
Program terminated with signal SIGQUIT, Quit.
The program no longer exists.
(gdb) 


In that example we examine the source of the program via the "list" command, set break at main(), and then step over the main function. As you can see, printf() also calls the __Newlibcall() function (which is part of the newlib libc.a). You could also debug to see what exactly it does, which argument it takes and what happens after.

By the way; when you are stepping with stepi (i.e. per instruction) and you reach a "bctrl" instruction, then, via stepi you will jump into the kernel, which is probably not what you want, so to jump over that kind of instruction, and to avoid entering to kernel, you can just do:

(gdb) break _start
(gdb) r
(gdb) disas
.....
(gdb) break *0xaddress_after_bctrl
(gdb) c

and then again continue with stepi.

Disassembling


When you reach a point in your program where a crash happens, you might want to see how your C function looks "for real", in assembly language. For this, you use the "disassemble" command (or "disas" for short):

7/0.RAM Disk:> gdb -q hello
(gdb) break main
Breakpoint 1 at 0x7f974208: file hello.c, line 3.
(gdb) r
Starting program: /RAM Disk/hello 
BS 6624f370
Current action: 2
 
Breakpoint 1, main () at hello.c:3
3       {
(gdb) disas
Dump of assembler code for function main:
0x7f974208 <main+0>:    stwu    r1,-16(r1)
0x7f97420c <main+4>:    mflr    r0
0x7f974210 <main+8>:    stw     r31,12(r1)
0x7f974214 <main+12>:   stw     r0,20(r1)
0x7f974218 <main+16>:   mr      r31,r1
0x7f97421c <main+20>:   lis     r9,25978
0x7f974220 <main+24>:   addi    r3,r9,-24520
0x7f974224 <main+28>:   crclr   4*cr1+eq
0x7f974228 <main+32>:   bl      0x7f97424c <printf>
0x7f97422c <main+36>:   li      r0,0
0x7f974230 <main+40>:   mr      r3,r0
0x7f974234 <main+44>:   lwz     r11,0(r1)
0x7f974238 <main+48>:   lwz     r0,4(r11)
0x7f97423c <main+52>:   mtlr    r0
0x7f974240 <main+56>:   lwz     r31,-4(r11)
0x7f974244 <main+60>:   mr      r1,r11
0x7f974248 <main+64>:   blr
End of assembler dump.
(gdb) 


Of course for real work you should know at least the basics of PowerPC assembly. You might want to read a book on this (the best one is called the "green book", check the links section), but even without deep knowledge you can see that there some magic happens, then a jump to "printf", and then the rest of the code performs a clean exit (return 0). Those strange values at the <main+20> and <main+24> are offsets to our "hello world" buffer.


Get more info


As you should know, assembly language is a set of instructions that the CPU understands directly. The PowerPC architecture is designed to manipulate data inside CPU registers. It has 32 general registers, 32 floating point registers, and some special ones. So, with every single "stepi", some registers will change their state, and that can be seen by the:

   -- info reg    - show all the registers at current moment
   -- info reg X  - show only register X


With this information, you can get to know exactly which data is placed in the registers, which memory regions they point to or what values they have.

You can also dump any memory to which your program has access, and to do this you use the "x" command (short for "examine"). The options for this command are the size and format of data to dump and an address where the data you are interested in resides. This can be pretty helpful when you want to know what kind of data you have in the stack, or an array. Lets take a look at that example:

7/0.RAM Disk:> gdb -q hello
(gdb) break main
Breakpoint 1 at 0x7f974208: file hello.c, line 3.
(gdb) r
Starting program: /RAM Disk/hello 
BS 657d7430
Current action: 2
 
Breakpoint 1, main () at hello.c:3
3       {
(gdb) disas
Dump of assembler code for function main:
0x7f974208 <main+0>:    stwu    r1,-16(r1)
0x7f97420c <main+4>:    mflr    r0
0x7f974210 <main+8>:    stw     r31,12(r1)
0x7f974214 <main+12>:   stw     r0,20(r1)
0x7f974218 <main+16>:   mr      r31,r1
0x7f97421c <main+20>:   lis     r9,25978
0x7f974220 <main+24>:   addi    r3,r9,-24520
0x7f974224 <main+28>:   crclr   4*cr1+eq
0x7f974228 <main+32>:   bl      0x7f97424c <printf>
0x7f97422c <main+36>:   li      r0,0
0x7f974230 <main+40>:   mr      r3,r0
0x7f974234 <main+44>:   lwz     r11,0(r1)
0x7f974238 <main+48>:   lwz     r0,4(r11)
0x7f97423c <main+52>:   mtlr    r0
0x7f974240 <main+56>:   lwz     r31,-4(r11)
0x7f974244 <main+60>:   mr      r1,r11
0x7f974248 <main+64>:   blr
End of assembler dump.
(gdb) break *0x7f974224
Breakpoint 2 at 0x7f974224: file hello.c, line 4.
(gdb) c
Continuing.
Current action: 0
BS 63b03568
Current action: 2
 
Breakpoint 2, 0x7f974224 in main () at hello.c:4
4          printf("just hello");
(gdb) info reg r3
r3             0x6579a038       1702469688
(gdb) x/1s 0x6579a038
0x6579a038 <_SDA_BASE_+28756968>:        "just hello"
(gdb)  


We set a breakpoint at main(), just to have ability to run and disassemble the main function. We then set a breakpoint at the "crclr" instruction before the jump to printf, and continued execution. We then hit the breakpoint, which means that the CPU stopped just before executing the instruction at 0x7f974224. The instructions just before that (at addresses 0x7f97421c and 0x7f974220) calculated an address and stored it in register 3 (r3). We viewed the contents of r3 with the "info reg r3" command, and saw that it contained "0x6579a038". We then examined the memory at that address as a string, and we can see that it is our "just hello" string, so we can see that the first argument to printf is placed in r3.


Final words.


So now, even at this point, you can already start to do some real-time debugging on OS4. Of course, this article only covers a few commands and examples. GDB has hundreds of commands with all kinds of features and possibilities, but hopefully this small guide will help to get you started.

Sure, to use GDB fully, you should know PowerPC assembly language, C, and how it all works at low-level, but you don't have to know all that for GDB to be useful. Hopefully this has helped you get started.

If that there is demand, I will write more articles to explain more aspects of our GDB, its different features and abilities.

Thanks for reading, and for Peter Gordon for proof-reading and helping with corrections.

Quick reference

--basics---

    amiga shell:> gdb filename   - start gdb with loaded binary to it
    (gdb) file filename          - load file when you are already in GDB
    (gdb) run                    - run :)
    (gdb) run arg1 arg2 etc      - run program with command line args
    (gdb) quit                   - quit :)
    (gdb) help command           - get help for a certain command

--breakpoints--

 
    (gdb) break foo()       - set break at function
    (gdb) break 5           - set break at line 5 of source code
    (gdb) break *0xXXXXXXXX - set break at address
    (gdb) info break        - display breakpoints
    (gdb) delete X          - delete breakpoint X (where X is a breakpoint number from "info break")
    (gdb) enable X          - enable breakpoint X
    (gdb) disable X         - disable breakpoint X

--disassembling--

 
    (gdb) disas               - disassemble current function
    (gdb) disas 0xXXXXXXX     - disassemble by address (unlike setting a breakpoint by address, you don't need a '*')
    (gdb) x/[num]i 0xXXXXXXX  - disassemble by "examine" any amount of instructions ([num]) at any address in memory

--stepping--

 
    (gdb) step               - step into the function on this line (if possible)
    (gdb) stepi              - step by assembly instructions
    (gdb) next               - run to the next line of this function
    (gdb) continue           - continue to next breakpoint (or till end)
 
    for all the stepping functions you can specify how many steps to do, e.g. stepi 100, or next 20.

--registers--

 
    (gdb) info registers     - show integer registers only (the most usable)
    (gdb) info all-registers - show all registers (including floating point ones, special ones etc, not used so often)
    (gdb) info reg X         - show only register X 

--misc--

 
    (gdb) list      - examine the source of the program
    (gdb) bt        - alias of "backtrace": show the current stack
    (gdb) RETURN    - if you hit "enter" while you in gdb, it will repeat the last command

Links.

[1] Green Book - the best for ppc-assembly.
[2] Article on DevPit - GDB relates, and was originally written from a 32bit PowerPC architecture perspective and register information will vary across architectures.
[3] Original GDB docs

You also can get article in the .txt format here.

Tags: 

Blog post type: 

Comments

Great article. I was looking for something like that. The debugging on programming is the most useful thing to know. Thanks again for that blog post.

jaokim's picture

Great intiative and great article!
Thank you!

YesCop's picture

I agree with my "colleagues". Great initiative and article.
For me nothing is really new but I will appreciate some more focus on how to read not memories but C++ variables like arrays or struct.
Of course if this is possible in GDB.