⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄

📁 兼容内核漫谈 适合想将Windows上的程序移植到其它平台上的朋友研究查看
💻
📖 第 1 页 / 共 3 页
字号:
  在本映像(由第一个参数map给定)中找到其“字符串表”strtab。
  根据第二个参数reloc_offset在本映像中的GOT和符号表中找到其表项。
    /* 这些表项给出了目标共享库和目标函数的名字。 */
  通过_dl_lookup_versioned_symbol()或_dl_lookup_symbol()找到目标共享库的映像,并找到目标函数的地址。
    /* 对于x86处理器,elf_machine_plt_value()不起作用。 */
  通过elf_machine_fixup_plt()把目标函数的装入地址填写到本映像GOT的相应表项中。
  返回目标函数的地址。
}

    这整个过程完成了对一个目标函数的懒连接,并且实施了对于目标函数的调用。懒连接对于大型的软件往往能节约许多用于动态连接的时间。大型软件就其设计和编程而言常常是面面俱到的,所以在代码中要调用成百上千的共享库子程序,而且一个共享库又可能调用许多别的共享库,把所有这些共享库全都连接好可能很费时间,从而使得软件的启动速度明显变慢。但是,在实际的运行中,却可能只是集中在对一小部分共享库函数的调用,因为有许多共享库函数只是在特殊的条件下才会受到调用。这样,按实际需要进行的懒连接就显出其优越性来了。当然,就具体的函数而言,懒连接所需的时间反倒更长,但是因为需要连接的数量大大减少了,总的消耗就降低了。另一方面,懒连接是分散、零星地进行的,即使所消耗的时间总量不变,也比较不容易被使用者感觉到,因而更能被接受。所以有时候懒也有懒的好处。
    总结两种动态连接的库函数调用过程,可以把它们表示为简明的流程如下:
懒连接:
调用点 —〉PLTn —〉GOTn —〉PLT0 —〉GOT0 —〉_dl_runtime_resolve() —〉fixup()—〉被调用函数入口 —〉返回调用点
完成了动态连接之后:
调用点 —〉PLTn —〉GOTn —〉被调用函数入口 —〉返回调用点

    这里所涉及的PLT和GOT都在调用者所在的映像中,GOTn的原始内容是在编译/连接的过程中生成的,但是GOT0的内容则要由解释器予以填写,并且_dl_runtime_resolve()和fixup()存在于解释器的映像中。所以,如果没有解释器,无论是正常的动态连接还是懒连接都无法实现。
    顺便还要提一下,按编译时所使用的选项,由解释器设置到got[2]中的函数指针也可以不是指向_dl_runtime_resolve(),而是指向_dl_runtime_profile();而_dl_runtime_profile()所调用的不是fixup(),而是profile_fixup()。函数profile_fixup()不但实现懒连接,还使得以后每次通过PLTn/GOTn进行共享库函数调用时可以进行计数,从而统计出对每个共享库函数的调用次数。

    上面说的是从固定地址的目标映像中调用共享库中的子程序。如前所述,从共享库中也可以调用别的共享库中的子程序,此时的过程只是略有不同。下面以共享库libwine.so为例作一些说明。
    先看ELF头:

ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x1a60
  Start of program headers:          52 (bytes into file)
  Start of section headers:          311932 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         3
  Size of section headers:           40 (bytes)
  Number of section headers:         32
  Section header string table index: 29

    不同之处首先在于映像的类型是DYN、表示动态连接库、而不是EXEC。另一方面,由于共享库是浮动的,没有固定的装入地址,所以程序入口0x1a60只是该入口在映像中的位移,而不像前面那样是0x8048750一类的目标地址。至于装入以后到底在什么位置上,那要取决于当时(用户空间)虚拟地址区间的动态分配。

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .hash             HASH            00000094 000094 000428 04   A  2   0  4
  [ 2] .dynsym           DYNSYM          000004bc 0004bc 000850 10   A  3  1d  4
  [ 3] .dynstr           STRTAB          00000d0c 000d0c 0005aa 00   A  0   0  1
   . . . . . .
  [13] .rodata           PROGBITS        00005140 005140 0004ec 00   A  0   0 32
  [14] .eh_frame         PROGBITS        0000562c 00562c 000004 00   A  0   0  4
  [15] .data             PROGBITS        00006630 005630 000060 00  WA  0   0  4
  [16] .dynamic          DYNAMIC         00006690 005690 0000e0 08  WA  3   0  4
   . . . . . .
  [20] .got              PROGBITS        00006784 005784 000104 04  WA  0   0  4
   . . . . . .

    这里.data以前各项的“地址”与“位移”都是一致的,但是.data在映像中的位移为0x5630而“地址”为0x6630,二者相差0x1000,即4K字节、或一个页面。其实这毫不奇怪,只是说在装入用户空间时要在.eh_frame的终点与.data的起点之间空出一个页面。这里所谓“地址”0x6630是在装入用户空间以后的映像中的位移,而“位移”0x5630是在映像文件中的位移。除此以外,就没有什么特殊的了。
    但是在PLT方面却有些不同。下面是libwine.so的PLT:

