专业的编程技术博客社区

网站首页 > 博客文章 正文

[Linux C/C++]如何调试无调试信息的动态链接库及其基本原理

baijin 2024-10-20 04:09:35 博客文章 60 ℃ 0 评论

通过前面的3篇文章,我们了解了GDB调试无调试信息的程序的方法及其原理:

  1. [Linux C/C++]如何正确分离可执行程序及其调试符号
  2. [Linux C/C++]如何对ELF目标文件(Object File)进行瘦身(strip)
  3. [Linux C/C++]如何调试没有调试符号的程序及其基本原理

其实我们还面临另外一个问题,就是如何调试没有调试信息的动态链接库?

事实上,在实际的项目中,我们面临更多的也是要调试没有调试信息的动态链接库,掌握如何调试无调试信息的动态链接库尤为重要。

我们同样以前面的例子为例,不过我们把相关模块编译成动态链接库。整个工程有5个文件组成,其中一个是makefile文件。

[root:~/work/v1/gdb/shared-lib/test2]# tree

.

├── main.c

├── makefile

├── mod1.c

├── mod2.c

└── mod3.c

0 directories, 5 files

[root:~/work/v1/gdb/shared-lib/test2]#

我们首先给出makefile文件的内容:

CFLAGS = -g -Wall -Wextra
LIB_SRCS = mod1.c mod2.c mod3.c
LIB_OBJS = $(patsubst %.c,%.o,$(LIB_SRCS))
TARGET = libfoo.so prog

all:$(TARGET)
$(LIB_OBJS):%.o:%.c
     $(CC) $(CFLAGS) -fPIC -o $@ -c lt;
libfoo.so:$(LIB_OBJS)
     $(CC) $(CFLAGS) -shared -o $@ $^
     cp $@ $(basename $@)_debug.so
     objcopy --only-keep-debug $@ $@.sym
     strip --strip-unneeded $@
prog: main.o libfoo.so
     $(CC) $(CFLAGS) -o $@ $^
     cp $@ $(basename $@).debug
     objcopy --only-keep-debug $@ $@.sym
     strip --strip-all $@
clean:
      $(RM) $(TARGET) $(LIB_OBJS) *.so *.sym *.debug

编译

[root:~/work/v1/gdb/shared-lib/test2]# make

cc -g -Wall -Wextra -fPIC -o mod1.o -c mod1.c

cc -g -Wall -Wextra -fPIC -o mod2.o -c mod2.c

cc -g -Wall -Wextra -fPIC -o mod3.o -c mod3.c

cc -g -Wall -Wextra -shared -o libfoo.so mod1.o mod2.o mod3.o

cp libfoo.so libfoo_debug.so

objcopy --only-keep-debug libfoo.so libfoo.so.sym

strip --strip-unneeded libfoo.so

cc -g -Wall -Wextra -o prog main.o libfoo.so

cp prog prog.debug

objcopy --only-keep-debug prog prog.sym

strip --strip-all prog

[root:~/work/v1/gdb/shared-lib/test2]#

从编译过程中可以看到:

1) libfoo.so由mod1.c, mod2.c和mod3.c等文件编译而成,我们已经对libfoo.so进行了strip,其调试符号被复制到了libfoo.so.sym中。

2) 我们已经对prog进行了strip,其调试符号被复制到了prog.sym中。

3) prog可执行程序依赖于main.o及libfoo.so。

检查prog及libfoo.so

[root:~/work/v1/gdb/shared-lib/test2]# file libfoo.so

libfoo.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=0feded9f2a6780b9caf692ca4adf420305fedc57, stripped

[root:~/work/v1/gdb/shared-lib/test2]# file prog

prog: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=2802a02e4c3b5dff60790251fcdb22376e4421e5, for GNU/Linux 3.2.0, stripped

[root:~/work/v1/gdb/shared-lib/test2]#

prog和libfoo.so都是没有调试信息且被strip过的的目标文件。

调试无调试信息的动态链接库libfoo.so

1) 首先,启动对程序prog的调试

[root:~/work/v1/gdb/shared-lib/test2]# gdb ./prog

Reading symbols from ./prog...

