GDB Debugging Methods (3) – Simple Debugging

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:

  1. Current registers
  2. DWARF debugging information
  3. Function call stack
  4. Variables
  5. Current function stack
  6. Memory content
  7. Thread status
  8. 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.

Leave a Comment