GDB — Intermediate

Recap: Here are the GDB-basics in case you need a recap.
We will be learning the following as part of the GDB-Intermediate article:
- Auto Display
- Attaching to a forked child process
- Attaching to an already running process
- Using gdb to alter the flow of program
- Examining memory areas
Auto Display
Instead of using the print command to watch the value of a variable or contents of a memory region for every line of execution, gdb allows us to create a watch list and automatically prints those values when we step through commands. display command allows us to both print variables and contents of a memory region.
Allowed formats of using display command :
(gdb) display expression
(gdb) display/format expression
(gdb) display/format address
Where, format can be used similar to format used with x command.
Let’s consider a simple word reversing program:
#include <stdio.h>
#include <string.h>int main(){
char *word = "test";
char reverseword[strlen(word)+1];
unsigned int letters_remaining = strlen(word);
char *wordpointer = &word[strlen(word)-1];
int i = 0;
while(letters_remaining > 0){
reverseword[i++] = *wordpointer--;
letters_remaining--;
}
printf("So the reversed word is %s\n",reverseword);
return 0;
}
Debugging the program:
(gdb) r
Starting program: /<path>/autodisplayBreakpoint 1, main () at autodisplay.c:5
5 char *word = "test";
(gdb) c
Continuing.Breakpoint 2, main () at autodisplay.c:10
10 while(letters_remaining > 0){
(gdb) display letters_remaining
1: letters_remaining = 4
(gdb) display *wordpointer
2: *wordpointer = 116 't'
(gdb) display/5cb reverseword
3: x/5cb reverseword
0x7fffffffe4a0: 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000'
(gdb) n
11 reverseword[i++] = *wordpointer--;
3: x/5cb reverseword
0x7fffffffe4a0: 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000'
2: *wordpointer = 116 't'
1: letters_remaining = 4
(gdb)
12 letters_remaining--;
3: x/5cb reverseword
0x7fffffffe4a0: 116 't' 0 '\000' 0 '\000' 0 '\000' 0 '\000'
2: *wordpointer = 115 's'
1: letters_remaining = 4
(gdb)
10 while(letters_remaining > 0){
3: x/5cb reverseword
0x7fffffffe4a0: 116 't' 0 '\000' 0 '\000' 0 '\000' 0 '\000'
2: *wordpointer = 115 's'
1: letters_remaining = 3
(gdb)
11 reverseword[i++] = *wordpointer--;
3: x/5cb reverseword
0x7fffffffe4a0: 116 't' 0 '\000' 0 '\000' 0 '\000' 0 '\000'
2: *wordpointer = 115 's'
1: letters_remaining = 3
(gdb)
12 letters_remaining--;
3: x/5cb reverseword
0x7fffffffe4a0: 116 't' 115 's' 0 '\000' 0 '\000' 0 '\000'
2: *wordpointer = 101 'e'
1: letters_remaining = 3
(gdb)
10 while(letters_remaining > 0){
3: x/5cb reverseword
0x7fffffffe4a0: 116 't' 115 's' 0 '\000' 0 '\000' 0 '\000'
2: *wordpointer = 101 'e'
1: letters_remaining = 2
(gdb)
11 reverseword[i++] = *wordpointer--;
3: x/5cb reverseword
0x7fffffffe4a0: 116 't' 115 's' 0 '\000' 0 '\000' 0 '\000'
2: *wordpointer = 101 'e'
1: letters_remaining = 2
(gdb)
12 letters_remaining--;
3: x/5cb reverseword
0x7fffffffe4a0: 116 't' 115 's' 101 'e' 0 '\000' 0 '\000'
2: *wordpointer = 116 't'
1: letters_remaining = 2
(gdb)
10 while(letters_remaining > 0){
3: x/5cb reverseword
0x7fffffffe4a0: 116 't' 115 's' 101 'e' 0 '\000' 0 '\000'
2: *wordpointer = 116 't'
1: letters_remaining = 1
(gdb)
11 reverseword[i++] = *wordpointer--;
3: x/5cb reverseword
0x7fffffffe4a0: 116 't' 115 's' 101 'e' 0 '\000' 0 '\000'
2: *wordpointer = 116 't'
1: letters_remaining = 1
(gdb)
12 letters_remaining--;
3: x/5cb reverseword
0x7fffffffe4a0: 116 't' 115 's' 101 'e' 116 't' 0 '\000'
2: *wordpointer = 0 '\000'
1: letters_remaining = 1
(gdb)
10 while(letters_remaining > 0){
3: x/5cb reverseword
0x7fffffffe4a0: 116 't' 115 's' 101 'e' 116 't' 0 '\000'
2: *wordpointer = 0 '\000'
1: letters_remaining = 0
(gdb)
14 printf("So the reversed word is %s\n",reverseword);
3: x/5cb reverseword
0x7fffffffe4a0: 116 't' 115 's' 101 'e' 116 't' 0 '\000'
2: *wordpointer = 0 '\000'
1: letters_remaining = 0
(gdb)
So the reversed word is tset
15 return 0;
3: x/5cb reverseword
0x7fffffffe4a0: 116 't' 115 's' 101 'e' 116 't' 0 '\000'
2: *wordpointer = 0 '\000'
1: letters_remaining = 0
(gdb) c
Continuing.
[Inferior 1 (process 4364) exited normally]
Attaching to a forked child process
Attaching to a forked child process
Let’s say you are writing a parallel program and you wish to spawn processes. The default behavior of gdb is to follow where the parent takes it.
Let’s consider a simple program that forks a child process to do some work in parallel with the main process .
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>void do_parent_process(){
sleep(1); /* Make sure parent is alive while the child works */
printf("PID of parent process: %d\n",getpid());
}void do_child_process(){
printf("PID of Child process: %d\n",getpid());
printf("PID of parent of child process: %d\n",getppid());
}
int main(){
pid_t pid;
pid = fork();
if(pid == 0){
/* Child process */
do_child_process();
}
else{
/* Parent process */
do_parent_process();
}
return 0;
}
If we debug the program with default behavior of gdb:
(gdb) b do_child_process
Breakpoint 1 at 0x400687: file main.c, line 11.
(gdb) b do_parent_process
Breakpoint 2 at 0x400661: file main.c, line 6.
(gdb) i b
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000000000400687 in do_child_process at main.c:11
2 breakpoint keep y 0x0000000000400661 in do_parent_process at main.c:6
(gdb) r
Starting program: <path>/fork-demo
PID of Child process: 2863
PID of parent of child process: 2859Breakpoint 2, do_parent_process () at main.c:6
6 sleep(1); /* To make sure parent is alive while the child works */
(gdb)
We see that gdb latched on the breakpoint in main process even though we had a breakpoint for the function in child process. So, how do we follow the child process?
‘set’ command to our rescue
(gdb) set follow-fork-mode child
(gdb) b do_child_process
Breakpoint 1 at 0x400687: file main.c, line 11.
(gdb) b do_parent_process
Breakpoint 2 at 0x400661: file main.c, line 6.
(gdb) r
Starting program: <path>/fork-demo
[New process 2874]
[Switching to process 2874]Breakpoint 1, do_child_process () at main.c:11
11 printf("PID of Child process: %d\n",getpid());
(gdb) PID of parent process: 2870(gdb) n
PID of Child process: 2874
12 printf("PID of parent of child process: %d\n",getppid());
(gdb)
Attaching to an already running process
Let’s say you wish to debug your daemon process (compiled with debug flag). You can easily attach to your process using the process ID and insert breakpoints.
$ ./daemonprocessOpen another terminal, find the pid of your process and attach to the process.
NOTE: Ubuntu requires sudo privileges to attach to an already running process
$ pgrep daemonprocess
8666
$ sudo gdb -q
(gdb) attach 8666
Attaching to process 8666
Reading symbols from <path>/daemonprocess...done.
Reading symbols from /lib/x86_64-linux-gnu/libc.so.6...Reading symbols from /usr/lib/debug//lib/x86_64-linux-gnu/libc-2.19.so...done.
done.
Loaded symbols for /lib/x86_64-linux-gnu/libc.so.6
Reading symbols from /lib64/ld-linux-x86-64.so.2...Reading symbols from /usr/lib/debug//lib/x86_64-linux-gnu/ld-2.19.so...done.
done.
Loaded symbols for /lib64/ld-linux-x86-64.so.2
0x00007f6844b27330 in __read_nocancel () at ../sysdeps/unix/syscall-template.S:81
81 ../sysdeps/unix/syscall-template.S: No such file or directory.
(gdb) b a_simple_loop
Breakpoint 1 at 0x400678: file daemonprocess.c, line 6.
(gdb) c
Continuing.Breakpoint 1, a_simple_loop (limit=50) at daemonprocess.c:6
6 for(iterator = 0; iterator < limit; iterator++){
(gdb) p limit
$1 = 50
Sometimes while debugging a program, you may need to alter the flow of program. There could be many reasons for altering the flow :
- Reset a counter in a loop
- Change the string input before performing a string operation
- Set the contents of some memory addresses of interest
- Observe the behavior of some code for a particular input
- Experiment with different values of a variable to test some piece of code
- Send a signal to your program
- Return prematurely from a function
Let’s get started with simple editing of variables using the ‘set’ command
#include <stdio.h>
#include <stdlib.h>typedef struct {
unsigned int balance;
} store;int credit(store *account, int amount){
account->balance += amount;
}int debit(store *account, int amount){
account->balance -= amount;
}int main(){
store *account = malloc(sizeof(store *));
credit(account, 200);
debit(account, 300);
printf("Balance in the store account = %u\n", account->balance);
}
Editing the variable value directly and using it’s memory address:
(gdb) r
Starting program: <path>/flow-alterBreakpoint 1, credit (account=0x602010, amount=200) at flow-alter.c:9
9 account->balance += amount;
(gdb) p amount
$1 = 200
(gdb) set var amount=5000
(gdb) p amount
$2 = 5000(gdb) c
Continuing.Breakpoint 2, debit (account=0x602010, amount=300) at flow-alter.c:13
13 account->balance -= amount;
(gdb) p &amount
$3 = (int *) 0x7fffffffde84
(gdb) set {int}0x7fffffffde84 = 50
(gdb) p amount
$4 = 50
(gdb)
Sometimes we may have missed some important lines of code and we may wish to jump back to the line for a second look. ‘jump’ command to our rescue! ‘jump’ command moves the execution to the specified line number and stops at the next breakpoint:
(gdb) r
Starting program: <path>/flow-alterBreakpoint 1, debit (account=0x602010, amount=300) at flow-alter.c:13
13 account->balance -= amount;
(gdb) l
8 int credit(store *account, int amount){
9 account->balance += amount;
10 }
11
12 int debit(store *account, int amount){
13 account->balance -= amount;
14 }
15
16 int main(){
17 store *account = malloc(sizeof(store *));
(gdb)
18 credit(account, 200);
19 debit(account, 300);
20 printf("Balance in the store account = %u\n", account->balance);
21 }
(gdb) n
14 }
(gdb) n
main () at flow-alter.c:20
20 printf("Balance in the store account = %u\n", account->balance);
(gdb) jump 18
Continuing at 0x4005cf.Breakpoint 1, debit (account=0x602010, amount=300) at flow-alter.c:13
13 account->balance -= amount;
(gdb)
We could also send signals to our program manually using ‘signal’ command.
(gdb) help signal
Continue program with the specified signal.
Usage: signal SIGNAL
The SIGNAL argument is processed the same as the handle command.An argument of "0" means continue the program without sending it a signal. This is useful in cases where the program stopped because of a signal, and you want to resume the program while discarding the signal.
(gdb) info signals
Signal Stop Print Pass to program DescriptionSIGHUP Yes Yes Yes Hangup
SIGINT Yes Yes No Interrupt
SIGQUIT Yes Yes Yes Quit
SIGILL Yes Yes Yes Illegal instruction
SIGTRAP Yes Yes No Trace/breakpoint trap
SIGABRT Yes Yes Yes Aborted
...
‘finish’ command exits a function being debugged but executes all the remaining code in function. Sometimes we may wish to exit a function immediately *without* executing the rest of the function. ‘return’ command to our rescue. We may also wish to specify a return value while using the command.
16 int main(){
17 store *account = malloc(sizeof(store *));
18 credit(account, 200);
19 debit(account, 300);
20 printf("Balance in the store account = %u\n", account->balance);
21 }
Breakpoint 1, debit (account=0x602010, amount=300) at flow-alter.c:13
13 account->balance -= amount;
(gdb) return
Make debit return now? (y or n) y
#0 main () at flow-alter.c:20
20 printf("Balance in the store account = %u\n", account->balance);
(gdb) p *account
$3 = {balance = 200}Examining memory areas
typedef struct {
char *name;
char enabled;
int salary;
} datastructure;How many bytes does the datastructure struct occupy on a 64 bit machine?
16? You’re right! The best way to look and understand this is by gdb!
(gdb) x/nfu address
(gdb) x address
Where,
n, f, and u are all optional parameters that specify how much memory to display and how to format it;
address is an expression giving the address where you want to start displaying memory. If you use defaults for nfu, you need not type the slash \.
n, The repeat count is a decimal integer; the default is 1. It specifies how much memory (counting by units u) to display.
f, the display format is one of the formats used by print (‘x’, ‘d’, ‘u’, ‘o’, ‘t’, ‘a’, ‘c’, ‘f’, ‘s’), and in addition ‘i’ (for machine instructions). The default is ‘x’ (hexadecimal) initially.
u, the unit size. The unit size is any of
- b Bytes
- h Halfwords (two bytes)
- w Words (four bytes) (Initial default)
- g Giant words (eight bytes).
Example:
Breakpoint 1, main () at memory.c:11
11 int main(){
(gdb) n
12 char bytearray[6]={0x1,0x2,0x3,0x4,0x5,0x6};
(gdb) x bytearray
0x7fffffffe510: 0x04030201
(gdb) x/6xb bytearray
0x7fffffffe510: 0x01 0x02 0x03 0x04 0x05 0x06
(gdb) x/6db bytearray
0x7fffffffe510: 1 2 3 4 5 6
(gdb) x/6cb bytearray
0x7fffffffe510: 1 '\001' 2 '\002' 3 '\003' 4 '\004' 5 '\005' 6 '\006'
(gdb) x/6ub bytearray
0x7fffffffe510: 1 2 3 4 5 6
(gdb) x/6ob bytearray
0x7fffffffe510: 01 02 03 04 05 06
(gdb) x/6tb bytearray
0x7fffffffe510: 00000001 00000010 00000011 00000100 00000101 00000110
(gdb) x/6ab bytearray
0x7fffffffe510: 0x1 0x2 0x3 0x4 0x5 0x6
(gdb) x/6fb bytearray
0x7fffffffe510: 1 2 3 4 5 6
(gdb) x/6sb bytearray
0x7fffffffe510: "\001\002\003\004\005\006"
0x7fffffffe517: ""
0x7fffffffe518: ""
0x7fffffffe519: "a\247\360\024\352\252q"
0x7fffffffe521: ""
0x7fffffffe522: ""
(gdb) x/6xh bytearray
0x7fffffffe510: 0x0201 0x0403 0x0605 0x0000 0x6100 0xf0a7
(gdb) x/6xw bytearray
0x7fffffffe510: 0x04030201 0x00000605 0xf0a76100 0x71aaea14
0x7fffffffe520: 0x00000000 0x00000000
(gdb) x/6xg bytearray
0x7fffffffe510: 0x0000060504030201 0x71aaea14f0a76100
0x7fffffffe520: 0x0000000000000000 0x00007ffff7a32f45
0x7fffffffe530: 0x0000000000000000 0x00007fffffffe608Examining memory areas can be very helpful to see the contents of a memory region pointed by a pointer. The ability to use examining tool also helps a lot in examining the stack contents and discover potential vulnerabilities. The next article ‘GDB — Advanced’ will demonstrate the practical use of this command by looking at buffer overflow exploit.
Hope that helped you gain some more expertise in using gdb 😊
Please comment if you want me to add any other useful day-to-day commands of gdb for your fellow debuggers.