(No debugging symbols found in ./prog)

(gdb) starti

Starting program: /root/work/v1/gdb/shared-lib/test2/prog

Program stopped.

0x00007ffff7fd0100 in _start () from /lib64/ld-linux-x86-64.so.2

(gdb) symbol-file prog.sym

Load new symbol table from "prog.sym"? (y or n) y

Reading symbols from prog.sym...

(gdb) disassemble main

Dump of assembler code for function main:

0x00005555555551a9 <+0>: endbr64

0x00005555555551ad <+4>: push %rbp

0x00005555555551ae <+5>: mov %rsp,%rbp

0x00005555555551b1 <+8>: lea 0xe59(%rip),%rsi # 0x555555556011 <__func__.2318>

0x00005555555551b8 <+15>: lea 0xe45(%rip),%rdi # 0x555555556004

0x00005555555551bf <+22>: mov $0x0,%eax

0x00005555555551c4 <+27>: callq 0x555555555090

0x00005555555551c9 <+32>: mov $0x0,%eax

0x00005555555551ce <+37>: callq 0x555555555080

0x00005555555551d3 <+42>: mov $0x0,%eax

0x00005555555551d8 <+47>: callq 0x5555555550a0

0x00005555555551dd <+52>: mov $0x0,%eax

0x00005555555551e2 <+57>: callq 0x5555555550b0

0x00005555555551e7 <+62>: mov $0x0,%eax

0x00005555555551ec <+67>: pop %rbp

0x00005555555551ed <+68>: retq

End of assembler dump.

(gdb)

2) 添加断点,使程序停止在main函数的入口

(gdb) b main

Breakpoint 1 at 0x5555555551a9: file main.c, line 9.

(gdb) c

Continuing.

Breakpoint 1, main () at main.c:9

9 {

(gdb)

3) 检查prog依赖并已经加载的动态链接库

(gdb) info sharedlibrary

From To Syms Read Shared Object Library

0x00007ffff7fd0100 0x00007ffff7ff2684 Yes /lib64/ld-linux-x86-64.so.2

0x00007ffff7fc5080 0x00007ffff7fc51ab Yes (*) ./libfoo.so

0x00007ffff7dda630 0x00007ffff7f4f4bd Yes /lib/x86_64-linux-gnu/libc.so.6

(*): Shared library is missing debugging information.

(gdb)

其中info sharedlibrary显示的是当前已经被动态链接器加载到进程地址空间的动态链接库,以及它们的.text节映射到进程地址空间的地址范围。例如,对于./libfoo.so,它的.text节映射到进程的地址空间范围为:

0x00007ffff7fc5080 - 0x00007ffff7fc51ab

根据我们在文章[Linux C/C++]如何调试没有调试符号的程序及其基本原理中介绍的GDB映射符号及其地址的原理,我们知道了只要GDB知道目标文件.text节映射到进程地址空间的地址范围,GDB就能正确匹配目标文件中的符号地址。其实这个基本原理对动态链接库来说是相同的。

但是我们前面看到的symbol-file是用于可执行文件的,并不适用于动态链接库,为此GDB提供了另外一个命令: add-symbol-file

(gdb) help add-symbol-file

Load symbols from FILE, assuming FILE has been dynamically loaded.

Usage: add-symbol-file FILE [-readnow | -readnever] [-o OFF] [ADDR] [-s SECT-NAME SECT-ADDR]...

ADDR is the starting address of the file's text.

......

(gdb)

通过add-symbol-file的帮助可以看到,add-symbol-file用于为动态链接的文件加载调试符号,不过需要我们提供一个ADDR参数,而这个参数正是符号文件的.text节在进程虚拟内存空间的起始地址。

4) 通过add-symbol-file加载动态链接库libfoo.so的符号信息

(gdb) list mod1_func

(gdb) disassemble mod1_func

Dump of assembler code for function mod1_func:

0x00007ffff7fc5139 <+0>: endbr64

0x00007ffff7fc513d <+4>: push %rbp

0x00007ffff7fc513e <+5>: mov %rsp,%rbp

0x00007ffff7fc5141 <+8>: mov $0x0,%eax

