计算机系统篇之链接(9):位置无关代码(下)——真正理解 PIC 函数调用的工作原理(Linux X86-64 示例)

Author: stormQ

Created: Wednesday, 15. April 2020 04:35PM

Last Modified: Sunday, 01. November 2020 11:38AM



摘要

本文以 Linux X86-64 程序为例,利用 gdb 详细分析了位置无关代码技术中函数调用的过程,从而真正理解位置无关代码的工作原理。

研究过程

step 1: 生成共享库 libadd_debug.so 和 libsub_debug.so,见前篇

step 2: 生成测试程序(用于调用以上两个共享库)——main_mix,见前篇

step 3: 分析可执行文件 main_mix 中 PLT 与 GOT 条目间的对应关系

1)查看可执行文件 main_mix 的 sections

$ readelf -S main_mix

输出结果为:

There are 36 section headersstarting at offset 0x1c40:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
Skip ......
  [ 9] .rela.dyn         RELA             0000000000400520  00000520
       0000000000000018  0000000000000018   A       5     0     8
  [10] .rela.plt         RELA             0000000000400538  00000538
       0000000000000048  0000000000000018  AI       5    24     8
  [11] .init             PROGBITS         0000000000400580  00000580
       000000000000001a  0000000000000000  AX       0     0     4
  [12] .plt              PROGBITS         00000000004005a0  000005a0
       0000000000000040  0000000000000010  AX       0     0     16
  [13] .plt.got          PROGBITS         00000000004005e0  000005e0
       0000000000000008  0000000000000000  AX       0     0     8
  [14] .text             PROGBITS         00000000004005f0  000005f0
       0000000000000192  0000000000000000  AX       0     0     16
  [15] .fini             PROGBITS         0000000000400784  00000784
       0000000000000009  0000000000000000  AX       0     0     4
  [16] .rodata           PROGBITS         0000000000400790  00000790
       0000000000000004  0000000000000004  AM       0     0     4
  [17] .eh_frame_hdr     PROGBITS         0000000000400794  00000794
       0000000000000034  0000000000000000   A       0     0     4
  [18] .eh_frame         PROGBITS         00000000004007c8  000007c8
       00000000000000f4  0000000000000000   A       0     0     8
  [19] .init_array       INIT_ARRAY       0000000000600df0  00000df0
       0000000000000008  0000000000000000  WA       0     0     8
  [20] .fini_array       FINI_ARRAY       0000000000600df8  00000df8
       0000000000000008  0000000000000000  WA       0     0     8
  [21] .jcr              PROGBITS         0000000000600e00  00000e00
       0000000000000008  0000000000000000  WA       0     0     8
  [22] .dynamic          DYNAMIC          0000000000600e08  00000e08
       00000000000001f0  0000000000000010  WA       6     0     8
  [23] .got              PROGBITS         0000000000600ff8  00000ff8
       0000000000000008  0000000000000008  WA       0     0     8
  [24] .got.plt          PROGBITS         0000000000601000  00001000
       0000000000000030  0000000000000008  WA       0     0     8
  [25] .data             PROGBITS         0000000000601030  00001030
       0000000000000010  0000000000000000  WA       0     0     8
  [26] .bss              NOBITS           0000000000601040  00001040
       0000000000000008  0000000000000000  WA       0     0     1
Skip ......
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing requiredo (OS specific), p (processor specific)

从输出结果中可以看出:

2)反汇编可执行文件 main_mix 的 .plt section

$ objdump -d main_mix

输出结果为:

; Skip ......

Disassembly of section .plt:

00000000004005a0 <_Z3addii@plt-0x10>:
  4005a0:    ff 35 62 0a 20 00       pushq  0x200a62(%rip)        # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
  4005a6:    ff 25 64 0a 20 00       jmpq   *0x200a64(%rip)        # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
  4005ac:    0f 140 00             nopl   0x0(%rax)

00000000004005b0 <_Z3addii@plt>:
  4005b0:    ff 25 62 0a 20 00       jmpq   *0x200a62(%rip)        # 601018 <_GLOBAL_OFFSET_TABLE_+0x18>
  4005b6:    68 00 00 00 00          pushq  $0x0
  4005bb:    e9 e0 ff ff ff          jmpq   4005a0 <_init+0x20>

