GDB Debugging Method (5) – Debugging Dynamic Libraries

In my personal experience, in most cases, GDB debugging can be used to debug the behavior of a specific dynamic library independently. This allows for convenient problem localization directly on the device where the anomaly occurs. This article introduces how to use GDB to locate issues for a single dynamic library on a production device.

Test Program

Here, we need to prepare two programs: one example.c to serve as the main caller, and another sum.c to implement the sum interface, with the code being sufficiently concise.

The code for sum.c is as follows:

int sum(int x, int y)
{
    int s = x + y;
    return s;
}

The code for example.c is as follows:

#include <stdio.h>
#include "sum.h"

static int y = 2;

int main()
{
    int x = 1;
    printf("sum=%d \n", sum(x, y));
}

Debugging Dynamic Libraries

First, compile the shared object:

gcc sum.c -shared -fPIC -o libsum.so

Then compile the binary:

gcc example.c -lsum -L./ -o example

The above compilations are done without the -g flag. Therefore, GDB can directly see the symbol status of libsum.so as follows:

# export LD_LIBRARY_PATH=./
# gdb ./example
(gdb) info sharedlibrary
From                To                  Syms Read   Shared Object Library
0x0000fffff7fcd0c0  0x0000fffff7fe5468  Yes (*)     /lib/ld-linux-aarch64.so.1
0x0000fffff7fba470  0x0000fffff7fba57c  Yes (*)     ./libsum.so
0x0000fffff7e26880  0x0000fffff7f15db8  Yes         /lib/aarch64-linux-gnu/libc.so.6
                                        No          linux-vdso.so.1

It can be seen that libsum.so has no debugging information.

In this case, if we want to debug the content of the sum function in libsum.so, we can only analyze it through registers and disassembly.

A smarter approach is to enable it to load symbols. The simplest way is to compile libsum.so with symbols, as follows:

gcc sum.c -shared -fPIC -g -o libsum.so

If developing natively on a Debian-based operating system, you can automatically or manually load the .debug_info section, or install the corresponding dbg package. This will not be elaborated here.

Now, check the binary loading status as follows:

(gdb) info sharedlibrary
From                To                  Syms Read   Shared Object Library
0x0000fffff7fcd0c0  0x0000fffff7fe5468  Yes (*)     /lib/ld-linux-aarch64.so.1
0x0000fffff7fba470  0x0000fffff7fba57c  Yes         ./libsum.so
0x0000fffff7e26880  0x0000fffff7f15db8  Yes         /lib/aarch64-linux-gnu/libc.so.6
                                        No          linux-vdso.so.1

It can be seen that libsum.so now contains debugging information and can be debugged directly.

(gdb) b sum
Breakpoint 1 at 0xfffff7fba560: file sum.c, line 3.
(gdb) r
Starting program: /root/gdb/example

Breakpoint 1, sum (x=1, y=2) at sum.c:3
3           int s = x + y;
(gdb) set x=4
(gdb) set y=6
(gdb) c
Continuing.
sum=10
[Inferior 1 (process 994034) exited normally]

The mapping relationship of the dynamic library libsum.so can also be viewed directly in GDB without needing to check /proc, as follows:

(gdb) info proc map
process 1005668
Mapped address spaces:
          Start Addr           End Addr       Size     Offset objfile
      0xaaaaaaaaa000     0xaaaaaaaab000     0x1000        0x0 /root/gdb/example
      0xaaaaaaaba000     0xaaaaaaabb000     0x1000        0x0 /root/gdb/example
      0xaaaaaaabb000     0xaaaaaaabc000     0x1000     0x1000 /root/gdb/example
      0xfffff7e06000     0xfffff7f60000   0x15a000        0x0 /usr/lib/aarch64-linux-gnu/libc-2.31.so
      0xfffff7f60000     0xfffff7f70000    0x10000   0x15a000 /usr/lib/aarch64-linux-gnu/libc-2.31.so
      0xfffff7f70000     0xfffff7f74000     0x4000   0x15a000 /usr/lib/aarch64-linux-gnu/libc-2.31.so
      0xfffff7f74000     0xfffff7f76000     0x2000   0x15e000 /usr/lib/aarch64-linux-gnu/libc-2.31.so
      0xfffff7f76000     0xfffff7f79000     0x3000        0x0
      0xfffff7fba000     0xfffff7fbb000     0x1000        0x0 /root/gdb/libsum.so
      0xfffff7fbb000     0xfffff7fca000     0xf000     0x1000 /root/gdb/libsum.so
      0xfffff7fca000     0xfffff7fcb000     0x1000        0x0 /root/gdb/libsum.so
      0xfffff7fcb000     0xfffff7fcc000     0x1000     0x1000 /root/gdb/libsum.so
      0xfffff7fcc000     0xfffff7fed000    0x21000        0x0 /usr/lib/aarch64-linux-gnu/ld-2.31.so
      0xfffff7ff7000     0xfffff7ffb000     0x4000        0x0
      0xfffff7ffb000     0xfffff7ffc000     0x1000        0x0 [vvar]
      0xfffff7ffc000     0xfffff7ffd000     0x1000        0x0 [vdso]
      0xfffff7ffd000     0xfffff7ffe000     0x1000    0x21000 /usr/lib/aarch64-linux-gnu/ld-2.31.so
      0xfffff7ffe000     0xfffff8000000     0x2000    0x22000 /usr/lib/aarch64-linux-gnu/ld-2.31.so
      0xfffffffdf000    0x1000000000000    0x21000        0x0 [stack]

Conclusion

It can be observed that if one is aware of the previous information regarding DWARF debugging information, debugging a dynamic library becomes exceptionally simple.

In practical work experience, it is impractical to add the -g flag to all code in the released binary, which would be a rather foolish action. The normal approach is to conduct preliminary peripheral analysis based on the symptoms of the problem to draw a rough conclusion about which modules the issue may arise from. Since different modules correspond to different so implementations, it is sufficient to load symbols for the suspected so files, allowing for confident debugging of the so. This technique is simple yet very useful for debugging large projects.

Leave a Comment