0x00007ffff7fc5146 <+13>: callq 0x7ffff7fc5070 <mod2_func@plt>

0x00007ffff7fc514b <+18>: mov $0x0,%eax

0x00007ffff7fc5150 <+23>: pop %rbp

0x00007ffff7fc5151 <+24>: retq

End of assembler dump.

(gdb)

通过list mod1_func命令可以看到,由于没有调试信息,所以GDB此时并不能显示mod1_func的源码,同时由于我们并没有移除libfoo.so中的.symtab节,所以依然能够通过符号mod1_func来进行反汇编。

(gdb) add-symbol-file libfoo.so.sym 0x00007ffff7fc5080

add symbol table from file "libfoo.so.sym" at

.text_addr = 0x7ffff7fc5080

(y or n) y

Reading symbols from libfoo.so.sym...

(gdb)

在通过add-symbol-file添加动态链接库的调试信息时,GDB会让我们确认提供的.text节的内存空间地址,如果正确的话,我们回答“y”即可,然后GDB就会把libfoo.so.sym中的调试信息加载到GDB的数据库中,同时帮我们匹配好地址和符号的对应关系。此时我们已经可以对动态链接库进行符号化的调试了。

(gdb) list mod1_func

2 #include <stdio.h>

3

4 int mod2_func();

5

6 int mod1_func()

7 {

8 mod2_func();

9

10 return 0;

11 }

(gdb) disassemble mod1_func

Dump of assembler code for function mod1_func:

0x00007ffff7fc5139 <+0>: endbr64

0x00007ffff7fc513d <+4>: push %rbp

0x00007ffff7fc513e <+5>: mov %rsp,%rbp

0x00007ffff7fc5141 <+8>: mov $0x0,%eax

0x00007ffff7fc5146 <+13>: callq 0x7ffff7fc5070 <mod2_func@plt>

0x00007ffff7fc514b <+18>: mov $0x0,%eax

0x00007ffff7fc5150 <+23>: pop %rbp

0x00007ffff7fc5151 <+24>: retq

End of assembler dump.

(gdb) list *0x00007ffff7fc5139

0x7ffff7fc5139 is in mod1_func (mod1.c:7).

2 #include <stdio.h>

3

4 int mod2_func();

5

6 int mod1_func()

7 {

8 mod2_func();

9

10 return 0;

11 }

(gdb)

5) 对动态链接库libfoo.so进行符号化调试

(gdb) b mod1_func

Breakpoint 2 at 0x7ffff7fc5139: file mod1.c, line 7.

(gdb) c

Continuing.

Enter main...

Breakpoint 2, mod1_func () at mod1.c:7

7 {

(gdb) bt

#0 mod1_func () at mod1.c:7

#1 0x00005555555551d3 in main () at main.c:12

(gdb) b mod1.c:8

Breakpoint 3 at 0x7ffff7fc5141: file mod1.c, line 8.

(gdb) c

Continuing.

Breakpoint 3, mod1_func () at mod1.c:8

8 mod2_func();

(gdb) s

mod2_func () at mod2.c:5

5 {

(gdb) bt

#0 mod2_func () at mod2.c:5

#1 0x00007ffff7fc514b in mod1_func () at mod1.c:8

#2 0x00005555555551d3 in main () at main.c:12

(gdb)

可以看到一切如预期,GDB正确匹配了libfoo.so的符号信息,能够显示libfoo.so中的函数源码,能够指出libfoo.so中函数所在源文件的名称。

GDB加载动态链接库调试符号的基本原理

要了解GDB加载动态链接库调试符号的基本原理,我们首先要认清楚如下的基本事实:

1) 应用程序可能依赖多个的动态链接库。

例如对于一个应用程序,它可能依赖多个动态链接库,如libxxx.so, libyyy.so, libzzz.so等。

2) 进程的虚拟地址空间是一个广阔的地址空间。

例如我们在文章[Linux C/C++]Hello world程序的虚拟地址空间是如何布局的?中介绍的64位进程的地址空间,就单其用户态来说,也是512TB的一个大小,是一个非常巨大的空间。

3) 动态链接库加载到进程的地址空间的位置具有不确定性。