00000000004005c0 <_Z3subii@plt>:
  4005c0:    ff 25 5a 0a 20 00       jmpq   *0x200a5a(%rip)        # 601020 <_GLOBAL_OFFSET_TABLE_+0x20>
  4005c6:    68 01 00 00 00          pushq  $0x1
  4005cb:    e9 d0 ff ff ff          jmpq   4005a0 <_init+0x20>

00000000004005d0 <__libc_start_main@plt>:
  4005d0:    ff 25 52 0a 20 00       jmpq   *0x200a52(%rip)        # 601028 <_GLOBAL_OFFSET_TABLE_+0x28>
  4005d6:    68 02 00 00 00          pushq  $0x2
  4005db:    e9 c0 ff ff ff          jmpq   4005a0 <_init+0x20>

; Skip ......

从输出结果中可以看出:

step 4: 分析第一次调用由共享库 libadd_debug.so 定义的 add() 函数的执行流程

1)使用 gdb(使用了GEF插件)运行测试程序——main_mix

$ gdb -q main_mix
gef➤ start

2)反汇编 main() 函数,并在在第一次调用 add() 函数处打断点

gef➤  disas                            ; a)反汇编 main() 函数
Dump of assembler code for function main():
   0x00000000004006e6 <+0>:    push   rbp
   0x00000000004006e7 <+1>:    mov    rbp,rsp
=> 0x00000000004006ea <+4>:    mov    esi,0xc
   0x00000000004006ef <+9>:    mov    edi,0xb
   0x00000000004006f4 <+14>:    call   0x4005b0 <_Z3addii@plt>
   0x00000000004006f9 <+19>:    mov    esi,0xc
   0x00000000004006fe <+24>:    mov    edi,0xb
   0x0000000000400703 <+29>:    call   0x4005b0 <_Z3addii@plt>
   0x0000000000400708 <+34>:    mov    esi,0xd
   0x000000000040070d <+39>:    mov    edi,0xf
   0x0000000000400712 <+44>:    call   0x4005c0 <_Z3subii@plt>
   0x0000000000400717 <+49>:    mov    esi,0xd
   0x000000000040071c <+54>:    mov    edi,0xf
   0x0000000000400721 <+59>:    call   0x4005c0 <_Z3subii@plt>
   0x0000000000400726 <+64>:    mov    eax,0x0
   0x000000000040072b <+69>:    pop    rbp
   0x000000000040072c <+70>:    ret
End of assembler dump.
gef➤  b *0x00000000004006f4            ; b)在第一次调用 add() 函数处打断点

3)反汇编 <_Z3addii@plt>,并打断点

gef➤  x/30x4005b0                    ; c)反汇编 add() 函数对应的 PLT 条目中的代码
   0x4005b0 <_Z3addii@plt>:    jmp    QWORD PTR [rip+0x200a62]        # 0x601018
   0x4005b6 <_Z3addii@plt+6>:    push   0x0
   0x4005bb <_Z3addii@plt+11>:    jmp    0x4005a0
gef➤  b *0x4005b0                    ; d)在 add() 函数对应的 PLT 条目的第一条指令处打断点
Breakpoint 5 at 0x4005b0
gef➤  b *0x4005b6                    ; e)在 add() 函数对应的 PLT 条目的第二条指令处打断点
Breakpoint 6 at 0x4005b6
gef➤  b *0x4005bb                    ; f)在 add() 函数对应的 PLT 条目的第三条指令处打断点
Breakpoint 7 at 0x4005bb

4)继续执行程序,add() 函数对应的 PLT 条目的第一条指令处的断点被击中

gef➤  c
gef➤  disas
Dump of assembler code for function _Z3addii@plt:
=> 0x00000000004005b0 <+0>:    jmp    QWORD PTR [rip+0x200a62]        # 0x601018
   0x00000000004005b6 <+6>:    push   0x0
   0x00000000004005bb <+11>:    jmp    0x4005a0
End of assembler dump.

5)计算 add() 函数对应的 PLT 条目的第一条指令要跳转的目的地址。验证“初始时,每个GOT条目(这里为.got.plt[3])都指向对应PLT条目的第二条指令”。

gef➤  p /x 0x00000000004005b6+0x200a62
$1 = 0x601018                        ; g)下一条指令的地址(0x4005b6)与 0x200a62 相加的结果为 0x601018(指向 .got.plt[3])
gef➤  x/gx 0x601018
0x601018:    0x00000000004005b6      ; h)要跳转的目的地址为 0x4005b6,即该 PLT 条目的第二条指令的地址

