Exploring Debugger (ITS8020)
This lab will show how a debugger could be used to explore a running program. We also try to use objdump program for disassembling.
Since the compiler by default wants to generate the smallest possible executable (as far as effortless compilation is concerned), it removes data about function names from the file (it's called the symbol table). To prevent stripping the symbol table from the file, use -g for compilation.
gcc -g filename.c -o filename
Thus the need to create makefile for easier compilation.
Why the symbol table is needed? Debugger uses it to provide valuable additional data while debugging. Note that in the world of embedded real-time systems, any modifications to the executable, even the -g or optimizations might affect the performance characteristics.
In short: while developing a program, use the -g option for compilation.
We use command-line debugger gdb - GNU DeBugger. Note that it does have graphical front-end, named ddd, but will take effort to get it running properly over X under windows. GDB is powerful and suprisingly easy to use.
- GDB manual
- You can type "help" or "help command" in gdb for reminder of commands and their syntax. There are many, but you will probably only use a small subset of them anyway.
To run it with your compiled program, type:
It will show the prompt (gdb) and nothing will happen. The program is waiting for you to set up breakpoints/watches and the run command.
To exit gdb, type:
Running the program
To run a program in gdb, use the command:
The program will run until you interrupt it with CTRL+C (in which case the program will be stopped wherever it was and you can examine wherever you are), the program finishes or crashes. Crashes are most interesting, since you get to see where the crash happens and examine the stack.
You can also specify the arguments after the "run" command.
Task: Run a program withing gdb, then run a crashing program within gdb. Observe the results.
To stop your program a certain point, you can add breakpoints to the program before running it. You can set them to functions, line numbers or even to specific lines on spefific files. If you need, you can even stop when your program tries to run a specific instruction on some memory address.
- break function
- break linenumber
- break file:function
- break file:linenumber
- break *address
To see the breakpoints, type
If you make mistakes, delete them (you get numbers with "info break"):
delete breakpoint number
Task: Add a breakpoint, then run until it arrives.
Examining the program
You should now have a stopped program somewhere. Debugger is excellent for inspecting memory: variables and stack can be read.
Most basic program for content is print:
print variablename print array print $eax
You can also dereference pointers with *. Print is so common, that it can be shortened to just 'p'.
Tips: You can eXamine memory with the x command. To see all the registers, write 'info registers'.
Task: Examine the variables in your program. Add some struct to your program and try whether and how it can be seen.
Changing the variables
Changing variables is extremely easy. You can just use 'print' command with = .
print x = 888 p c = '\0'
If you don't want to see the variable, you can also use the 'set' command:
set var x = 99 set var c = EOF
You can examine the call stack with backtrace command: it's human readable and you get the line numbers and function names for each stack frame:
For just getting the current frame information, type:
Task Find a program with segmentation fault (or create one). Run it until it crashes, then examine where it did so.
Tip: To examine local variables of some other frame, you can use 'up' command to move upwards in stack frames, then use 'frame'. Try it.
You can also make the stopped program to run in small increments. The commands for that are 'next' and 'step'.
- next - execute the next line of code.
- step - execute the next line of code while stepping into any called functions
- continue - continue running normally
If you want to see the line where you are:
You can add a line number to list, to see code in particurlar location.
Tip: you can also run by only one instruction: 'stepi' and 'nexti' commands do this.
Task: Run a program with breakpoints, try to understand the difference between 'step' and 'next'.
You can stop the program whenever some variable changes. Such places are called watchpoints:
Task: Add a watchpoint for some variable, see how it behaves. Use 'continue' to keep running.
You can turn the numbers that are fed into the processor into readable assembly:
disas disas functionname
Task: Try to disassemble some simple arithmetic function. Observe how it manages the stack before starting the computation and how it prepares it before returning.
You can also disassemble with the 'objdump' program.
Task: Disassemble a program: how does the result differ from gdb's disas? Also, test other options for objdump.
Once you understand how the stack is managed, assembler language starts to make sense. You can disassemble any compiled program, but they are messy without the source code and symbol table.
You could try to write a program which runs two functions, the first one quits, the second one does something interesting. You could then change the "main" function so that its 'call' instruction would call the second function instead of the first without looking at the source code. Alternatively (bonus task) one could find a 'jmp' instruction and change its address to jump just before the second function. Simple replacement of 4 bytes can make a lot of difference.
I hope that the process was enlightening. Use it for good, not evil.