GDB Debugging Methods (4) – Debugging Information

The key to whether a program is easy to debug lies in the completeness of the debugging information. This article provides a brief introduction to what DWARF debugging information is based on this premise.

What is DWARF

Debugging With Attributed Record Formats is a debugging information format used by many compilers, which allows for easy source-level debugging of code. The standard documentation for DWARF is as follows:

https://dwarfstd.org/doc/DWARF5.pdf

What Does DWARF Contain

When we compile a program, we can compare the presence of DWARF in ELF by compiling with and without the -g option, as shown below:

gcc example.c -g -o example_with_dwarf
gcc example.c -o example_without_dwarf

At this point, we can compare their sections as follows:

readelf -S example_with_dwarf
readelf -S example_without_dwarf

The comparison is as follows:

GDB Debugging Methods (4) - Debugging Information

As we can see, DWARF actually adds the following sections:

  1. .debug_aranges
  2. .debug_info
  3. .debug_abbrev
  4. .debug_line
  5. .debug_str

The contents of these sections provide auxiliary information for source-level debugging of this binary, mainly including the following:

  1. Function names
  2. Variable calculations
  3. Variable types
  4. Source code lines
  5. Source file paths

Using dwarfdump, we can parse the actual contents of these sections as follows:

# dwarfdump -a example_with_dwarf

.debug_info

COMPILE_UNIT<header overall offset = 0x00000000>:
< 0><0x0000000b>  DW_TAG_compile_unit
                    DW_AT_producer              GNU C17 10.3.0 -mlittle-endian -mabi=lp64 -g -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection
                    DW_AT_language              DW_LANG_C99
                    DW_AT_name                  example.c
                    DW_AT_comp_dir              /root/gdb
                    DW_AT_low_pc                0x00000894
                    DW_AT_high_pc               <offset-from-lowpc>180
                    DW_AT_stmt_list             0x00000000

LOCAL_SYMBOLS:
< 1><0x0000002d>    DW_TAG_base_type
                      DW_AT_byte_size             0x00000008
                      DW_AT_encoding              DW_ATE_unsigned
                      DW_AT_name                  long unsigned int
......

.debug_line: line number info for a single cu
Source lines (from CU-DIE at .debug_info offset 0x0000000b):

            NS new statement, BB new basic block, ET endoftext sequence
            PE prologue end, EB epilogue begin
            IS=val ISA number, DI=val discriminator value
<pc>        [lno,col] NS BB ET PE EB IS= DI= uri: "filepath"
0x00000894  [   9, 1] NS uri: "/root/gdb/example.c"
......

.debug_str
name at offset 0x00000000, length   13is'long long int'
name at offset 0x0000000e, length    4is'main'
name at offset 0x00000013, length   22is'long long unsigned int'
name at offset 0x0000002a, length   13is'unsigned char'
name at offset 0x00000038, length    9is'example.c'
name at offset 0x00000042, length   18is'short unsigned int'
name at offset 0x00000055, length  123is'GNU C17 10.3.0 -mlittle-endian -mabi=lp64 -g -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection'
name at offset 0x000000d1, length    5is'hello'
name at offset 0x000000d7, length    9is'short int'
name at offset 0x000000e1, length    9is'/root/gdb'

.debug_aranges

COMPILE_UNIT<header overall offset = 0x00000000>:
< 0><0x0000000b>  DW_TAG_compile_unit
                    DW_AT_producer              GNU C17 10.3.0 -mlittle-endian -mabi=lp64 -g -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection
                    DW_AT_language              DW_LANG_C99
                    DW_AT_name                  example.c
                    DW_AT_comp_dir              /root/gdb
                    DW_AT_low_pc                0x00000894
                    DW_AT_high_pc               <offset-from-lowpc>180
                    DW_AT_stmt_list             0x00000000


arange starts at 0x00000894, length of0x000000b4, cu_die_offset = 0x0000000b
arange end

.debug_frame

How to Extract DWARF

In Linux distributions, such as the Debian series, the software package with the same name provides a ddeb, which contains the debug information for that program, i.e., the DWARF information file.