6)继续执行程序

gef➤  c
gef➤  disas
Dump of assembler code for function _Z3addii@plt:
   0x00000000004005b0 <+0>:    jmp    QWORD PTR [rip+0x200a62]        # 0x601018
=> 0x00000000004005b6 <+6>:    push   0x0                          ; i)可以看到确实是跳转到了该 PLT 条目的第二条指令
   0x00000000004005bb <+11>:    jmp    0x4005a0
End of assembler dump.

7)验证“add() 函数的 PLT 条目的第三条指令的作用为:跳转到动态链接器中的解析函数”

gef➤  x/3i 0x4005a0                                                ; j)反汇编 add() 函数的 PLT 条目的第三条指令要跳转的代码
   0x4005a0:    push   QWORD PTR [rip+0x200a62]        # 0x601008
   0x4005a6:    jmp    QWORD PTR [rip+0x200a64]        # 0x601010
   0x4005ac:    nop    DWORD PTR [rax+0x0]
gef➤  p /x 0x4005ac+0x200a64
$2 = 0x601010
gef➤  x/gx 0x601010
0x601010:    0x00007ffff7dee870
gef➤  x/gx 0x00007ffff7dee870
0x7ffff7dee870 <_dl_runtime_resolve_avx>:    0xe0e48348e3894853      ; k)add() 函数的 PLT 条目的第三条指令实际跳转到了 _dl_runtime_resolve_avx 函数中
gef➤  xinfo 0x7ffff7dee870                                        ; l)查看地址0x7ffff7dee870(_dl_runtime_resolve_avx)的信息
───────────────────────────────────────────────────────────────────────────────── xinfo: 0x7ffff7dee870 ─────────────────────────────────────────────────────────────────────────────────
Page: 0x00007ffff7dd7000  →  0x00007ffff7dfd000 (size=0x26000)
Permissions: r-x
Pathname: /lib/x86_64-linux-gnu/ld-2.23.so        ; m)可以看到,_dl_runtime_resolve_avx函数确实是由动态链接器 ld-2.23.so 定义的
Offset (from page): 0x17870
Inode: 12325294
Segment: .text (0x00007ffff7dd7ac0-0x00007ffff7df5850)
Symbol: _dl_runtime_resolve_avx

8)验证“动态链接器确定该函数(这里为 add() 函数)的运行时位置,用这个地址(这里为 0x7ffff7bd5640)重写其GOT条目(这里为.got.plt[3])”

gef➤  disas main
Dump of assembler code for function main():
   0x00000000004006e6 <+0>:    push   rbp
   0x00000000004006e7 <+1>:    mov    rbp,rsp
   0x00000000004006ea <+4>:    mov    esi,0xc
   0x00000000004006ef <+9>:    mov    edi,0xb
   0x00000000004006f4 <+14>:    call   0x4005b0 <_Z3addii@plt>
   0x00000000004006f9 <+19>:    mov    esi,0xc
   0x00000000004006fe <+24>:    mov    edi,0xb
   0x0000000000400703 <+29>:    call   0x4005b0 <_Z3addii@plt>
   0x0000000000400708 <+34>:    mov    esi,0xd
   0x000000000040070d <+39>:    mov    edi,0xf
   0x0000000000400712 <+44>:    call   0x4005c0 <_Z3subii@plt>
   0x0000000000400717 <+49>:    mov    esi,0xd
   0x000000000040071c <+54>:    mov    edi,0xf
   0x0000000000400721 <+59>:    call   0x4005c0 <_Z3subii@plt>
   0x0000000000400726 <+64>:    mov    eax,0x0
   0x000000000040072b <+69>:    pop    rbp
   0x000000000040072c <+70>:    ret
End of assembler dump.
gef➤  b *0x00000000004006f9                                    ; n)在第一次调用 add() 函数后面的第一条指令(0x4006f9)处打断点
Breakpoint 8 at 0x4006f9: file main_mix.cpp, line 7.
gef➤  c
gef➤  c
gef➤  disas
Dump of assembler code for function main():
   0x00000000004006e6 <+0>:    push   rbp
   0x00000000004006e7 <+1>:    mov    rbp,rsp
   0x00000000004006ea <+4>:    mov    esi,0xc
   0x00000000004006ef <+9>:    mov    edi,0xb
   0x00000000004006f4 <+14>:    call   0x4005b0 <_Z3addii@plt>
