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

📄

📁 兼容内核漫谈 适合想将Windows上的程序移植到其它平台上的朋友研究查看
💻
📖 第 1 页 / 共 3 页
字号:
  [22] .bss              NOBITS          0804a324 001324 000008 00  WA  0   0  4
  [23] .stab             PROGBITS        00000000 001324 004878 0c     24   0  4
  [24] .stabstr          STRTAB          00000000 005b9c 014cd4 00      0   0  1
  [25] .comment          PROGBITS        00000000 01a870 000165 00      0   0  1
  [26] .debug_aranges    PROGBITS        00000000 01a9d8 000078 00      0   0  8
  [27] .debug_pubnames   PROGBITS        00000000 01aa50 000025 00      0   0  1
  [28] .debug_info       PROGBITS        00000000 01aa75 000a98 00      0   0  1
  [29] .debug_abbrev     PROGBITS        00000000 01b50d 000138 00      0   0  1
  [30] .debug_line       PROGBITS        00000000 01b645 000284 00      0   0  1
  [31] .debug_frame      PROGBITS        00000000 01b8cc 000014 00      0   0  4
  [32] .debug_str        PROGBITS        00000000 01b8e0 0006be 01  MS  0   0  1
  [33] .shstrtab         STRTAB          00000000 01bf9e 00013a 00      0   0  1
  [34] .symtab           SYMTAB          00000000 01c678 000890 10     35  5c  4
  [35] .strtab           STRTAB          00000000 01cf08 0005db 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

    这是按Section的名称列出的,其中跟动态连接有关的Section也出现在前面名为Dynamic的Segment中,只是在那里是按类型列出的。例如,前面类型为HASH的表项说与此有关的信息在0x8048128处,而这里则说有个名为.hash的Section,其起始地址为0x8048128。还有,前面类型为PLTGOT的表项说与此有关的信息在0x804a2c4处,这里则说有个名为.got的Section,其起始地址为0x804a2c4,不过Section表中提供的信息更加详细一些,有些信息则互相补充。在Section表中,只要类型为PROGBITS,就说明这个Section的内容都来自映像文件,反之类型为NOBITS就说明这个Section的内容并非来自映像文件。
    有些Section名是读者本来就知道的,例如.text、.data、.bss;有些则从它们的名称就可猜测出来,例如.symtab是符号表、.rodata是只读数据、还有.comment和.debug_info等等。还有一些可能就不知道了,这里择其要者先作些简略的介绍:

    (1).hash。为便于根据函数/变量名找到有关的符号表项,需要对函数/变量名进行hash计算,并根据计算值建立hash队列。
● .dynsym。需要加以动态连接的符号表,类似于内核模块中的INPORT符号表。这是动态连接符号表的数据结构部分,须与.dynstr联用。
● .dynstr。动态连接符号表的字符串部分,与.dynsym联用。
● .rel.dyn。用于动态连接的重定位信息。
● .rel.plt。一个结构数组,其中的每个元素都代表着GOP表中的一个表项GOTn(见下)。
● .init。在进入main()之前执行的代码在这个Section中。
● .plt。“过程连接表(Procedure Linking Table)”,见后。
● .fini。从main()返回之后执行的代码在这个Section中,最后会调用exit()。
● .ctors。表示“Constructor”,是一个函数指针数组,这些函数需要在程序初始化阶段(进入main()之前,在.init中)加以调用。
● .dtors。表示“Distructor”,也是一个函数指针数组,这些函数需要在程序扫尾阶段(从main()返回之后,在.fini中)加以调用。
● .got。“全局位移表(Global Offset Table)”,见后。
● .strtab。与符号表有关的字符串都集中在这个Section中。

    其中我们最关心的是“过程连接表(Procedure Linking Table)”PLT和“全局位移表(Global Offset Table)”GOT。程序之间的动态连接就是通过这两个表实现的。
    下面我们通过一个实例来说明程序之间的动态连接。目标映像/usr/local/bin/wine的main()函数中调用了一个库函数getenv(),这个函数在C语言共享库libc.so.6中。下面是main()经编译/连接以后的汇编代码:

08048ce0 <main>:
8048ce0: 55                    push   %ebp
8048ce1: 89 e5                 mov    %esp,%ebp
. . . . . .
8048cef: 68 20 91 04 08        push   $0x8049120
8048cf4: e8 47 f9 ff ff        call   8048640 <_init+0x58>

    本来,这里call指令机器代码的后4个字节应该是目标函数getenv()的入口地址。可是,这个目标函数在共享库libc.so.6中,而这个共享库的装入地址是浮动的,要到装入了以后才能知道其地址。怎么办?一个不必很有天分的人就能想到的简单办法是:编译时先让这条call指令空着,但是创建一个带有字符串“getenv”的数据结构,并让这个数据结构中有个指针反过来指向这条call指令;而在动态连接时,则让“解释器”在共享库的导出符号表中寻找这个符号,找到后根据其装入后的位置计算出应该填入这条call指令的数值,再把结果填写到这里的call指令中、即地址为0x8048cf5的地方。当然,程序中调用getenv()的地方可能不止一个,所以在调用者的映像中需要把所有调用getenv()的地方都记下来。然而,不幸的是这样的地方可能成百上千,而类似于getenv()这样由共享库提供的函数也可能成百上千。更何况一个共享库可能还要用到别的共享库,从而形成一个多层次的共享库“图”。这样一来,动态连接的效率就大成问题了,显然这不是个好办法。
    那么实际采用的办法是什么样的呢?这里的call指令采用的是相对寻址,调用的子程序入口地址为0x8048640,我们就循着这个地址看过去:

8048640: ff 25 dc a2 04 08     jmp    *0x804a2dc
8048646: 68 18 00 00 00        push   $0x18
804864b: e9 b0 ff ff ff        jmp    8048600 <_init+0x18>

    这就已经在wine映像的PLT表中了,这几条指令就构成getenv()在PLT中的表项,程序中凡是对getenv()的调用都先来到这里。当然,PLT表中有许多这样的表项,对应着许多需要通过动态连接引入的函数,凡是这样的表项都以PLTn表示之。所有的PLTn都是相似的,但是PLT表中的第一个表项、即PLT0、却是特殊的:

08048600 <.plt>:
8048600: ff 35 c8 a2 04 08     pushl  0x804a2c8
8048606: ff 25 cc a2 04 08     jmp    *0x804a2cc
804860c: 00 00                 add    %al,(%eax)
804860e: 00 00                 add    %al,(%eax)
8048610: ff 25 d0 a2 04 08     jmp    *0x804a2d0
8048616: 68 00 00 00 00        push   $0x0
804861b: e9 e0 ff ff ff        jmp    8048600 <_init+0x18>
8048620: ff 25 d4 a2 04 08     jmp    *0x804a2d4
8048626: 68 08 00 00 00        push   $0x8
804862b: e9 d0 ff ff ff        jmp    8048600 <_init+0x18>
8048630: ff 25 d8 a2 04 08     jmp    *0x804a2d8
8048636: 68 10 00 00 00        push   $0x10
804863b: e9 c0 ff ff ff        jmp    8048600 <_init+0x18>
8048640: ff 25 dc a2 04 08     jmp    *0x804a2dc
8048646: 68 18 00 00 00        push   $0x18
804864b: e9 b0 ff ff ff        jmp    8048600 <_init+0x18>
. . . . . .

    可以看出,除PLT0以外,所有的PLTn的形式都是一样的,而且最后的jmp指令都是以0x8048600、即PLT0为目标,所不同的只是第一条jmp指令的目标和push指令中的数据。PLT0则与之不同,但是包括PLT0在内的每个表项都占16个字节,所以整个PLT就像是个数组。其实PLT0只需要12个字节,但是为了大小划一而补了4个字节的0。
    注意每个PLTn中的第一条jmp指令是间接寻址的。以getenv()的表项为例,是以地址0x804a2dc处的内容为目标地址进行跳转。这样,只要把getenv()装入用户空间后的入口地址填写在0x804a2dc处,就可以实现正确的跳转,即实现了与共享库中函数getenv()的动态连接。这样,对于共享库函数的每次调用,额外的消耗只是执行一条间接寻址的jmp指令所需的时间。另一方面,这是不涉及堆栈的跳转指令,堆栈的内容在跳转的过程中保持不变,所以当getenv()执行ret指令返回时就直接回到了调用它的地方,在这里是前面的main()中。
    由此可见,解释器的任务就是事先把getenv()装入用户空间后的入口地址填写在0x804a2dc处。不仅是getenv(),共享库提供的库函数可能有很多,对于每个这样的库函数都得保存一个用于间接寻址跳转的指针。保存这些指针的地方就是GOT。与PLT相对应,每个PLTn在GOT中都有个相应的GOTn,但是每个GOTn只是一个函数指针。同样,GOT0也是特殊的,而且GOT0的大小也不一样,有12个字节,相当于三个GOTn那么大。在“解释器”ld-linux.so的代码中把GOT0的三个长字表示成got[0]、got[1]、和got[2],注意不要跟GOTn相混淆。显然、解释器负有正确设置所有GOTn的责任。
    既然如此,每个PLTn中只要一条指令就行了,代码中为什么有三条呢?还有,PLT0和GOT0又是干什么用的呢?原来,那都是为实现“懒惰式”的动态连接、即“懒连接”而存在的。简而言之,“懒连接”就是解释器并不事先完成对共享库函数的动态连接、即不事先设置GOTn、而把对具体共享库函数的动态连接拖延到真正要用的时候才来进行,需要用哪一个函数就连接哪一个函数,绝不“积极主动”,以免劳而无功。
    然而,要是不事先设置好GOTn的内容,PLTn中的(间接寻址)跳转指令会跳到什么地方去?这要看GOTn中的原始内容,这内容来自目标映像(对外进行库函数调用的映像)。
    我们看wine映像所提供的GOT原始内容。这个映像的GOT起始地址为0x0804a2c4,跳过GOT0的12个字节,GOTn是从0x0804a2d0开始的:

Relocation section '.rel.plt' at offset 0x548 contains 20 entries:

Offset     Info    Type            Sym.Value  Sym. Name
0804a2d0  00000107 R_386_JUMP_SLOT   08048610   strchr
0804a2d4  00000207 R_386_JUMP_SLOT   08048620   getpid
0804a2d8  00000307 R_386_JUMP_SLOT   08048630   fprintf
0804a2dc  00000407 R_386_JUMP_SLOT   08048640   getenv
0804a2e0  00000507 R_386_JUMP_SLOT   08048650   pthread_create
. . . . . .

    从这里地址为0x0804a2dc的这一表项看,似乎这个指针所指向的是0x08048640,这正是指令“jmp *0x804a2dc”所在的位置。设想如果CPU在尚未完成对getenv()的动态连接之前就调用了这个函数,从而在PLT中执行了指令“jmp *0x804a2dc”,那岂不就是执行了一条指向其自身的跳转指令?这可是一个最紧扣的死循环!
    然而事实并非如此,这里的信息是经过readelf整理的,旨在让使用者知道这一表项跟PLT表中地址为0x08048640的表项相对应,并且相应的函数名为getenv。这是readel综合了好几方面的信息才形成的报告,不幸的是在有些情况下这就成了很误导的报告。而实际上存储在映像中这个位置上的数据却有所不同。为此,我们通过另一个工具od去观察这个位置上真实的、原始的内容。根据前面关于.got的信息,地址0x 0804a2c4在映像中的位移是0x0012c4,所以从0x0804a2d0开始的几个指针是:

0012d0 8616 0804 8626 0804 8636 0804 8646 0804
0012e0 8656 0804 8666 0804 8676 0804 8686 0804

    可见,地址0x0804a2dc处的内容其实指向0x08048646。这个地址同样在getenv()的PLT表项中,但这是第二条指令“push $0x18”所在的地址。所以,即使在动态连接之前就试图调用getenv(),至少在这里是不会出问题的。
    回过去看getenv()在PLT中的表项。在执行了“push $0x18”以后,下一条指令是“jmp 0x8048600”,这就是PLT0的起点。注意每个PLTn表项的最后一条指令都是相同的,都是跳转到PLT0的起点,所不同的只是压入堆栈的数值,所以这里0x18就代表着getenv()。另一方面,PLT中在getenv()之前的几个表项压入堆栈的数值分别为0x0、0x8、0x10,所以0x18表示这是PLT中的第4个函数。相应地,我们从.dynsym的内容也可以得到验证:

Symbol table '.dynsym' contains 25 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 08048610   359 FUNC    GLOBAL DEFAULT  UND strchr@GLIBC_2.0 (2)
     2: 08048620     8 FUNC    GLOBAL DEFAULT  UND getpid@GLIBC_2.0 (2)
     3: 08048630    23 FUNC    GLOBAL DEFAULT  UND fprintf@GLIBC_2.0 (2)
     4: 08048640   229 FUNC    GLOBAL DEFAULT  UND getenv@GLIBC_2.0 (2)
     5: 08048650   312 FUNC    GLOBAL DEFAULT  UND pthread_create@GLIBC_2.1 (3)
      . . . . . .

    再看PLT0,从getenv()的PLT表项跳转到PLT0以后,先把地址0x804a2c8处的内容压入堆栈,这就是got[1]的内容,是由解释器事先设置好了的,这是一个指向代表着本映像的数据结构的指针。然后又是间接寻址的跳转指令,这次使用的地址是0x804a2cc,即&got[2],其内容是个指针,也是由解释器事先设置好的,指向一个函数_dl_runtime_resolve(),这就是用来实现懒连接的函数。注意每次进入_dl_runtime_resolve()只完成一个共享库函数的懒连接,那就是把目标函数的实际地址填写到相应的GOTn中,并且跳转到这个函数。
    这样,当目标程序对外调用某个共享库函数时,如果对该函数的动态连接业已完成,那么CPU通过相应的PLTn表项和GOTn表项进行间接寻址的跳转。而若是在尚未建立连接之前,那就临时实行“懒连接”。此时先后由PLTn和PLT0压入堆栈的两项数据分别代表着具体的(调用者)映像和需要调用的具体函数。
    下面就是_dl_runtime_resolve()的事了。

_dl_runtime_resolve:\n\
pushl %eax             # Preserve registers otherwise clobbered.\n\
pushl %ecx\n\
pushl %edx\n\
movl 16(%esp), %edx     # Copy args pushed by PLT in register.  Note\n\
movl 12(%esp), %eax     # that `fixup' takes its parameters in regs.\n\
call fixup               # Call resolver.\n\
popl %edx              # Get register content back.\n\
popl %ecx\n\
xchgl %eax, (%esp)      # Get %eax contents end store function address.\n\
ret $8                  # Jump to function address.\n\

    由于前面的三条push指令,这里的12(%esp)就是PLT0所压入的数据结构指针,也就是下面fixup()的第一个调用参数;而16(%esp)就是PLTn所压入的目标函数标识(位移)。函数fixup()一方面从共享库映像中找到目标函数的入口、并将其填写在GOTn中,使得下一次再调用同一函数时可以直接从PLTn通过间接寻址进入目标函数;一方面通过寄存器%eax返回这个函数指针。然后,指令“xchgl %eax, (%esp)”把这个指针交换到了堆栈上。这样一来,下面的“ret $8”指令就使CPU“返回”到了目标函数中,同时又从堆栈上清除了由PLT0和PLTn压入的两项数据。当CPU进入目标函数时,堆栈上的内容首先是调用点的返回地址,然后是对于目标函数的调用参数,就像目标函数直接受到调用时一样。

    下面是fixup()的伪代码,有兴趣的读者可以在GLIBC-2.3/elf/dl-runtime.c中找到它的源代码。

fixup (struct link_map *map, unsigned reloc_offset)
{

⌨️ 快捷键说明

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