Linux在运行与位置无关PIC(Position IndependentCode)的用户态程序并加载动态链接库时,函数符号的解析过程将涉及到全局偏移量表GOT(Global Offset Table)与Lazy Binding("懒绑定")。
在位置无关代码PIC中一般不能包含动态链接库中符号的绝对地址。当运行某个调用动态库函数符号的用户态程序时,用户态程序在编译链接阶段并不知晓该符号的具体位置,只有等到运行阶段,动态加载器将所需要的共享库加载到内存后,才最终确定符号的地址。而在编译阶段所有与位置无关的函数调用都将被保存到ELF文件的过程链接表PLT(Procedure Linkage Table)中。
现在通过GDB调试演示Glibc动态链接库中"syscall"函数的动态解析过程。源代码如下:
1 /* syscall_test.cpp-- GOT and Lazy Binding mechanism debugging */
…
16 /* Sandbox needs to be configured with policy by init_sandbox.
17 * New syscall 400 are reserved to config policy, then switch filter on!
18 * but funcode=20 is specially reserved to directly ret -1 for debugging.
19 */
20
21 int init_sandbox()
22 {
… /*policy configuration details ignored*/
51 }
52
53 int main()
54 {
55 init_sandbox();
56 // here ignore the ret value check for init_sandbox
57 // try to see if (400,20) return -1 or not
58 int x = syscall(400,20);
59 cout << "syscall(400,20)" << x << endl;
…
65 return 0;
66 }
首先,反汇编包用户态程序,基本工具为readelf和objdump:
在编译阶段生成的ELF文件中,用于获取实际syscall地址的syscall@plt函数代码只有寥寥3条指令,事实上过程链接表PLT中的每个子函数都只有3条指令。
现在来分析syscall@plt:第1条指令”jmpq *0x20228a(%rip)”会跳转至GOT表中的某个位置。如果此时syscall是第一次被调用,那么GOT表中相应位置保存的是0x00400e06,即第2条指令的位置;如果syccall函数地址之前已经被解析完成,那么GOT表中相应位置保存的便是函数的实际地址,此时syscall@plt的第2,3条指令便不会被执行。这种机制就是所谓的Lazy Binding,即当且仅当动态库中函数符号第一次被调用时才去解析函数的实际位置,然后交由相应过程链接表PLT中的第2,3条指令完成函数实际地址的解析工作。也就是说,动态链接库中的函数符号只有在第一次被