由函数符号找不到联想到的动态链接过程

原创作品,转载请注明出处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值,所以会导致找不到。

原文地址:https://www.cnblogs.com/leo0000/p/5707482.html