=> 0x00000000004006f9 <+19>:    mov    esi,0xc              ; o)0x4006f9 处的断点被击中
   0x00000000004006fe <+24>:    mov    edi,0xb
   0x0000000000400703 <+29>:    call   0x4005b0 <_Z3addii@plt>
   0x0000000000400708 <+34>:    mov    esi,0xd
   0x000000000040070d <+39>:    mov    edi,0xf
   0x0000000000400712 <+44>:    call   0x4005c0 <_Z3subii@plt>
   0x0000000000400717 <+49>:    mov    esi,0xd
   0x000000000040071c <+54>:    mov    edi,0xf
   0x0000000000400721 <+59>:    call   0x4005c0 <_Z3subii@plt>
   0x0000000000400726 <+64>:    mov    eax,0x0
   0x000000000040072b <+69>:    pop    rbp
   0x000000000040072c <+70>:    ret
End of assembler dump.
gef➤  x/gx 0x601018                                        ; p)在_dl_runtime_resolve_avx函数执行完后,查看 .got.plt[3] 的值
0x601018:    0x00007ffff7bd5640
gef➤  x/gx 0x00007ffff7bd5640
0x7ffff7bd5640 <add(int, int)>:    0x89fc7d89e5894855      ; q)可以看到 .got.plt[3] 的值变成了 add() 函数的运行时地址
gef➤  x/i 0x00007ffff7bd5640
   0x7ffff7bd5640 <add(int, int)>:    push   rbp          ; r).got.plt[3] 的值确实指向 add() 函数的运行时地址

step 5: 分析后续再调用 add() 函数的执行流程

gef➤  c
gef➤  disas
Dump of assembler code for function main():
   0x00000000004006e6 <+0>:    push   rbp
   0x00000000004006e7 <+1>:    mov    rbp,rsp
   0x00000000004006ea <+4>:    mov    esi,0xc
   0x00000000004006ef <+9>:    mov    edi,0xb
   0x00000000004006f4 <+14>:    call   0x4005b0 <_Z3addii@plt>
   0x00000000004006f9 <+19>:    mov    esi,0xc
   0x00000000004006fe <+24>:    mov    edi,0xb
=> 0x0000000000400703 <+29>:    call   0x4005b0 <_Z3addii@plt>
   0x0000000000400708 <+34>:    mov    esi,0xd
   0x000000000040070d <+39>:    mov    edi,0xf
   0x0000000000400712 <+44>:    call   0x4005c0 <_Z3subii@plt>
   0x0000000000400717 <+49>:    mov    esi,0xd
   0x000000000040071c <+54>:    mov    edi,0xf
   0x0000000000400721 <+59>:    call   0x4005c0 <_Z3subii@plt>
   0x0000000000400726 <+64>:    mov    eax,0x0
   0x000000000040072b <+69>:    pop    rbp
   0x000000000040072c <+70>:    ret
End of assembler dump.
gef➤  x/3i 0x4005b0
   0x4005b0 <_Z3addii@plt>:    jmp    QWORD PTR [rip+0x200a62]        # 0x601018
   0x4005b6 <_Z3addii@plt+6>:    push   0x0
   0x4005bb <_Z3addii@plt+11>:    jmp    0x4005a0
gef➤  x/gx 0x4005b6+0x200a62
0x601018:    0x00007ffff7bd5640
gef➤  x/i 0x00007ffff7bd5640
   0x7ffff7bd5640 <add(int, int)>:    push   rbp          ; s)可以看出,后续再调用 add() 函数时,其对应的 PLT 条目中的第一条指令会直接跳转到 add() 函数的运行时地址

step 6: 进一步研究

如果可执行文件和共享库都调用由共享库定义的函数时,验证“如果一个目标模块(包括共享模块)调用定义在共享库中的任何函数,那么它就有自己的GOT和PLT。”。


下一篇:计算机系统篇之链接(10):.bss、.data 和 .rodata sections 之间的区别

上一篇:计算机系统篇之链接(8):位置无关代码(中)——真正理解 PIC 数据引用的工作原理(Linux X86-64 示例)

首页