11 KiB
+++ title = "GDB" +++
GDB
GDB is short for the GNU Debugger. GDB is a program that helps you track down errors by interactively debugging them (“GDB: The Gnu Project Debugger”1). It can start and stop your program, look around, and put in ad hoc constraints and checks. Here are a few examples.
Setting breakpoints programmatically
A breakpoint is a line of code where you want the execution to stop and give control back to the debugger. A useful trick when debugging complex C programs with GDB is setting breakpoints in the source code.
int main() {
int val = 1;
val = 42;
asm("int $3"); // set a breakpoint here
val = 7;
}
$ gcc main.c -g -o main
$ gdb --args ./main
(gdb) r
[...]
Program received signal SIGTRAP, Trace/breakpoint trap.
main () at main.c:6
6 val = 7;
(gdb) p val
$1 = 42
You can also set breakpoints programmatically. Assume that we have no optimization and the line numbers are as follows
int main() {
int val = 1;
val = 42;
val = 7;
}
We can now set the breakpoint before the program starts.
$ gcc main.c -g -o main
$ gdb --args ./main
(gdb) break main.c:4
[...]
(gdb) p val
$1 = 42
Checking memory content
We can also use gdb to check the content of different pieces of memory. For example,
int main() {
char bad_string[3] = {'C', 'a', 't'};
printf("%s", bad_string);
}
Compiled we get
$ gcc main.c -g -o main && ./main
$ Cat ZVQ<56> $
We can now use gdb to look at specific bytes of the string and reason about when the program should’ve stopped running
(gdb) l
1 #include <stdio.h>
2 int main() {
3 char bad_string[3] = {'C', 'a', 't'};
4 printf("%s", bad_string);
5 }
(gdb) b 4
Breakpoint 1 at 0x100000f57: file main.c, line 4.
(gdb) r
[...]
Breakpoint 1, main () at main.c:4
4 printf("%s", bad_string);
(gdb) x/16xb bad_string
0x7fff5fbff9cd: 0x63 0x61 0x74 0xe0 0xf9 0xbf 0x5f 0xff
0x7fff5fbff9d5: 0x7f 0x00 0x00 0xfd 0xb5 0x23 0x89 0xff
(gdb)
Here, by using the x command with parameters 16xb
, we can see that starting at memory address 0x7fff5fbff9c`` (value of
bad_string),
printf` would actually see the following sequence of bytes as a string because we provided a malformed string without a null terminator.
Involved gdb example
Here is how one of your TAs would go through and debug a simple program that is going wrong. First, the source code of the program. If you can see the error immediately, please bear with us.
#include <stdio.h>
double convert_to_radians(int deg);
int main(){
for (int deg = 0; deg > 360; ++deg){
double radians = convert_to_radians(deg);
printf("%d. %f\n", deg, radians);
}
return 0;
}
double convert_to_radians(int deg){
return ( 31415 / 1000 ) * deg / 180;
}
How can we use gdb to debug? First we ought to load GDB.
$ gdb --args ./main
(gdb) layout src; # If you want a GUI type
(gdb) run
(gdb)
Want to take a look at the source?
(gdb) l
1 #include <stdio.h>
2
3 double convert_to_radians(int deg);
4
5 int main(){
6 for (int deg = 0; deg > 360; ++deg){
7 double radians = convert_to_radians(deg);
8 printf("%d. %f\n", deg, radians);
9 }
10 return 0;
(gdb) break 7 # break <file>:line or break <file>:function
(gdb) run
(gdb)
From running the code, the breakpoint didn’t even trigger, meaning the code never got to that point. That’s because of the comparison! Okay, flip the sign it should work now right?
(gdb) run
350. 60.000000
351. 60.000000
352. 60.000000
353. 60.000000
354. 60.000000
355. 61.000000
356. 61.000000
357. 61.000000
358. 61.000000
359. 61.000000
(gdb) break 14 if deg == 359 # Let's check the last iteration only
(gdb) run
...
(gdb) print/x deg # print the hex value of degree
$1 = 0x167
(gdb) print (31415/1000)
$2 = 0x31
(gdb) print (31415/1000.0)
$3 = 201.749
(gdb) print (31415.0/10000.0)
$4 = 3.1414999999999999
That was only the bare minimum, though most of you will get by with that. There are a whole load more resources on the web, here are a few specific ones that can help you get started.
Shell
What do you actually use to run your program? A shell! A shell is a programming language that is running inside your terminal. A terminal is merely a window to input commands. Now, on POSIX we usually have one shell called sh
that is linked to a POSIX compliant shell called dash
. Most of the time, you use a shell called bash
that is somewhat POSIX compliant but has some nifty built-in features. If you want to be even more advanced, zsh has some more powerful features like tab complete on programs and fuzzy patterns.
Undefined Behavior Sanitizer
The undefined behavior sanitizer is a wonderful tool provided by the llvm project. It allows you to compile code with a runtime checker to make sure that you don’t do undefined behavior for various categories. We will try to include it into our projects, but requires support form all the external libraries that we use so we may not get around to all of them. https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html
Undefined behavior - why we can’t solve it in general
Also please please read Chris Lattner’s 3 Part blog post on undefined behavior. It can shed light on debug builds and the mystery of compiler optimization.
http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html
Clang Static Build Tools
Clang provides a great drop-in replacement tools for compiling programs. If you want to see if there is an error that may cause a race condition, casting error, etc, all you need to do is the following.
$ scan-build make
And in addition to the make output, you will get static build warnings.
strace and ltrace
strace
and ltrace
are two programs that trace the system calls and library calls respectively of a running program or command. These may be missing on your system so to install feel free to run the following.
$ sudo apt install strace ltrace
Debugging with ltrace
can be as simple as figuring out what was the return call of the last library call that failed.
int main() {
FILE *fp = fopen("I don't exist", "r");
fprintf(fp, "a");
fclose(fp);
return 0;
}
> ltrace ./a.out
__libc_start_main(0x8048454, 1, 0xbfc19db4, 0x80484c0, 0x8048530 <unfinished ...>
fopen("I don't exist", "r") = 0x0
fwrite("Invalid Write\n", 1, 14, 0x0 <unfinished ...>
--- SIGSEGV (Segmentation fault) ---
+++ killed by SIGSEGV +++
ltrace
output can clue you in to weird things your program is doing live. Unfortunately, ltrace
can’t be used to inject faults, meaning that ltrace can tell you what is happening, but it can’t tamper with what is already happening.
strace
on the other hand could modify your program. Debugging with strace
is amazing. The basic usage is running strace
with a program, and it’ll get you a complete list of system call parameters.
$ strace head README.md
execve("/usr/bin/head", ["head", "README.md"], 0x7ffff28c8fa8 /* 60 vars */) = 0
brk(NULL) = 0x7ffff5719000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=32804, ...}) = 0
...
If the output is too verbose, you can use trace=
with a command delimited list of syscalls to filter all but those calls.
$ strace -e trace=read,write head README.md
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\260\34\2\0\0\0\0\0"..., 832) = 832
read(3, "# Locale name alias data base.\n#"..., 4096) = 2995
read(3, "", 4096) = 0
read(3, "# C Datastructures\n\n[![Build Sta"..., 8192) = 1250
write(1, "# C Datastructures\n", 19# C Datastructures
You can also trace files or targets.
$ strace -e trace=read,write -P README.md head README.md
strace: Requested path 'README.md' resolved into '/mnt/c/Users/user/personal/libds/README.md'
read(3, "# C Datastructures\n\n[![Build Sta"..., 8192) = 1250
Newer versions of strace
can actually inject faults into your program. This is useful when you want to occasionally make reads and writes fail for example in a networking application, which your program should handle. The problem is as of early 2019, that version is missing from Ubuntu repositories. Meaning that you’ll have to install it from the source.
printfs
When all else fails, print! Each of your functions should have an idea of what it is going to do. You want to test that each of your functions is doing what it set out to do and see exactly where your code breaks. In the case with race conditions, tsan may be able to help, but having each thread print out data at certain times could help you identify the race condition.
To make printfs useful, try to have a macro that fills in the context by which the printf was called – a log statement if you will. A simple useful but untested log statement could be as follows. Try to make a test and figure out something that is going wrong, then log the state of your variables.
#include <execinfo.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <unistd.h>
// bt is print backtrace
const int num_stack = 10;
int __log(int line, const char *file, int bt, const char *fmt, ...) {
if (bt) {
void *raw_trace[num_stack];
size_t size = backtrace(raw_trace, sizeof(raw_trace) / sizeof(raw_trace[0]));
char **syms = backtrace_symbols(raw_trace, size);
for(ssize_t i = 0; i < size; i++) {
fprintf(stderr, "|%s:%d| %s\n", file, line, syms[i]);
}
free(syms);
}
int ret = fprintf(stderr, "|%s:%d| ", file, line);
va_list args;
va_start(args, fmt);
ret += vfprintf(stderr, fmt, args);
va_end(args);
ret += fprintf(stderr, "\n");
return ret;
}
#ifdef DEBUG
#define log(...) __log(__LINE__, __FILE__, 0, __VA_ARGS__)
#define bt(...) __log(__LINE__, __FILE__, 1, __VA_ARGS__)
#else
#define log(...)
#define bt(...)
#endif
//Use as log(args like printf) or bt(args like printf) to either log or get backtrace
int main() {
log("Hello Log");
bt("Hello Backtrace");
}
And then use as appropriately. Check out the compiling and linking section in the appendix if you have any questions on how a C program gets translated to machine code.
-
“GDB: The Gnu Project Debugger.” 2019. GDB: The GNU Project Debugger. Free Software Foundation. https://www.gnu.org/software/gdb/. ↩︎