以实际的Hello World小程序为例子,作为动态库的学习材料。

开章上个图,看完明白这个图。

ELF中PLT和GOT工作机制简介-可还原_动态库

1.编译过程

以最简单的 Hello world为例

#include <stdio.h>

int main()

{

      printf("hello, world\n");

      return 0;

}

预处理:
gcc -E hello.c -o hello.i

编译:

gcc -S hello.i -o hello.s

汇编:

gcc -g -c hello.s -o hello.o

链接:

gcc hello.o -o hello

2.ELF文件格式

得到的hello是elf格式的。

readelf -h hello

ELF Header:

  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00

  Class:                             ELF64

  Data:                              2's complement, little endian

  Version:                           1 (current)

  OS/ABI:                            UNIX - System V

  ABI Version:                       0

  Type:                              EXEC (Executable file)

  Machine:                           AArch64

  Version:                           0x1

  Entry point address:               0x4004a0

  Start of program headers:          64 (bytes into file)

  Start of section headers:          69408 (bytes into file)

  Flags:                             0x0

  Size of this header:               64 (bytes)

  Size of program headers:           56 (bytes)

  Number of program headers:         9

  Size of section headers:           64 (bytes)

  Number of section headers:         34

  Section header string table index: 33

 

这个可以写个文章,后续涉及了再展开。

 

3.GOT/PLT

我们知道.text segment是只读的,也就是说在编译成可以执行文件之后,不能被修改了,如何确保它能够正确的引用在加载时才能确定下来的动态链接库里的符号呢?需要GOT和PLT作为跳板来实现了。

GOT全称Global Offset Table,即全局偏移量表。它在可执行文件中是一个单独的section,位于.data section的前面。每个被目标模块引用的全局符号(函数或者变量)都对应于GOT中一个8字节的条目。编译器还为GOT中每个条目生成一个重定位记录。在加载时,动态链接器会重定位GOT中的每个条目,使得它包含正确的目标地址。

PLT全称Procedure Linkage Table,即过程链接表。它在可执行文件中也是一个单独的section,位于.text section的前面。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目实际上都是一小段可执行的代码。

GOT/PLT是链接器在执行重定向时会用到的部分。

3.1GOT

GOT, 即Global Offset Table, 全局偏移表. 是链接器在执行链接时实际上要填充的部分, 保存了所有外部符号的地址信息。除了每个函数占用一个GOT表项外。

.got.plt相当于.plt的GOT全局偏移表, 其内容有两种情况, 1)如果在之前查找过该符号,内容为外部函数的具体地址。2)如果没查找过, 则内容为跳转回.plt的代码, 并执行查找。

.got保存全局变量引用的地址,.got.plt保存函数引用地址。

.got表属于数据段, 是可写的. 表中存储的是指针.

3.2PLT

PLT, 即Procedure Linkage Table, 进程链接表, 表里包含了一些代码,
用来调用链接器来解析某个外部函数的地址, 并填充到.got.plt中, 然后跳转到该函数; 或直接在.got.plt中查找并跳转到对应外部函数(如果已经填充过)。

GOT表可写不可执行, PLT可执行不可写,他们相互作用来实现函数符号的延时绑定。ASLR并不随机化PLT部分。

我们知道通过readelf -S hello可以得到段信息。

4.Section

通过readelf -S hello可以得到段信息。

有两种类型的符号表, 一种是常规的(.symtab和.strtab), 另一种是动态的(.dynsym和.dynstr)。常规的符号表通常只在调试时用到. 平时用的strip命令删除的就是该符号表。

这里要关注的是这些section的地址。

There are 30 section headers, starting at offset 0x10db8:

 

Section Headers:

  [Nr] Name              Type             Address           Offset

       Size              EntSize          Flags  Link  Info  Align

  [ 0]                   NULL             0000000000000000  00000000

       0000000000000000  0000000000000000           0     0     0

  [ 1] .interp           PROGBITS         0000000000400238  00000238

       000000000000001b  0000000000000000   A       0     0     1

  [ 2] .note.ABI-tag     NOTE             0000000000400254  00000254

       0000000000000020  0000000000000000   A       0     0     4

  [ 3] .note.gnu.build-i NOTE             0000000000400274  00000274

       0000000000000024  0000000000000000   A       0     0     4

  [ 4] .gnu.hash         GNU_HASH         0000000000400298  00000298

       0000000000000030  0000000000000000   A       5     0     8

  [ 5] .dynsym           DYNSYM           00000000004002c8  000002c8

       0000000000000078  0000000000000018   A       6     1     8

  [ 6] .dynstr           STRTAB           0000000000400340  00000340

       0000000000000042  0000000000000000   A       0     0     1

  [ 7] .gnu.version      VERSYM           0000000000400382  00000382

       000000000000000a  0000000000000002   A       5     0     2

  [ 8] .gnu.version_r    VERNEED          0000000000400390  00000390

       0000000000000020  0000000000000000   A       6     1     8

  [ 9] .rela.dyn         RELA             00000000004003b0  000003b0

       0000000000000018  0000000000000018   A       5     0     8

  [10] .rela.plt         RELA             00000000004003c8  000003c8

       0000000000000060  0000000000000018  AI       5    23     8

  [11] .init             PROGBITS         0000000000400428  00000428

       0000000000000014  0000000000000000  AX       0     0     4

  [12] .plt              PROGBITS         0000000000400440  00000440

       0000000000000060  0000000000000010  AX       0     0     16

  [13] .text             PROGBITS         00000000004004a0  000004a0

       00000000000001fc  0000000000000000  AX       0     0     8

  [14] .fini             PROGBITS         000000000040069c  0000069c

       0000000000000010  0000000000000000  AX       0     0     4

  [15] .rodata           PROGBITS         00000000004006b0  000006b0

       000000000000001d  0000000000000000   A       0     0     8

  [16] .eh_frame_hdr     PROGBITS         00000000004006d0  000006d0

       0000000000000044  0000000000000000   A       0     0     4

  [17] .eh_frame         PROGBITS         0000000000400718  00000718

       0000000000000134  0000000000000000   A       0     0     8

  [18] .init_array       INIT_ARRAY       000000000041fdf0  0000fdf0

       0000000000000008  0000000000000008  WA       0     0     8

  [19] .fini_array       FINI_ARRAY       000000000041fdf8  0000fdf8

       0000000000000008  0000000000000008  WA       0     0     8

  [20] .jcr              PROGBITS         000000000041fe00  0000fe00

       0000000000000008  0000000000000000  WA       0     0     8

  [21] .dynamic          DYNAMIC          000000000041fe08  0000fe08

       00000000000001d0  0000000000000010  WA       6     0     8

  [22] .got              PROGBITS         000000000041ffd8  0000ffd8

       0000000000000010  0000000000000008  WA       0     0     8

  [23] .got.plt          PROGBITS         000000000041ffe8  0000ffe8

       0000000000000038  0000000000000008  WA       0     0     8

  [24] .data             PROGBITS         0000000000420020  00010020

       0000000000000004  0000000000000000  WA       0     0     1

  [25] .bss              NOBITS           0000000000420024  00010024

       0000000000000004  0000000000000000  WA       0     0     1

  [26] .comment          PROGBITS         0000000000000000  00010024

       000000000000002d  0000000000000001  MS       0     0     1

  [27] .symtab           SYMTAB           0000000000000000  00010058

       0000000000000978  0000000000000018          28    79     8

  [28] .strtab           STRTAB           0000000000000000  000109d0

       00000000000002de  0000000000000000           0     0     1

  [29] .shstrtab         STRTAB           0000000000000000  00010cae

       0000000000000108  0000000000000000           0     0     1

Key to Flags:

  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),

  L (link order), O (extra OS processing required), G (group), T (TLS),

  C (compressed), x (unknown), o (OS specific), E (exclude),

  p (processor specific)

4.1Section片段PLT/GOT信息

[12] .plt              PROGBITS         0000000000400440  00000440

       0000000000000060  0000000000000010  AX       0     0     16

 

[22] .got              PROGBITS         000000000041ffd8  0000ffd8

       0000000000000010  0000000000000008  WA       0     0     8

[23] .got.plt          PROGBITS         000000000041ffe8  0000ffe8

       0000000000000038  0000000000000008  WA       0     0     8

[24] .data             PROGBITS         0000000000420020  00010020

       0000000000000004  0000000000000000  WA       0     0     1

 

可以看到

.plt起始于0x400440,每个条目大小为0x10(16个字节),共6个条目;

.got起始于0x41ffd8,每个条目大小为0x8(8个字节),共2个条目;

.got.plt起始于41ffe8,每个条目大小为0x8(8个字节),共7个条目;

通过objdump -d hello摘取片段:

.plt的section

5.单步调试/实例演示

有了这些知识有,以上面的hello为例。在调用puts函数的地方设置断点。

#gdb ./hello

在puts处设置断点,通过disassemble main显示汇编

gdb>disassemble main

=> 0x0000000000400610 <+16>:   bl  0x400490 <puts@plt>

可以看到0x400490这个地址就是plt section中的puts@plt,也就是说当调用puts函数的时候,是会调到plt中的。

gdb>disassemble 0x400490