00001700 <.plt>:
    1700: ff b3 04 00 00 00     pushl  0x4(%ebx)
    1706: ff a3 08 00 00 00     jmp    *0x8(%ebx)
    170c: 00 00                 add    %al,(%eax)
    170e: 00 00                 add    %al,(%eax)
    1710: ff a3 0c 00 00 00     jmp    *0xc(%ebx)
    1716: 68 00 00 00 00        push   $0x0
    171b: e9 e0 ff ff ff        jmp    1700 <_init+0x18>
    1720: ff a3 10 00 00 00     jmp    *0x10(%ebx)
    1726: 68 08 00 00 00        push   $0x8
    172b: e9 d0 ff ff ff        jmp    1700 <_init+0x18>
    1730: ff a3 14 00 00 00     jmp    *0x14(%ebx)
    1736: 68 10 00 00 00        push   $0x10
    173b: e9 c0 ff ff ff        jmp    1700 <_init+0x18>
    1740: ff a3 18 00 00 00     jmp    *0x18(%ebx)
    1746: 68 18 00 00 00        push   $0x18
    174b: e9 b0 ff ff ff        jmp    1700 <_init+0x18>
     . . . . . .

    与前面的PLT作一比较,就可以看到:无论是PLT0或PLTn,在形式上都与前面的一样,只是现在要用到GOT时均须使用基地址加位移的寻址方式。同样是间接寻址,在固定地址的映像中GOT的位置是预定的,而在浮动的共享库中则无法预先确定其地址。但是,共享库的GOT是共享库映像的一部分,随着共享库映像一起浮动,而GOT与PLT及代码之间的相对位移则保持不变,所以只要使寄存器%ebx中的基地址也一起浮动,就总是可以正确地寻访到GOT。所以这里PLT中的汇编代码有个前提,就是当通过call指令进入任何一个PLTn时寄存器%ebx的内容就是GOT的起点。
    那么怎样保证使%ebx的内容指向GOT呢?我们看一个实例。这一次在使用objdump时加了-S可选项,把编译前的C语言源程序也一起打印出来。

