在《使用GDB调试C库》(http://www.linuxdiyf.com/linux/24479.html)中提到过调试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运行时动态库搜索路径优先级》(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
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
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出现的哪些情况了,即行号和源码是一一对应的。
本文结束!