在Linux平台上,动态链接库都必须支持PIC(Position Independent Code, 位置无关代码)。理论上来说,动态链接库是可以加载到进程的任何地址空间的位置上执行的,虽然系统规定了动态链接库加载到虚拟地址空间的一个大致范围,但由于ASLR(Address Space Layout Randomization,地址空间布局随机化)的存在,加载到具体进程的哪段地址空间是不确定的,可能每次运行加载的位置也是变化的。

但是,即便GDB计算错了哪怕一个字节偏移的地址,都会导致GDB在加载符号并映射其地址时出现错误,从而导致调试的失败,这是无法接受的。

GDB如何匹配动态链接库的符号文件到进程的地址空间呢?

对于GDB来说,面对多个动态链接库和动态链接库加载到进程虚拟地址空间的不确定性,就需要我们在提供符号文件的同时还需要提供一个地址,从而告诉GDB来如何正确的把符号文件中的符号映射到进程虚拟地址空间中。

一般来说,每个ELF目标文件都会存在.text节,并且在ELF文件的Header中记录了.text节的虚拟地址,我们假定这个地址是X,只不过这个虚拟地址是由编译器GCC在编译时安排的,当然GCC也安排了ELF文件中每个调试符号的地址。而我们保存的动态链接库的调试文件,正是从具有调试信息的动态链接库文件中复制出来的,而且保留了完整的ELF Header,这个我们在上面的文章中已经介绍过了。

而当这个ELF文件被加载到进程的虚拟地址空间时,加载器(loader)会给.text节安排一个加载地址,我们假定这个地址是Y,通过加载前后.text节的地址,GDB就能得到这个偏移量offset:

offset = Y - X

根据这个偏移量,GDB在加载符号文件中,就能够计算出其中每个符号应该映射到进程地址空间的哪个地址,就能够实现符号和地址的精确匹配,从而实现对动态链接库的符号化调试。

但是,如果我们给了错误的.text节的加载地址,就会导致GDB在匹配符号及其地址时出错,从而无法正确的调试。例如:

(gdb) info sharedlibrary

From To Syms Read Shared Object Library

0x00007ffff7fd0100 0x00007ffff7ff2684 Yes /lib64/ld-linux-x86-64.so.2

0x00007ffff7fc5080 0x00007ffff7fc51ab Yes (*) ./libfoo.so

0x00007ffff7dda630 0x00007ffff7f4f4bd Yes /lib/x86_64-linux-gnu/libc.so.6

(*): Shared library is missing debugging information.

(gdb) disassemble mod1_func

Dump of assembler code for function mod1_func:

0x00007ffff7fc5139 <+0>: endbr64

0x00007ffff7fc513d <+4>: push %rbp

0x00007ffff7fc513e <+5>: mov %rsp,%rbp

0x00007ffff7fc5141 <+8>: mov $0x0,%eax

0x00007ffff7fc5146 <+13>: callq 0x7ffff7fc5070 <mod2_func@plt>

0x00007ffff7fc514b <+18>: mov $0x0,%eax

0x00007ffff7fc5150 <+23>: pop %rbp

0x00007ffff7fc5151 <+24>: retq

End of assembler dump.

假如我们在通过add-symbol-file加载符号文件的地址时给了一个错误的地址值0x00007ffff7fc507F(0x00007ffff7fc5080 - 0x1 = 0x00007ffff7fc507F),也就是有一个字节地址的偏移错误。

(gdb) add-symbol-file libfoo.so.sym 0x00007ffff7fc507F

add symbol table from file "libfoo.so.sym" at

.text_addr = 0x7ffff7fc507f

(y or n) y

Reading symbols from libfoo.so.sym...

(gdb) list mod1_func

2 #include <stdio.h>

3

4 int mod2_func();

5

6 int mod1_func()

7 {

8 mod2_func();

9

10 return 0;

11 }

(gdb) disassemble mod1_func

Dump of assembler code for function mod1_func:

0x00007ffff7fc5138 <+0>: push %rbx

0x00007ffff7fc513a <+2>: nop %edx

0x00007ffff7fc513d <+5>: push %rbp