/* print the usage message */
static void debug_usage(void)
{
    2e84: 55                    push   %ebp
    2e85: 89 e5                 mov    %esp,%ebp
    2e87: 53                    push   %ebx
    2e88: 83 ec 08              sub    $0x8,%esp
    2e8b: e8 00 00 00 00        call   2e90 <debug_usage+0xc>
    2e90: 5b                    pop    %ebx
    2e91: 81 c3 f4 38 00 00     add    $0x38f4,%ebx
    static const char usage[] =
        "Syntax of the WINEDEBUG variable:\n"
        "  WINEDEBUG=[class]+xxx,[class]-yyy,...\n\n"
        "Example: WINEDEBUG=+all,warn-heap\n"
        "    turns on all messages except warning heap messages\n"
        "Available message classes: err, warn, fixme, trace\n";
    write( 2, usage, sizeof(usage) - 1 );
    2e97: 68 d7 00 00 00        push   $0xd7
    2e9c: 8d 83 bc ec ff ff     lea    0xffffecbc(%ebx),%eax
    2ea2: 50                    push   %eax
    2ea3: 6a 02                 push   $0x2
    2ea5: e8 86 e8 ff ff        call   1730 <_init+0x48>

    这次要调用的函数是write()。这个函数的本身在另一个共享库libc.so中,而libwine.so映像中为调用该函数而设的PLTn表项则在位移为0x1730处。为了在进入PLT之前使%ebx指向GOT,这里玩了一个小小的“诡计”。在位移0x2e8b处有一条call指令,这条指令用的是相对寻址,相对位移为0表明所调用的目标就是它的下一条指令,即位移0x2e90处的指令。从CPU的执行轨迹看,这条指令的执行与否并没有什么影响,因为它的下一条指令本来就在0x2e90处。可是,另一方面,由于是call指令,堆栈上就有了它的返回地址,那也是0x2e90(确切地说是映像在用户空间的起点加上位移0x2e90,见下)。所以,这条call指令的意图和作用只是把地址0x2e90放到了堆栈上,接着的pop指令则又把它放到了寄存器%ebx中。注意真正在运行时放入%ebx中的数值其实并不是0x2e90,而是这个映像在用户空间的起点加上位移0x2e90。这样,就达到了让%ebx的内容“水涨船高”的目的。可是这样放入%ebx的还只是位移为0x2e90处在用户空间的实际地址,而不是GOT在用户空间的实际地址,所以接着又在上面加上了二者的差距0x38f4。我们不妨算一下:0x2e90加0x38f4是0x6784,而前面所列.got的相对地址恰好是0x6784。
    至于GOT的内容,那些已经完成了动态连接的GOTn表项所持有的就是指向受调用共享库映像中相应函数的指针,这与固定地址映像中的GOTn并无不同,所不同的只是从相应PLTn中引用这个指针时的寻址方式不同。然而GOTn中用于懒连接的原始内容就有些不同了,下面仍用od观察libwine.so映像文件中GOT所在处的原始内容。注意GOT的起点0x6784是在装入用户空间以后的映像中的位移,而在映像文件中的位移则为0x5784:

005780 0000 0000 6690 0000 0000 0000 0000 0000
005790 1716 0000 1726 0000 1736 0000 1746 0000
0057a0 1756 0000 1766 0000 1776 0000 1786 0000
0057b0 1796 0000 17a6 0000 17b6 0000 17c6 0000
0057c0 17d6 0000 17e6 0000 17f6 0000 1806 0000
. . . . . .
    对于共享库函数write(),这里的指针指向0x001736,但是那只是相应PLTn中的push指令在映像文件中的位移。显然,将映像装入(映射到)用户空间之后,还需要根据装入的位置对这些指针作出调整,这也是由解释器完成的。具体地,这是解释器通过一个函数_dl_relocate_object()完成的。对于装入的每一个共享库,解释器都要通过这个函数对其执行重定位(relocate),其中就包括了对其各个GOTn表项的重定位。

    最后还要说明,同一个共享库的映像可以同时被映射到多个进程的用户空间。比方说,要是映像中的某个页面此刻存在于某个物理页面,那么这个物理页面就被映射到所有装入了这个共享库的进程中,只是在各个进程中的虚拟地址可能不同(但都在用户空间)。也就是说,一个拷贝为多个进程所共享,所以才叫“共享”库。不过这只是大体上而言,实际的情况还要复杂一些。读者在前面看到,wine映像有两个类型为LOAD的Segment。前者的访问权限为可读可执行、但是不可写,这当然可以为多个进程所共享。而后者的访问权限却是可读可写,这就不能由多个进程共享了。所以,凡属这个Segment中的页面,每个有关的进程就各有其自己的物理页面。再看这两个Segment中的内容。前者有(例如) .text和.plt等Section。这是可以理解的,因为.text是程序代码,这对于所有共享这个程序库的进程都一样;而.plt就是PLT,里面的内容也是对于所有共享这个程序库的进程都一样。再说,这些Section的内容也不会随着程序的运行而改变(不可写)。后者的内容则有(例如).data、.bss、.got等Section。不言而喻,.data和.bss中都是数据,当然不能让不同的进程互相干扰,必须得各有各的物理空间。至于.got的内容,那也得因进程而异。因为这是用来建立动态连接的,但是同一共享库在不同进程中的映射地址却可能不同,从而引起.got的内容也不相同。
    在与别的进程共享程序段等等信息之余,每个进程都需要有些私有的、“本地的”信息,不能与别的进程共享,这是很自然、也比较容易实现的,因为毕竟不同的进程“生活”在不同的空间中。而同在一个空间的若干线程,则一般是不分彼此、“肝胆相照”的。但是,有时候也会需要有些“私房”,特别是在对一些全局量的使用上需要有只属于本线程的拷贝。为了解决这样的问题,就发展起来一种技术称为“线程本地存储(Thread Local Storage)”、即TLS。当然,解释器对于支持TLS的共享库有特殊的处理,这里就不深入进去了。

    现在读者对ELF动态连接的过程已经有了个大致的认识。在此基础上,解释器的作用就可想而知了,大体上就是:
    ●  检查目标映像中类型为DYNAMIC的Segment,其中每个类型为NEEDED的表项都指定了一个需要用到的共享库。
    ●  对于所需的每个共享库,装入该共享库,并根据具体的装入地址对其实行重定位操作,包括修正其GOT中的原始内容。
    ●  如果不是懒连接,就根据目标映像的动态符号表对(目标映像中)每个相应的GOTn表项实施动态连接。
    ●  检查目标映像直接使用的每个(一级)共享库,如果又要用到别的(二级)共享库,就对其递归实施上述操作。余类推。
    ●  最后转入目标映像的程序入口。
    至于解释器的具体代码,则一来比较冗长,二来并非我们当务之急,就留待以后空一些时候再说吧。

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -