We know that ptrace frequently issues PTRACE_GETREGS and PTRACE_SETREGS commands, modifying the PC register, allowing the process to be controlled by gdb and execute related commands at the trace point.
This article uses a simple program to demonstrate basic debugging tasks using gdb.
What to Debug
Based on the understanding of ptrace, we can summarize what gdb can retrieve:
- Current registers
- DWARF debugging information
- Function call stack
- Variables
- Current function stack
- Memory content
- Thread status
- Function assembly instructions
Example Code
To implement simple debugging with gdb, here is a basic example code:
#include <stdio.h>
static char hello[] = "hello";
int y = 2;
void mem()
{
void *p = malloc(sizeof(char)*10);
memcpy(p, hello, strlen(hello) + 1);
free(p);
}
int sum(int x)
{
int s = x + y;
printf("sum=%d\n", s);
return s;
}
int main()
{
int x = 1;
sum(x);
mem();
}
Practice
To compile with debugging and DWARF information, simply add the -g option during compilation:
gcc example.c -g -o example
Then you can debug directly with gdb:
# gdb ./example
Setting Breakpoints
(gdb) b sum
Breakpoint 1 at 0x780: file example.c, line 8.
(gdb) r
Starting program: /root/gdb/example
Breakpoint 1, sum (x=1) at example.c:8
8 int s = x + y;
Viewing Registers
Since the parameter of sum is x, we can see that the value of x0 is 1:
(gdb) i r
x0 0x1 1
x1 0xfffffffff498 281474976707736
x2 0xfffffffff4a8 281474976707752
x3 0xaaaaaaaaa7b4 187649984473012
x4 0x0 0
x5 0xc56a615efa85d69 889080589597564265
x6 0xfffff7f87ad8 281474842000088
x7 0x40401000000000004629718009122914304
x8 0xffffffffffffffff-1
x9 0xffff 65535
x10 0x800000008000 140737488388096
x11 0x0 0
x12 0xfffff7e1be48 281474840510024
x13 0x0 0
x14 0x0 0
x15 0x6fffff47 1879048007
x16 0xaaaaaaabafa0 187649984540576
x17 0xfffff7e38c68 281474840628328
x18 0x73516240 1934713408
x19 0xaaaaaaaaa7d8 187649984473048
x20 0x0 0
x21 0xaaaaaaaaa660 187649984472672
x22 0x0 0
x23 0x0 0
x24 0x0 0
x25 0x0 0
x26 0x0 0
x27 0x0 0
x28 0x0 0
x29 0xfffffffff2f0 281474976707312
x30 0xaaaaaaaaa7cc 187649984473036
sp 0xfffffffff2f0 0xfffffffff2f0
pc 0xaaaaaaaaa780 0xaaaaaaaaa780 <sum+12>
cpsr 0x60000000 [ EL=0 C Z ]
fpsr 0x0 0
fpcr 0x0 0
Viewing the Stack
Since we can obtain the registers, we can infer the stack from x30 and x29, and combined with the DWARF information, we can print as follows:
(gdb) bt
#0 sum (x=1) at example.c:8
#1 0x0000aaaaaaaaa7cc in main () at example.c:16
Viewing Source Code
Since the code contains DWARF information, we can view the code lines as follows:
(gdb) l
13 int main()
14 {
15 int x = 1;
16 sum(x);
17 }
18
Viewing Variables
DWARF provides offsets for local variables, as well as fixed loading addresses for global and static variables. Therefore, we can view variables in gdb using DWARF information:
(gdb) p x
$3 = 1
(gdb) p y
$4 = 2
(gdb) p hello
$5 = "hello"
(gdb) p s
$6 = 0
Viewing Memory
Since DWARF helps us calculate the value of p, we can easily print memory through x as follows:
(gdb) u 12
mem () at example.c:12
12 free(p);
(gdb) x/10c p
0xaaaaaaabc6b0: 104 'h' 101 'e' 108 'l' 108 'l' 111 'o' 0 '\000' 0 '\000' 0 '\000'
0xaaaaaaabc6b8: 0 '\000' 0 '\000'
Similarly, we can use dump to directly print a memory area, as long as we calculate the starting and ending addresses of the variable: First, we disassemble the mem function:
(gdb) disassemble/m
Dump of assembler code for function mem:
9 {
0x0000aaaaaaaaa894 <+0>: stp x29, x30, [sp, #-32]!
0x0000aaaaaaaaa898 <+4>: mov x29, sp
10 void *p = malloc(sizeof(char)*10);
=> 0x0000aaaaaaaaa89c <+8>: mov x0, #0xa // #10
0x0000aaaaaaaaa8a0 <+12>: bl 0xaaaaaaaaa720 <malloc@plt>
0x0000aaaaaaaaa8a4 <+16>: str x0, [sp, #24]
11 memcpy(p, hello, strlen(hello) + 1);
0x0000aaaaaaaaa8a8 <+20>: adrp x0, 0xaaaaaaabb000
0x0000aaaaaaaaa8ac <+24>: add x0, x0, #0x10
0x0000aaaaaaaaa8b0 <+28>: bl 0xaaaaaaaaa700 <strlen@plt>
0x0000aaaaaaaaa8b4 <+32>: add x0, x0, #0x1
0x0000aaaaaaaaa8b8 <+36>: mov x2, x0
0x0000aaaaaaaaa8bc <+40>: adrp x0, 0xaaaaaaabb000
0x0000aaaaaaaaa8c0 <+44>: add x1, x0, #0x10
0x0000aaaaaaaaa8c4 <+48>: ldr x0, [sp, #24]
0x0000aaaaaaaaa8c8 <+52>: bl 0xaaaaaaaaa6f0 <memcpy@plt>
12 free(p);
0x0000aaaaaaaaa8cc <+56>: ldr x0, [sp, #24]
0x0000aaaaaaaaa8d0 <+60>: bl 0xaaaaaaaaa760 <free@plt>
13 }
0x0000aaaaaaaaa8d4 <+64>: nop
0x0000aaaaaaaaa8d8 <+68>: ldp x29, x30, [sp], #32
0x0000aaaaaaaaa8dc <+72>: ret
End of assembler dump.
If we want to extract the memory of <span>void* p</span>, we can see that sp+24 stores the address returned by malloc, and the argument passed to malloc is 0xa, as follows:
10 void *p = malloc(sizeof(char)*10);
=> 0x0000aaaaaaaaa89c <+8>: mov x0, #0xa // #10
0x0000aaaaaaaaa8a0 <+12>: bl 0xaaaaaaaaa720 <malloc@plt>
0x0000aaaaaaaaa8a4 <+16>: str x0, [sp, #24]
Then we can parse the malloc return address as follows:
(gdb) x/gx $sp + 24
0xfffffffff2e8: 0x0000aaaaaaabc6b0
So we can directly dump it out as follows:
(gdb) u 12
mem () at example.c:12
12 free(p);
(gdb) dump binary memory test.bin 0x0000aaaaaaabc6b0 0x0000aaaaaaabc6b0+10
At this point, we can see that test.bin contains the memory content we want to view, which is hello:
# hexdump -C test.bin
00000000 68 65 6c 6c 6f 00 00 00 00 00 |hello.....|
0000000a
Printing Types
Printing types is also a very common command. With DWARF, we can automatically resolve the variable types, so we can print directly as follows:
(gdb) ptype y
type = int
Printing Threads
When debugging multithreaded programs, you can switch threads using the threads command and control a specific thread. The command to print threads is as follows:
(gdb) i threads
Id Target Id Frame
* 1 process 392716 "example" mem () at example.c:12
Conclusion
At this point, the basic methods of simple debugging with gdb have been explained. It can be observed that gdb debugging largely relies on DWARF debugging information. If this information is available, debugging becomes very straightforward. If this information is absent, manual calculations are required, and without auxiliary instrumentation techniques or breakpoints, debugging calculations can become quite cumbersome. However, in summary, it is essential to ensure that the binary contains DWARF debugging information during debugging.