linux动态库的PIC笔记
日期:2014-09-28 16:01:21
最后更新日期:2014-10-01 23:57:13
最后更新日期:2014-09-28
环境:
[code lang="cpp"]
发行版:CentOS release 6.2 (Final)
内核:2.6.32-220.el6.x86_64
gcc (GCC) 4.4.7 20120313 (Red Hat 4.4.7-4)
[/code]
写在前面:
动态库这种方式,笔者认为不一定适合于目前的服务器端程序部署。为什么呢?早期计算机系统,内存紧张,对比静态库,有了动态库作为解决方案。当现在的计算机系统而言,库所占用的内存其实并不多,反而是程序的在七七八八的机器运行环境导致容易出现兼容问题。所以现在有了很多“统一环境”的部署方案,比如系统虚拟化技术,使用相同的系统镜像,运行于虚拟机或实体机上,或者使用-容器技术,比如去年开源很火的docker程序,相比前一种系统虚拟化,显得更轻量化。日常工作中,公司环境没有上诉两种环境-系统虚拟化或容器技术,实现生产编译部署时,笔者倾向于静态编译。学习的目的,不在于拘泥于它,所以这里继续讨论动态的PIC(position indepent code)技术。 写在前面的后面:
阅读有关动态库的PIC的文献,由于笔者调试一个printf函数。没错,就是printf函数。如果后面有机缘,笔者还会继续讲这个程序。这个程序如下:
[code lang="cpp"]
//test.c
//gcc -o test test.c
#include<stdio.h>
int main()
{
printf("no\n");
printf("yes\n");
return 0;
}
[/code]
gdb这个程序时,在笔者文章开头所诉的环境下,gdb会有一个问题,若调用layout asm显示汇编的窗口,设置main函数断点,不停的si单步进入汇编函数,函数重定向时会出现:
[code lang="cpp"]
(gdb) layout asm
(gdb) b main
(gdb) run
(gdb) si //这里一直输入si,回车
0x00000000004003c3 in puts@plt ()
(gdb) si
No function contains program counter for selected frame.
[/code]
这个问题,没啥头绪,感觉程序的用户空间,应该都是随你来折腾。今天早上,无意中发现两种解决办法:
1.另外一个系统的,高版本的gcc,gdb没有遇到这个问题,可以一直追踪进去,看函数怎么重定位的。该系统的gcc版本为:gcc (Ubuntu/Linaro 4.8.1-10ubuntu9) 4.8.1
2.在这个系统上,只要不调用layout asm显示汇编窗口,还是可以一直追着进去的。
只能这样猜测,这个版本的gdb有bug,在高版本的gdb,这个bug被修复了。记得大学,也遇到过一次类似的问题。所以遇到这样问题的伙伴,不要惊讶了。
正文:
本文写的不系统,只是笔者的理解而已。可能还是看参考1,比较系统详细。x86架构下,linux的用户态的程序,调用一个函数,转化为汇编指令就是一个call语句,程序的cs代码段寄存器只有一个,那么call这个函数,就是call这个函数的地址。对于动态库而言,就是相对地址。怎么确定这个相对地址?根本就是参考1重点指出的两个key point。
[code lang="cpp"]
Key insight #1 - offset between text and data sections
Key insight #2 - making an IP-relative offset work on x86
[/code]
数据或函数地址,都是用相对偏移来计算出绝对地址。数据在数据段内的相对偏移和函数地址在代码段内的相对偏移,再利用上当前PC寄存器的值,这样来计算最终地址。当前PC寄存器的值,怎么放到通用寄存器里面呢?参考1该文所诉到的方法,借助一个临时跳转:
[code lang="cpp"]
call TMPLABEL
TMPLABEL:
pop ebx
[/code]
call指令,会把下一条指令(pop ebx)的PC值push到栈上,后面的指令再pop到ebx。这样ebx就有了TMPLABEL这个地址。
a.怎么找到共享库里面的数据? 应用程序内有一张GOT(Global Offset Table )表,记录了某变量的地址,该地址即为动态库映射后的地址。链接器链接的时候,知道某个变量在共享库的GOT表的index,然后GOT表首地址加上index个表项的偏移就是该变量的地址。赋值的汇编指令伪码如下:
[code lang="cpp"]
mov edx, [ADDR_OF_VAR]
[/code]
可以转变为:
[code lang="cpp"]
; 1. Somehow get the address of the GOT into ebx
lea ebx, ADDR_OF_GOT
; 2. Suppose ADDR_OF_VAR is stored at offset 0x10
; in the GOT. Then this will place ADDR_OF_VAR
; into edx.
mov edx, DWORD PTR [ebx + 0x10]
; 3. Finally, access the variable and place its
; value into edx.
mov edx, DWORD PTR [edx]
[/code]
b.怎么找到共享库的函数地址? 函数地址也可以像数据那样,直接在GOT表里面存放函数映射后的地址,然后类似于取数据那样,计算函数的地址。但是共享库使用了一种延迟banding的技术。当真正要用到它的时候,才计算该地址。算是一种性能优化吧。延迟binding的实现基于加了一个PLT(The Procedure Linkage Table)表。调用某个函数时,首先jmp到PLT表的index表项位置,index的值链接时,可以确定。该表项位置一般类似这样几条指令:
[code lang="cpp"]
jmp *(GOT+index)
push resolve index
jmp resolver
[/code]
第一次,调用该函数时,GOT+index存放的地址并不是真正地址,而是jmp过来的下一条地址即push resolve index指令。接下来的过程,会把函数地址解析出来,并存放到GOT+index位置,这样下一次调用时,就直接jmp过去,而不需要重新resolve了。
参考一:
1.http://eli.thegreenplace.net/2011/11/03/position-independent-code-pic-in-shared-libraries/