源码级调试C库

来源:互联网 发布:和俄罗斯女人啪啪知乎 编辑:程序博客网 时间:2024/06/07 00:18

在以前的一篇《使用GDB调试C库》中提到过调试C库的问题,一开始的办法是使用ubuntu提供的libc6-dbg来调试,后来觉得这个办法并不完美,所以文章后续给出了使用源码编译glibc的办法,觉得还不够详细,因此这篇文章重新来叙述这个过程,力争详细并且简单明了。

我的系统环境如下:


注意事项:(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运行时动态库搜索路径优先级》我们可以得出,可以通过-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 path=~/lib/lib/tls/i686/sse2/cmov:~/lib/lib/tls/i686/sse2:~/lib/lib/tls/i686/cmov:~/lib/lib/tls/i686:~/lib/lib/tls/sse2/cmov:~/lib/lib/tls/sse2:~/lib/lib/tls/cmov:~/lib/lib/tls:~/lib/lib/i686/sse2/cmov:~/lib/lib/i686/sse2:~/lib/lib/i686/cmov:~/lib/lib/i686:~/lib/lib/sse2/cmov:~/lib/lib/sse2:~/lib/lib/cmov:~/lib/lib          (RPATH from file ./hello)
     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.6
     27568:       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库呢? 

原来通过-Wl传递的参数并不会被扩展(确切的说叫波浪号扩展,那是shell的特性),所有还是使用具体的路径吧!!!

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 path=/home/astrol/lib/lib/tls/i686/sse2/cmov:/home/astrol/lib/lib/tls/i686/sse2:/home/astrol/lib/lib/tls/i686/cmov:/home/astrol/lib/lib/tls/i686:/home/astrol/lib/lib/tls/sse2/cmov:/home/astrol/lib/lib/tls/sse2:/home/astrol/lib/lib/tls/cmov:/home/astrol/lib/lib/tls:/home/astrol/lib/lib/i686/sse2/cmov:/home/astrol/lib/lib/i686/sse2:/home/astrol/lib/lib/i686/cmov:/home/astrol/lib/lib/i686:/home/astrol/lib/lib/sse2/cmov:/home/astrol/lib/lib/sse2:/home/astrol/lib/lib/cmov:/home/astrol/lib/lib         (RPATH from file ./hello)
     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出现的哪些情况了,即行号和源码是一一对应的。


本文结束![2016-9-25 19:10:58]


参考链接:

《Multiple glibc libraries on a single host》

1 0
原创粉丝点击