0x00007ffff7fc513e <+6>: mov %rsp,%rbp

0x00007ffff7fc5141 <+9>: mov $0x0,%eax

0x00007ffff7fc5146 <+14>: callq 0x7ffff7fc5070 <mod2_func@plt>

0x00007ffff7fc514b <+19>: mov $0x0,%eax

0x00007ffff7fc5150 <+24>: pop %rbp

End of assembler dump.

(gdb)

由于我们提供.text节的地址错误,导致GDB计算得到mod1_func应该匹配到的地址为0x00007ffff7fc5138,其实这是错误的,这些反汇编代码并不是mod1_func的反汇编代码。mod1_func正确的反汇编代码如下:

[root:~/work/v1/gdb/shared-lib/test2]# objdump --disassemble=mod1_func libfoo_debug.so

libfoo_debug.so: file format elf64-x86-64

Disassembly of section .init:

Disassembly of section .plt:

Disassembly of section .plt.got:

Disassembly of section .plt.sec:

Disassembly of section .text:

0000000000001139 <mod1_func>:

1139: f3 0f 1e fa endbr64

113d: 55 push %rbp

113e: 48 89 e5 mov %rsp,%rbp

1141: b8 00 00 00 00 mov $0x0,%eax

1146: e8 25 ff ff ff callq 1070 <mod2_func@plt>

114b: b8 00 00 00 00 mov $0x0,%eax

1150: 5d pop %rbp

1151: c3 retq

Disassembly of section .fini:

[root:~/work/v1/gdb/shared-lib/test2]#

这个错误产生的过程如下:

1) 首先得到.text节在ELF文件中的虚拟地址: 0x0000000000001080

[root:~/work/v1/gdb/shared-lib/test2]# readelf -S libfoo.so.sym

There are 35 section headers, starting at offset 0x1e88:

...

[14] .text NOBITS 0000000000001080 00001000

000000000000012b 0000000000000000 AX 0 0 16

...

2) 其次,得到mod1_func在调试文件中的虚拟地址:0x0000000000001139

[root:~/work/v1/gdb/shared-lib/test2]# readelf -s libfoo.so.sym

Symbol table '.symtab' contains 63 entries:

Num: Value Size Type Bind Vis Ndx Name

0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND

...

56: 0000000000001139 25 FUNC GLOBAL DEFAULT 14 mod1_func

...

3) 计算得到offset

offset = 0x00007ffff7fc507F - 0x0000000000001080 = 0x00007ffff7fc3fff

4) GDB计算得到mod1_func符号对应到进程地址空间中的虚拟地址

mod1_func的地址 = 0x0000000000001139 + 0x00007ffff7fc3fff = 0x00007ffff7fc5138

这个错误就是这样产生的,这可能将使调试过程无法理解,让我们无比沮丧和失望。做为程序员,我们必须避免。


附录: GDB的add-symbol-file

最后我们在对GDB的add-symbol-file做一个总结:

add-symbol-file是的GDB的一个命令,用于在调试程序时加载动态链接库的符号文件。它允许将符号表文件与正在调试的二进制文件相关联,以便在调试过程中能够访问函数名称、变量名称等符号信息。

在GDB中使用add-symbol-file命令,需要提供两个参数:

第一个参数是符号表文件的路径,通常具有`.sym`或`.debug`的扩展名。

第二个参数是符号表文件对应的二进制目标文件的.text节的加载地址。该地址告诉调试器将符号信息应用于二进制目标文件的特定内存位置。

当执行add-symbol-file命令时,GDB会将符号信息加载到调试器的内部数据库中,以便在调试会话中使用。加载符号表后,GDB可以将函数名称和变量名称替代地址显示给用户,这对于理解和跟踪程序的执行非常有帮助。

需要注意的是,add-symbol-file只是加载符号表信息,而不会加载代码或数据。因此,在使用该命令之前,二进制文件本身已经被加载到调试器中。

总而言之,add-symbol-file是通过加载符号表文件与二进制文件相关联,使得GDB能够在调试过程中识别函数名称和变量名称。这样,开发者可以更方便地调试和理解程序的行为。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表