0000000000400490 <puts@plt>:

  400490: 90000110 adrp x16, 420000 <__libc_start_main@GLIBC_2.17>

  400494: f9400e11 ldr  x17, [x16,#24]

  400498: 91006210 add  x16, x16, #0x18

  40049c: d61f0220 br   x17

  40044c: 913fe210 add  x16, x16, #0xff8

  400450: d61f0220 br   x17

  400454: d503201f nop

  400458: d503201f nop

  40045c: d503201f nop

其中adrp是获取4KB的页的基址。这个地址:0x420000中有包含了进程相关外部函数的地址,而__libc_start_main为首个。0x420000这个地址是属于.got.plt。.got的地址是 000000000041ffd8, .got.plt的地址是000000000041ffe8。

先来看下:

(gdb) x /16x 0x00000000041ffe8

0x41ffe8: 0x00000000    0x00000000    0xbe801168    0x0000ffff

0x41fff8: 0xbe7e4110    0x0000ffff    0xbe621624    0x0000ffff

0x420008: 0x00400440    0x00000000    0x00400440    0x00000000

0x420018: 0x00400440   0x00000000    0x00000000    0x00000000

(gdb) disas 0x00000000041ffd8

Dump of assembler code for function _GLOBAL_OFFSET_TABLE_:

   0x000000000041ffd8: .inst    0x0041fe08 ; undefined

   0x000000000041ffdc: .inst    0x00000000 ; undefined

   0x000000000041ffe0: .inst    0x00000000 ; undefined

   0x000000000041ffe4: .inst    0x00000000 ; undefined

End of assembler dump.

.got.plt

0x0000ffffbe801168 GOT[1]:设置动态库映射信息数据结构link_map地址。

0x0000ffffbe7e4110GOT[2]:是_dl_runtime_resolve函数地址,设置动态连接器符号解析函数的地址。

.got中,第一项是0x0041fe08也就是.dynamic section,其他都是未定义的。

0x0000ffffbe621624__libc_start_main函数地址。

继续,接着查看地址0x420000。

(gdb) x /20x 0x420000

0x420000:   0xbe621624  0x0000ffff  0x00400440  0x00000000

0x420010:   0x00400440  0x00000000  0x00400440 0x00000000

0x420020:   0x00000000  0x00000000  0x00000000  0x00000000

0x420030:   0x00000000  0x00000000  0x00000000  0x00000000

0x420040:   0x00000000  0x00000000  0x00000000  0x00000000

其中[x16,#24]是0x00400440 ,并不是函数的地址,而是.plt的开始地址。这个不是函数的地址,是PLT中prepare resolver指令的地址。如下源码中。

(gdb) disassemble  0x00400440,0x00400490

Dump of assembler code from 0x400440 to 0x400490:

   0x0000000000400440: stp  x16, x30, [sp,#-16]!

   0x0000000000400444: adrp x16, 0x41f000

   0x0000000000400448: ldr  x17, [x16,#4088]

   0x000000000040044c: add  x16, x16, #0xff8

   0x0000000000400450: br   x17

   0x0000000000400454: nop

   0x0000000000400458: nop

   0x000000000040045c: nop

其中0x41f000+#4088= 0x41fff8,该地址中是_dl_runtime_resolve 的函数地址。所以说_dl_runtime_resolve是常驻在.plt中的开始部分的。

也可以如下方式查看:(gdb) x /16x 0x00400440

0x400440:   0xa9bf7bf0  0xf00000f0  0xf947fe11  0x913fe210

0x400450:   0xd61f0220  0xd503201f  0xd503201f  0xd503201f

0x400460 <__libc_start_main@plt>:  0x90000110  0xf9400211  0x91000210  0xd61f0220

0x400470 <__gmon_start__@plt>: 0x90000110  0xf9400611  0x91002210  0xd61f0220

0x400480 <abort@plt>:  0x90000110  0xf9400a11  0x91004210  0xd61f0220

会调用 _dl_runtime_resolve 解析 puts 函数的地址,并将该函数真正的地址填充到 puts@got.plt,最后跳转到 puts 函数继续执行代码。

继续gdb调试执行后如下:

(gdb) s

hello, world

23      mov w0, 0

(gdb) x /20x 0x420000

0x420000:   0xbe621624  0x0000ffff  0x00400440  0x00000000

0x420010:   0x00400440  0x00000000  0xbe66c7e8 0x0000ffff

0x420020:   0x00000000  0x00000000  0x00000000  0x00000000

0x420030:   0x00000000  0x00000000  0x00000000  0x00000000

0x420040:   0x00000000  0x00000000  0x00000000  0x00000000

发现got.plt中的函数地址变化了,已经是0xbe66c7e8 0x0000ffff

(gdb) disassemble  0x0000ffffbe66c7e8

Dump of assembler code for function puts:

……

然后发现这个地址已经是puts函数所在地址了。

这个地址是如何变化的?就是_dl_runtime_resolve函数干的。

ELF中PLT和GOT工作机制简介-可还原_可执行_02

 

关于dll_runtime_resolve的工作机制,这个更加深入的探讨,到以后有机会再进一步深入