Taking example_with_dwarf as an example, we first extract its DWARF debug information as follows:

objcopy --only-keep-debug example_with_dwarf example.debug

Then we remove the debug information from example_with_dwarf as follows:

objcopy --strip-debug example_with_dwarf

At this point, gdb debugging example_with_dwarf has no symbols, as shown below:

# gdb ./example_with_dwarf
Reading symbols from ./example_with_dwarf...
(No debugging symbols found in ./example_with_dwarf)
(gdb) list
No symbol table is loaded.  Use the "file" command.

However, we can manually load the DWARF as follows:

(gdb) add-symbol-file example.debug
add symbol table from file "example.debug"
(y or n) y
Reading symbols from example.debug...
(gdb) list
11          memcpy(p, hello, strlen(hello) + 1);
12          free(p);
13      }
14
15      int sum(int x)
16      {
17          int s = x + y;
18          printf("sum=%d\n", s);
19          mem();
20          return s;

This allows the binary without DWARF information to easily load the separate DWARF file, thus supporting convenient source-level debugging.

Strip

In general, Debian packages will call strip for further trimming, rather than just trimming debug info as shown below:

strip program
objcopy --strip-debug program

This means that the stripped program will naturally lose both <span>.symtab</span> and <span>.strtab</span>. Therefore, when debugging the operating system, it is important to note that stripped binaries are more difficult to debug than binaries that only have debug info removed. The reason is that <span>.symtab</span> and <span>.strtab</span> store the symbol tables for linking and debugging. The explanation is as follows:

If a program only removes debug info, then because <span>.symtab</span> and <span>.strtab</span> exist, breakpoints can still be set because the function names we input can still be found by gdb. For example:

(gdb) l
No symbol table is loaded.  Use the "file" command.
(gdb) b mem
Breakpoint 1 at 0x8a0
(gdb) r
Starting program: /root/gdb/example_with_dwarf
Breakpoint 1, 0x0000aaaaaaaaa8a0 in mem ()

Because the additional information provided by DWARF does not exist, the address is instead based on the runtime address of the code, but the program can still be debugged.

However, for applications stripped by strip, if we run gdb, we may not be able to set breakpoints by name, as shown below:

# strip  example_with_dwarf
# gdb ./example_with_dwarf
(gdb) l
No symbol table is loaded.  Use the "file" command.
(gdb) b main
Function "main" not defined.
Breakpoint 1 (main) pending.
(gdb) b mem
Function "mem" not defined.
Breakpoint 2 (mem) pending.
(gdb) r
Starting program: /root/gdb/example_with_dwarf
sum=3
[Inferior 1 (process 756218) exited normally]

As we can see, we are issuing function names in gdb, but because <span>.symtab</span> and <span>.strtab</span> are missing, it naturally cannot find the corresponding function addresses by name.

How to Find the Entry Point of a Stripped Binary

As mentioned above, if a program cannot find the address by function name after being stripped, how can we locate and debug it? In fact, we can find the function entry point through calculations, as follows:

When the program is stripped, we cannot set a breakpoint at main:

(gdb) b main
Function "main" not defined.

However, every program has an entry point, which is at the entry of the ELF header. In gdb, we can read the actual offset of the entry after loading the program once, as follows:

(gdb) r
Starting program: /root/gdb/example
sum=3
[Inferior 1 (process 830429) exited normally]
(gdb) info files
Symbols from "/root/gdb/example".
Local exec file:
        `/root/gdb/example', file type elf64-littleaarch64.
        Entry point: 0xaaaaaaaaa780

As we can see, the entry of this program is 0xaaaaaaaaa780, so we can set a breakpoint at this address:

(gdb) b *0xaaaaaaaaa780
Breakpoint 1 at 0xaaaaaaaaa780
(gdb) r
Starting program: /root/gdb/example

Breakpoint 1, 0x0000aaaaaaaaa780 in ?? ()

This is the actual function entry point. It is worth noting that this is not the main function, but the <span>_start</span> function, and we can proceed with some hacking operations from here.

How to Debug a Program Without Symbols

As mentioned above, in operating systems, many programs are stripped, making it difficult to debug them simply because function names do not correspond to addresses, and it is challenging to achieve this through manual calculations.

However, a program that is said to be without symbols should be one that was compiled without the -g flag and has not been stripped.

So how do we debug such a program?

This situation is actually quite simple. Based on previous articles, we know that the main reason a program is easy to debug is the loading of DWARF. Therefore, the question is how to debug a function without the help of DWARF information by inferring calculations through registers. Below is a simple practice using a sum function.

The function is as follows:

int sum(int x)
{
    int s = x + y;
    printf("sum=%d\n", s);
    return s;
}

So how do we obtain the values of x, y, and s without the help of DWARF information? We can do this by examining the registers and disassembling the code. We can set a breakpoint at the sum function and then disassemble it as follows:

(gdb) b sum
Breakpoint 1 at 0x8f4
(gdb) r
Starting program: /root/gdb/example

Breakpoint 1, 0x0000aaaaaaaaa8f4 in sum ()
(gdb) disassemble
Dump of assembler code for function sum:
   0x0000aaaaaaaaa8e0 <+0>:     stp     x29, x30, [sp, #-48]!
   0x0000aaaaaaaaa8e4 <+4>:     mov     x29, sp
   0x0000aaaaaaaaa8e8 <+8>:     str     w0, [sp, #28]
   0x0000aaaaaaaaa8ec <+12>:    adrp    x0, 0xaaaaaaabb000
   0x0000aaaaaaaaa8f0 <+16>:    add     x0, x0, #0x18
=> 0x0000aaaaaaaaa8f4 <+20>:    ldr     w0, [x0]
   0x0000aaaaaaaaa8f8 <+24>:    ldr     w1, [sp, #28]
   0x0000aaaaaaaaa8fc <+28>:    add     w0, w1, w0
   0x0000aaaaaaaaa900 <+32>:    str     w0, [sp, #44]
   0x0000aaaaaaaaa904 <+36>:    ldr     w1, [sp, #44]
   0x0000aaaaaaaaa908 <+40>:    adrp    x0, 0xaaaaaaaaa000
   0x0000aaaaaaaaa90c <+44>:    add     x0, x0, #0x9e8
   0x0000aaaaaaaaa910 <+48>:    bl      0xaaaaaaaaa770 <printf@plt>
   0x0000aaaaaaaaa914 <+52>:    ldr     w0, [sp, #44]
   0x0000aaaaaaaaa918 <+56>:    ldp     x29, x30, [sp], #48
   0x0000aaaaaaaaa91c <+60>:    ret
End of assembler dump.

As we can see, by analyzing the assembly code, the value of y should be at 0xaaaaaaabb018, so we can directly read and verify it:

0xaaaaaaabb018 <y>:     0x00000002

While the value of x should be at sp+28, we can read and verify it:

(gdb) x/wx $sp+28
0xfffffffff30c: 0x00000001

At this point, the value of s is stored at sp+44, so we first adjust the stack frame and then print it:

(gdb) b *0x0000aaaaaaaaa904
Breakpoint 2 at 0xaaaaaaaaa904
(gdb) c
Continuing.

Breakpoint 2, 0x0000aaaaaaaaa904 in sum ()
(gdb) x $sp+44
0xfffffffff31c: 0x00000003

Thus, we have completed a simple debugging of a function without symbols. Essentially, it involves disassembling to understand the code and modifying memory. Compared to debugging programs without DWARF information, this is much more tedious.

Conclusion

This article provides a simple introduction based on debugging information, allowing for a clearer understanding of gdb debugging techniques. If a program lacks DWARF information, debugging it requires analyzing the stack and assembly to understand it, which usually only allows for fragmentary logical analysis.However, if a program has DWARF information, using gdb for debugging becomes very convenient, allowing for a comprehensive analysis of code behavior.If a program is stripped, based on personal experience, it is not recommended to force analysis, as even if memory addresses are calculated, access may not be possible, making the investment not worth the return.

Leave a Comment