原创作品,转载请注明出处http://www.cnblogs.com/leo0000/p/5707482.html
最近遇到一个因符号找不到导致程序无法启动的情况,原因就是在于字符串表被修改了。下面是我自己创建的一个栗子;因为自己遇到的问题就是函数指针引起的,所以这边还是使用函数指针。
test1.c,后面将会编译成libtest1.so
int fun2() { return -1; } int (*pf)(); int fun1() { pf = fun2; return pf() ; }
test3.c,调用libtest1.so中的函数fun1,
int fun1(); int main() { fun1(); return 1; }
这里讲两个elf文件中的知识,elf文件一般有两个符号表,分别是.dynsym和.symtab,和两个字符串表,分别是.dynstr和.strtab,做了一下实验如果是连接动态库时,修改后者是不会对连接以及运行时造成影响的,而如果是静态链接,那么两者必须全部都一致。但是反汇编时使用的是后者。
本来test3文件是可以运行的,现在我把dynstr中的fun2改成fun6,此时会出现下面的提示,实际上该符号根本不需要重定向。这个可以通过将fun2定义为static时对比得知,如果fun2是static的,那么dynsym表中并不会出现fun2,或者是可执行文件也不会导出fun2。所以这边可以确定.dynsym中的是导出符号或者需要重定位符号。
./test: symbol lookup error: libtest1.so: undefined symbol: fun6
首先修改的是字符串表,间接影响到了符号表,但导出符号中关于fun6函数的信息还是
2: 00000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.0 (3) 3: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ 4: 00000000 0 NOTYPE WEAK DEFAULT UND _Jv_RegisterClasses 5: 00002010 0 NOTYPE GLOBAL DEFAULT ABS _edata 6: 00002018 4 OBJECT GLOBAL DEFAULT 23 pf 7: 000004a6 62 FUNC GLOBAL DEFAULT 11 fun1 8: 0000201c 0 NOTYPE GLOBAL DEFAULT ABS _end 9: 0000049c 10 FUNC GLOBAL DEFAULT 11 fun6
未修改前:
9: 0000049c 10 FUNC GLOBAL DEFAULT 11 fun2
接着实验,我把fun1改成fun8此时还是提示fun6找不到
5: 00002010 0 NOTYPE GLOBAL DEFAULT ABS _edata 6: 00002018 4 OBJECT GLOBAL DEFAULT 23 pf 7: 000004a6 62 FUNC GLOBAL DEFAULT 11 fun8 8: 0000201c 0 NOTYPE GLOBAL DEFAULT ABS _end 9: 0000049c 10 FUNC GLOBAL DEFAULT 11 fun6 10: 00002010 0 NOTYPE GLOBAL DEFAULT ABS __bss_start 11: 00000368 0 FUNC GLOBAL DEFAULT 9 _init 12: 00000528 0 FUNC GLOBAL DEFAULT 12 _fini
另一个实验,如果把fun2改成fun1,fun1还是fun1,那么会导致程序一直在调用fun1,知道栈溢出。
所以现在可以确定几件事情,对应源码来看,pf=fun2这句话相当于变成了pf=fun1,然后变成了解析fun1的函数位置。查看源码可以知道,pf是指向某个字符串表的字符串,通过该字符串来解析的,
当然这个我们就会想到使用的是同一个字符串表,fun6还是fun6啊,还是可以找到啊,但是实际上如果一个程序的符号很少,当然可以通过这种逐个对比的方式,在我前一篇的博文中热补丁就是使用的这种方式,而实际上,解释器程序会把字符串通过哈希函数映射到字符串表中的某个条目,而我们编译时使用的键值是fun2,而现在使用的是fun6,所以会导致找不到的现象出现。所以当我们把字符串改为fun6时,查找fun6,当然是找不到的。所以确定原因在于pf这个全局变量是需要重定位的。而将fun1改为fun8时还是提示fun6找不到,原因在于解析重定向表时的顺序。
另外一种情况:
#include <stdio.h> int fun2() { return -1; } int (*pf)(); int fun1() { printf("fun1 "); // pf = fun2; return fun2() ; }
所有的现象还是不变。查看反汇编代码可以看到,本来调用fun2的位置(也就是现在调用fun1的位置),
000004a6 <fun1>: 4a6: 55 push %ebp 4a7: 89 e5 mov %esp,%ebp 4a9: 53 push %ebx 4aa: 83 ec 14 sub $0x14,%esp 4ad: e8 e5 ff ff ff call 497 <__i686.get_pc_thunk.bx> 4b2: 81 c3 42 1b 00 00 add $0x1b42,%ebx 4b8: 8d 83 3e e5 ff ff lea -0x1ac2(%ebx),%eax 4be: 89 04 24 mov %eax,(%esp) 4c1: e8 fa fe ff ff call 3c0 <puts@plt> 4c6: e8 d5 fe ff ff call 3a0 <fun1@plt> 4cb: 83 c4 14 add $0x14,%esp 4ce: 5b pop %ebx
是通过plt表来解析的,这个解析效果和函数指针是一样的,因为是通过符号名称来重定位的。
曾经一度怀疑自己以前的想法是不是错了,以前我认为解释器只是解析重定向表中的符号,乍一看还以为解释器解析整个符号表。当然现在看来我还是正确的,解释器是只解析重定向表中的符号,只是使用了效率更高的hash表,而不是遍历。
总结一下,解释器解释函数符号时,通过函数符号经过hash函数运算得到hash值,然后映射到符号表中的一个条目,再通过符号表重定位该符号,不同符号会导致不同的hash值,所以会导致找不到。