CS:APP 你还记得多少?
CS:APP是上个学期读的。花了一个学期的功夫看完了。但是我刚才发现我把它给忘了!
昨天在看巴别塔上的雇工上关于 GNU/Linux 系统上关于运行时链接的一篇How PLT Works。觉得挺对胃口,操起 Emacs 比划了两下。突然有似曾相识的感觉。咦?CS:APP!从书堆里翻出它一看,原来都在里面呢。唉……
不过在自己操练的时候,有一点要注意的。先记下来:在用 gdb 调试 shared object 的时候,为了让 gdb 能不出类似 “Error while mapping shared library sections: libtest.so: sucess” 的错,最好能:
export LD_LIBRARY_PATH=/path/to/so-files:$LD_LIBRARY_PATH
而且新版的 gcc 产生的代码和文中 Red Hat 所带 gcc 产生的代码还颇不同,主要体现在 foo():
(gdb) disass Dump of assembler code for function foo: 0xa7fe3510: push %ebp 0xa7fe3511 : mov %esp,%ebp 0xa7fe3513 : push %ebx 0xa7fe3514 : call 0xa7fe3505 <__i686 .get_pc_thunk.bx> 0xa7fe3519 : add $0x1187,%ebx 0xa7fe351f : sub $0x14,%esp 0xa7fe3522 : mov 0xfffffff0(%ebx),%eax 0xa7fe3528 : mov (%eax),%eax 0xa7fe352a : mov %eax,0x4(%esp) 0xa7fe352e : lea 0xffffef0e(%ebx),%eax 0xa7fe3534 : mov %eax,(%esp) 0xa7fe3537 : call 0xa7fe340c 0xa7fe353c : add $0x14,%esp 0xa7fe353f : xor %eax,%eax 0xa7fe3541 : pop %ebx 0xa7fe3542 : pop %ebp 0xa7fe3543 : ret 0xa7fe3544 : nop
__i686.get_pc_thunk.bx是干什么的?
(gdb) disass 0xa7fe3505 Dump of assembler code for function __i686.get_pc_thunk.bx: 0xa7fe3505 <__i686 .get_pc_thunk.bx+0>: mov (%esp),%ebx 0xa7fe3508 <__i686 .get_pc_thunk.bx+3>: ret 0xa7fe3509 <__i686 .get_pc_thunk.bx+4>: nop
原来上面的函数功能相当简单,就是利用call命令的副作用,把下一个指令的地址压栈,再从栈顶取出,放到%ebx里。接着加上 0x1187 的偏移量,算出 GOT 的位置。下面就是例行公事一般地给 printf 分配 0x14 的栈空间。再根据 xyz 的地址在 GOT 上的偏移量(-0xF)和"OK %d\n"字符串的地址在 GOT 上的偏移量(-0x10F1),分别算出 xyz 的值和格式化字符串的地址,先后压栈。调用 0xa7fe340c。细心的同学可能注意到了,printf@plt。说明 printf 也是在 PLT 上的。的确,printf 是 libc 的一员,a.out 动态地调用它,这样可以省一些存储空间,这和调用 foo 用的方法同出一辙。。
通过这个手段给 %ebx 赋值完毕以后,我们检查一下两个参数:
(gdb) x *($ebx+0xfffffff0) 0x8049710: 0x00000004 (gdb) x/s $ebx+0xffffef0e 0xa7fe35ae <_fini +26>: “OK %d\n”
在 PLT 的 foo 表项中,
(gdb) disass Dump of assembler code for function foo@plt: 0x080483ac: jmp *0x8049700 0x080483b2 : push $0x10 0x080483b7 : jmp 0x804837c <_init +24> End of assembler dump.
和文中导致段错误的 disass 不同,在我这里根本就无法反汇编到 0x804837c 地址的代码:
(gdb) disass 0x804837c No function contains specified address.
难免有遗珠之憾。
本以为不必专门写一个 foo() 函数的,只要 printf 两次就可以探究 GOT 和 PLT 的奥秘。因为 printf 本身就是在 PLT 上的。但是,当我第二次进入 printf 的时候发现,它进入的 printf 实现代码里,没有看到 PIC 函数(即 printf 自己)引用外部数据的代码(即__i686.get_pc_thunk.bx)。另外,在调用 printf 时不能一次加“/n”,另一次不加。因为 gcc 会把加上“\n”的 printf 翻译成 puts@plt,把不加的翻译成真正的 printf@plt,否则两次调用都会写 GOT 中的相应表项。

你编译的时候加-g选项了吗?
不大理解为什么新版本需要__i686.get_pc_thunk.bx,难道为了觉得获得eip值得调用可能很频繁,所以干脆独立出来减少产生的code的size?
Comment by Morgan — July 27, 2006 @ 10:57 am
我加 -g 和不加 -g 都试过,反汇编的结果是一样的。我在 koders 上找到的这个文件 http://www.koders.com/c/fidBCA780D03B02A33E8C9EDC8936D79951760DDE77.aspx
同时包含了两种 PIC INIT 方式,显然后者是 deprecated 的。
这里 http://www.gentoo.org/proj/en/hardened/pic-fix-guide.xml 也说了零星 __i686.get_pc_thunk.bx 的知识,只说它是标准的获得 GOT 的办法,至于用 call-mov-ret 而不用 call-pop ,也没有说明。
另外,我看到 http://gcc.gnu.org/ml/gcc-prs/2003-05/msg02140.html 中提到,GCC 3.4 (时间是2003年,而你的 gdb 的编译时间是2002年)用 -O3 -fPIC 生成的代码里面也有 __i686.get_pc_thunk.bx。我觉得你不妨用 -Os 来编译代码试试看,看看那时的GCC 会不会因为要减小程序大小而使用 __i686.get_pc_thunk.bx。
Comment by Administrator — July 27, 2006 @ 5:55 pm