红联Linux门户
Linux帮助

源码级调试C库

发布时间:2016-09-26 09:03:58来源:linux网站作者:astrotycoon
在《使用GDB调试C库》(http://www.linuxdiyf.com/linux/24479.html)中提到过调试C库的问题,一开始的办法是使用ubuntu提供的libc6-dbg来调试,后来觉得这个办法并不完美,所以文章后续给出了使用源码编译glibc的办法,觉得还不够详细,因此这篇文章重新来叙述这个过程,力争详细并且简单明了。
 
我的系统环境如下:
源码级调试C库
注意事项:
(1)确保系统剩余磁盘不小于3个G,你不会想到编译调试版本的C库需要这么大的磁盘空间。
(2)确保很多工具已经安装,例如安装过程中提示我需要gawk,则sudo apt-get install gawk安装即可。
 
第一步:下载源码并解压
astrol@astrol:~$ wget -c http://ftp.gnu.org/gnu/glibc/glibc-2.23.tar.gz
astrol@astrol:~$ tar zxf glibc-2.23.tar.gz
 
第二步:安装准备工作以及configure
在目录/glibc-2.23下的INSTALL安装说明中提到,C库不能在源码目录下直接编译安装,所以我在home目录下新建立了一个目录用于编译C库,目录为~/libc。又建立了一个~/lib目录用于最后的C库安装目录。
astrol@astrol:~$ mkdir libc lib
astrol@astrol:~$ cd libc
astrol@astrol:~/libc$ ../glibc-2.23/configure --prefix=/home/astrol/lib CFLAGS="-O1 -g3 -ggdb" CXXFLAGS="-O1 -g3 -ggdb" --disable-werror
注意,我为了调试,所以加了-g3 -ggdb调试选项,-Ox是必须得,因为C库必须要指定,还有最后的--disable-werror也是必须得,否则会将编译过程中的很多警告信息归为错误,那么就没法继续编译了。这里我只是根据我自身的要求加的几个选项,你也可以根据自己的需求自行添加,参考../glibc-2.23/configure --help的提示帮助。
 
第三步:编译源码
astrol@astrol:~/libc$ make
编译的过程是很漫长的,也是最容易出错的,good luck!!!
 
第四步:安装编译好的C库
到这里,恭喜你编译成功过了。
astrol@astrol:~/libc$ du -sh
3.1G    .
看到没,足足有3个多G,可怕!
最后make install,就将编译好的库安装到我指定的~/lib中。
astrol@astrol:~/libc$ make install
进入~/lib,ls,咦,怎么没有生成的库呢,仔细一看,原来所有的库都在子目录lib下,啊,生成的库还真多。
astrol@astrol:~$ cd lib
astrol@astrol:~/lib$ ls
bin  etc  include  lib  libexec  sbin  share  var
astrol@astrol:~/lib$ cd lib
astrol@astrol:~/lib/lib$ ls
audit                    libBrokenLocale.so.1  libdl.so               libnss_compat.so       libnss_nisplus-2.23.so  librpcsvc.a
crt1.o                   libc-2.23.so          libdl.so.2             libnss_compat.so.2     libnss_nisplus.so       librt-2.23.so
crti.o                   libc.a                libg.a                 libnss_db-2.23.so      libnss_nisplus.so.2     librt.a
crtn.o                   libcidn-2.23.so       libieee.a              libnss_db.so           libnss_nis.so           librt.so
gconv                    libcidn.so            libm-2.23.so           libnss_db.so.2         libnss_nis.so.2         librt.so.1
gcrt1.o                  libcidn.so.1          libm.a                 libnss_dns-2.23.so     libpcprofile.so         libSegFault.so
ld-2.23.so               libc_nonshared.a      libmcheck.a            libnss_dns.so          libpthread-2.23.so      libthread_db-1.0.so
ld-linux.so.2            libcrypt-2.23.so      libmemusage.so         libnss_dns.so.2        libpthread.a            libthread_db.so
libanl-2.23.so           libcrypt.a            libm.so                libnss_files-2.23.so   libpthread_nonshared.a  libthread_db.so.1
libanl.a                 libcrypt.so           libm.so.6              libnss_files.so        libpthread.so           libutil-2.23.so
libanl.so                libcrypt.so.1         libnsl-2.23.so         libnss_files.so.2      libpthread.so.0        libutil.a
libanl.so.1              libc.so               libnsl.a               libnss_hesiod-2.23.so  libresolv-2.23.so       libutil.so
libBrokenLocale-2.23.so  libc.so.6             libnsl.so              libnss_hesiod.so       libresolv.a             libutil.so.1
libBrokenLocale.a        libdl-2.23.so         libnsl.so.1            libnss_hesiod.so.2     libresolv.so            Mcrt1.o
libBrokenLocale.so       libdl.a               libnss_compat-2.23.so  libnss_nis-2.23.so     libresolv.so.2          Scrt1.o
 
第五步:如何使用编译好的C库呢
现在C库的编译和安装都彻底完成了。接下来就是如何使用编译好的C库,并且GDB调试了。
其实,接下来的问题可以这样描述:系统中存在多版本的C库时,如何使我们的应用程序选择使用哪一个C库呢?
我们这里存在两个版本的C库,一个是系统原生的C库,不带调试符号信息,另一个就是我们刚刚编译好的C库了,拥有详细的调试信息。
我们使用经典的hello world程序来做测试。
astrol@astrol:~$ mkdir libc_test
astrol@astrol:~$ cd libc_test/
astrol@astrol:~/libc_test$ vim hello.c
astrol@astrol:~/libc_test$ gcc -o hello hello.c
astrol@astrol:~/libc_test$ ./hello &
hello world
测试源码如下:
#include <stdio.h>  
#include <unistd.h>  
int main(int argc, char *argv[])  
{  
printf("hello world\n");  
while (1) {  
sleep(1);  
}  
return (0);  
}  
OK,一切正常。但是生成的hello可执行程序默认使用的C库和链接器都是系统原生的版本,怎么验证?如下:
astrol@astrol:~/libc_test$ ldd hello
linux-gate.so.1 =>  (0xb77e0000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7611000)
/lib/ld-linux.so.2 (0x800ab000)
astrol@astrol:~/libc_test$ readelf --program-headers hello
Elf file type is EXEC (Executable file)
Entry point 0x8048310
There are 9 program headers, starting at offset 52
Program Headers:
Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
PHDR           0x000034 0x08048034 0x08048034 0x00120 0x00120 R E 0x4
INTERP         0x000154 0x08048154 0x08048154 0x00013 0x00013 R   0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD           0x000000 0x08048000 0x08048000 0x005c4 0x005c4 R E 0x1000
LOAD           0x000f08 0x08049f08 0x08049f08 0x00114 0x00118 RW  0x1000
DYNAMIC        0x000f14 0x08049f14 0x08049f14 0x000e8 0x000e8 RW  0x4
... ...
可以很清楚看到,当hello程序运行时加载的C库和链接器使用的都是系统原生的,也可以通过proc来查看
astrol@astrol:~/libc_test$ ps aux | grep hello
astrol     694  0.0  0.1   2200   532 pts/17   S    17:17   0:00 ./hello
astrol    6549  7.0  0.1   6848   764 pts/17   S+   17:26   0:00 grep --color=auto hello
astrol@astrol:~/libc_test$ cat /proc/694/maps
08048000-08049000 r-xp 00000000 08:01 17         /home/astrol/libc_test/hello
08049000-0804a000 r--p 00000000 08:01 17         /home/astrol/libc_test/hello
0804a000-0804b000 rw-p 00001000 08:01 17         /home/astrol/libc_test/hello
08ec7000-08ee8000 rw-p 00000000 00:00 0          [heap]
b75fa000-b77a9000 r-xp 00000000 08:01 2622768    /lib/i386-linux-gnu/libc-2.23.so
b77a9000-b77aa000 ---p 001af000 08:01 2622768    /lib/i386-linux-gnu/libc-2.23.so
b77aa000-b77ac000 r--p 001af000 08:01 2622768    /lib/i386-linux-gnu/libc-2.23.so
b77ac000-b77ad000 rw-p 001b1000 08:01 2622768    /lib/i386-linux-gnu/libc-2.23.so
b77ad000-b77b0000 rw-p 00000000 00:00 0
b77c5000-b77c7000 rw-p 00000000 00:00 0
b77c7000-b77c9000 r--p 00000000 00:00 0          [vvar]
b77c9000-b77ca000 r-xp 00000000 00:00 0          [vdso]
b77ca000-b77ec000 r-xp 00000000 08:01 2622740    /lib/i386-linux-gnu/ld-2.23.so
b77ec000-b77ed000 rw-p 00000000 00:00 0
b77ed000-b77ee000 r--p 00022000 08:01 2622740    /lib/i386-linux-gnu/ld-2.23.so
b77ee000-b77ef000 rw-p 00023000 08:01 2622740    /lib/i386-linux-gnu/ld-2.23.so
bfc56000-bfc77000 rw-p 00000000 00:00 0          [stack]
该如何做,才能使生成的hello程序在运行时加载的C库和链接器是我们生成的呢?好,重点来了!
根据文章《 Linux运行时动态库搜索路径优先级》(http://www.linuxdiyf.com/linux/24481.html)我们可以得出,可以通过-Wl,-rpath来加载我们编译出的C库,因为通过-Wl,-rpath指定的路径优先级最高。
astrol@astrol:~/libc_test$ gcc -o hello hello.c -Wl,-rpath=~/lib/lib
astrol@astrol:~/libc_test$ readelf --dynamic hello
Dynamic section at offset 0xf0c contains 25 entries:
Tag        Type                         Name/Value
0x00000001 (NEEDED)                     Shared library: [libc.so.6]
0x0000000f (RPATH)                      Library rpath: [~/lib/lib]
0x0000000c (INIT)                       0x80482d4
... ...
可以看到,~/lib/lib是被写入最终的可执行文件的。我们用环境变量LD_DEBUG来看下具体的搜索C库的顺序:
astrol@astrol:~/libc_test$ LD_DEBUG=libs ./hello
27568:     find library=libc.so.6 [0]; searching
27568:      search 
源码级调试C库
27568:       trying file=~/lib/lib/tls/i686/sse2/cmov/libc.so.6
27568:       trying file=~/lib/lib/tls/i686/sse2/libc.so.6
27568:       trying file=~/lib/lib/tls/i686/cmov/libc.so.6
27568:       trying file=~/lib/lib/tls/i686/libc.so.6
27568:       trying file=~/lib/lib/tls/sse2/cmov/libc.so.6
27568:       trying file=~/lib/lib/tls/sse2/libc.so.6
27568:       trying file=~/lib/lib/tls/cmov/libc.so.6
27568:       trying file=~/lib/lib/tls/libc.so.6
27568:       trying file=~/lib/lib/i686/sse2/cmov/libc.so.6
27568:       trying file=~/lib/lib/i686/sse2/libc.so.6
27568:       trying file=~/lib/lib/i686/cmov/libc.so.6
27568:       trying file=~/lib/lib/i686/libc.so.6
27568:       trying file=~/lib/lib/sse2/cmov/libc.so.6
27568:       trying file=~/lib/lib/sse2/libc.so.62
7568:       trying file=~/lib/lib/cmov/libc.so.6
27568:       trying file=~/lib/lib/libc.so.6
27568:      search cache=/etc/ld.so.cache
27568:       trying file=/lib/i386-linux-gnu/libc.so.6
27568:
27568:
27568:     calling init: /lib/i386-linux-gnu/libc.so.6
27568:
27568:
27568:     initialize program: ./hello
27568:
27568:
27568:     transferring control: ./hello
27568:
可以很清楚的看到,系统先从~/lib/lib中搜索C库,接着是 配置文件/etc/ld.so.conf中指定的动态库搜索路径,最后是系统默认搜索路径。咦,不对,怎么最后还是使用的系统C库呢? 
具体原因我不得而知,反正是没有对~做扩展,那就换成具体的路径吧!
astrol@astrol:~/libc_test$ gcc -o hello hello.c -Wl,-rpath=/home/astrol/lib/lib
astrol@astrol:~/libc_test$ LD_DEBUG=libs ./hello
27582:     find library=libc.so.6 [0]; searching
27582:      search
源码级调试C库
27582:       trying file=/home/astrol/lib/lib/tls/i686/sse2/cmov/libc.so.6
27582:       trying file=/home/astrol/lib/lib/tls/i686/sse2/libc.so.6
27582:       trying file=/home/astrol/lib/lib/tls/i686/cmov/libc.so.6
27582:       trying file=/home/astrol/lib/lib/tls/i686/libc.so.6
27582:       trying file=/home/astrol/lib/lib/tls/sse2/cmov/libc.so.6
27582:       trying file=/home/astrol/lib/lib/tls/sse2/libc.so.6
27582:       trying file=/home/astrol/lib/lib/tls/cmov/libc.so.6
27582:       trying file=/home/astrol/lib/lib/tls/libc.so.6
27582:       trying file=/home/astrol/lib/lib/i686/sse2/cmov/libc.so.6
27582:       trying file=/home/astrol/lib/lib/i686/sse2/libc.so.6
27582:       trying file=/home/astrol/lib/lib/i686/cmov/libc.so.6
27582:       trying file=/home/astrol/lib/lib/i686/libc.so.6
27582:       trying file=/home/astrol/lib/lib/sse2/cmov/libc.so.6
27582:       trying file=/home/astrol/lib/lib/sse2/libc.so.6
27582:       trying file=/home/astrol/lib/lib/cmov/libc.so.6
27582:       trying file=/home/astrol/lib/lib/libc.so.6
27582:
27582:
27582:     calling init: /home/astrol/lib/lib/libc.so.6
27582:
27582:
27582:     initialize program: ./hello
27582:
27582:
27582:     transferring control: ./hello
终于可以成功加载我自己的C库了!那么链接器呢,如何做?简单,通过链接选项-Wl,--dynamic-linker=/home/astrol/lib/lib/ld-2.23.so就可以了!这样做的最终效果就是把程序运行时的动态链接器告诉系统,让它加载调用!
astrol@astrol:~/libc_test$ gcc -o hello hello.c -Wl,-rpath=/home/astrol/lib/lib -Wl,--dynamic-linker=/home/astrol/lib/lib/ld-2.23.so
astrol@astrol:~/libc_test$ readelf --program-headers hello
Elf file type is EXEC (Executable file)
Entry point 0x8048360
There are 9 program headers, starting at offset 52
Program Headers:
Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
PHDR           0x000034 0x08048034 0x08048034 0x00120 0x00120 R E 0x4
INTERP         0x000154 0x08048154 0x08048154 0x00020 0x00020 R   0x1
[Requesting program interpreter: /home/astrol/lib/lib/ld-2.23.so]
LOAD           0x000000 0x08048000 0x08048000 0x0060c 0x0060c R E 0x1000
LOAD           0x000f00 0x08049f00 0x08049f00 0x00120 0x00124 RW  0x1000
DYNAMIC        0x000f0c 0x08049f0c 0x08049f0c 0x000f0 0x000f0 RW  0x4
NOTE           0x000174 0x08048174 0x08048174 0x00044 0x00044 R   0x4
GNU_EH_FRAME   0x00051c 0x0804851c 0x0804851c 0x0002c 0x0002c R   0x4
GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x10
GNU_RELRO      0x000f00 0x08049f00 0x08049f00 0x00100 0x00100 R   0x1
运行程序,通过maps文件验证下:
astrol@astrol:~/libc_test$ ./hello &
[1] 27645
astrol@astrol:~/libc_test$ hello world
astrol@astrol:~/libc_test$ ps aux | grep hello
astrol   27645  0.2  0.0   2092   156 pts/17   S    18:38   0:00 ./hello
astrol   27647  0.0  0.1   6848   852 pts/17   S+   18:38   0:00 grep --color=auto hello
astrol@astrol:~/libc_test$ cat /proc/27645/maps
08048000-08049000 r-xp 00000000 08:01 17         /home/astrol/libc_test/hello
08049000-0804a000 r--p 00000000 08:01 17         /home/astrol/libc_test/hello
0804a000-0804b000 rw-p 00001000 08:01 17         /home/astrol/libc_test/hello
087f3000-08814000 rw-p 00000000 00:00 0          [heap]
b75b0000-b75b1000 rw-p 00000000 00:00 0
b75b1000-b7748000 r-xp 00000000 08:01 1181945    /home/astrol/lib/lib/libc-2.23.so
b7748000-b7749000 ---p 00197000 08:01 1181945    /home/astrol/lib/lib/libc-2.23.so
b7749000-b774b000 r--p 00197000 08:01 1181945    /home/astrol/lib/lib/libc-2.23.so
b774b000-b774c000 rw-p 00199000 08:01 1181945    /home/astrol/lib/lib/libc-2.23.so
b774c000-b7750000 rw-p 00000000 00:00 0
b7750000-b7752000 r--p 00000000 00:00 0          [vvar]
b7752000-b7753000 r-xp 00000000 00:00 0          [vdso]
b7753000-b7773000 r-xp 00000000 08:01 1184346    /home/astrol/lib/lib/ld-2.23.so
b7773000-b7774000 r--p 0001f000 08:01 1184346    /home/astrol/lib/lib/ld-2.23.so
b7774000-b7775000 rw-p 00020000 08:01 1184346    /home/astrol/lib/lib/ld-2.23.so
bfd66000-bfd87000 rw-p 00000000 00:00 0          [stack]
 
第六步:使用GDB调试
现在使用gdb调试我们的hello。gdb hello -q进入调试。使用set verbose on打开gdb信息打印,可以更好的看到调试信息。
astrol@astrol:~/libc_test$ gcc -g3 -ggdb -o hello hello.c -Wl,-rpath=/home/astrol/lib/lib -Wl,--dynamic-linker=/home/astrol/lib/lib/ld-2.23.so
astrol@astrol:~/libc_test$ gdb hello -q
Reading symbols from hello...done.
(gdb) set verbose on
(gdb) start
Temporary breakpoint 1 at 0x804846c: file hello.c, line 6.
Starting program: /home/astrol/libc_test/hello
Reading symbols from /home/astrol/lib/lib/ld-2.23.so...done.
Reading symbols from system-supplied DSO at 0xb7fdd000...(no debugging symbols found)...done.
Reading in symbols for dl-debug.c...done.
Reading in symbols for rtld.c...done.
Reading symbols from /home/astrol/lib/lib/libc.so.6...done.
Temporary breakpoint 1, main (Reading in symbols for ../sysdeps/x86/libc-start.c...done.
argc=1, argv=0xbffff5f4) at hello.c:6
6               printf("hello world\n");
(gdb)
gdb成功加载了两个库和它们的符号信息。那么接下来的调试就能很好的继续了。这里我演示下printf的工作过程,观察下PLT的大致工作过程。
(gdb) disassemble /m
Dump of assembler code for function main:
5       {
0x0804845b <+0>:     lea    0x4(%esp),%ecx
0x0804845f <+4>:     and    $0xfffffff0,%esp
0x08048462 <+7>:     pushl  -0x4(%ecx)
0x08048465 <+10>:    push   %ebp
0x08048466 <+11>:    mov    %esp,%ebp
0x08048468 <+13>:    push   %ecx
0x08048469 <+14>:    sub    $0x4,%esp
6               printf("hello world\n");
=> 0x0804846c <+17>:    sub    $0xc,%esp
0x0804846f <+20>:    push   $0x8048510
0x08048474 <+25>:    call   0x8048330 <puts@plt>
0x08048479 <+30>:    add    $0x10,%esp
7
8               while (1) {
9                       sleep(1);
0x0804847c <+33>:    sub    $0xc,%esp
0x0804847f <+36>:    push   $0x1
0x08048481 <+38>:    call   0x8048320 <sleep@plt>
0x08048486 <+43>:    add    $0x10,%esp
10              }
0x08048489 <+46>:    jmp    0x804847c <main+33>
End of assembler dump.
(gdb)
地址0x8048330就是puts的PLT入口处。stepi跟踪进去!
(gdb) stepi
0x08048330 in puts@plt ()
(gdb) disassemble /m
Dump of assembler code for function puts@plt:
=> 0x08048330 <+0>:     jmp    *0x804a010
0x08048336 <+6>:     push   $0x8
0x0804833b <+11>:    jmp    0x8048310
End of assembler dump.
(gdb)
继续跟进,最后jmp到0x8048310,可以通过x命令看到0x8048310处的指令如下:
(gdb) x/3i 0x8048310
0x8048310:   pushl  0x804a004
0x8048316:   jmp    *0x804a008
0x804831c:   add    %al,(%eax)
继续jmp到*0x804a008,这就是_dl_runtime_resolve函数的地址,它是最终进入_dl_fixup函数的“跳板”。继续跟进,看最后进入_dl_fixup函数后效果如何。
最终进入_dl_fixup函数后,发现是很正常的,gdb能很好的进行源码级调试,不会出现Ubuntu提供的/usr/lib/debug出现的哪些情况了,即行号和源码是一一对应的。
 
本文结束!
 
本文永久更新地址:http://www.linuxdiyf.com/linux/24480.html