Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

位置无关代码(PIC) #15

Open
chenpengcong opened this issue Jun 12, 2018 · 0 comments
Open

位置无关代码(PIC) #15

chenpengcong opened this issue Jun 12, 2018 · 0 comments

Comments

@chenpengcong
Copy link
Owner

本文主要研究动态共享库如何实现位置无关,基于如下代码进行研究

本机环境 Linux cike-pc 4.14.0-deepin2-amd64 #1 SMP PREEMPT Deepin 4.14.12-2 (2018-01-06) x86_64 GNU/Linux

extern int global_var;
static int static_var = 6;

void bar();
static void baz();

void foo()
{
    global_var = 66;
    static_var = 66;
    bar();
    baz();
}

static void baz()
{

}

使用fPIC指定编译成位置无关的动态共享库$ gcc -shared -fPIC mydlib.c -o libmydlib.so

该例子对包含了4种类型的地址访问,分别是模块内部函数调用模块内部数据访问模块外部数据访问模块间函数调用

首先,我们反编译出汇编代码。

$ objdump -d libmydlib.so 

00000000000006b0 <foo>:
 6b0:    55                       push   %rbp
 6b1:    48 89 e5                 mov    %rsp,%rbp
 6b4:    48 8b 05 1d 09 20 00     mov    0x20091d(%rip),%rax        # 200fd8 <global_var>
 6bb:    c7 00 42 00 00 00        movl   $0x42,(%rax)
 6c1:    c7 05 5d 09 20 00 42     movl   $0x42,0x20095d(%rip)        # 201028 <static_var>
 6c8:    00 00 00 
 6cb:    b8 00 00 00 00           mov    $0x0,%eax
 6d0:    e8 bb fe ff ff           callq  590 <bar@plt>
 6d5:    b8 00 00 00 00           mov    $0x0,%eax
 6da:    e8 03 00 00 00           callq  6e2 <baz>
 6df:    90                       nop
 6e0:    5d                       pop    %rbp
 6e1:    c3                       retq   

00000000000006e2 <baz>:
 6e2:    55                       push   %rbp
 6e3:    48 89 e5                 mov    %rsp,%rbp
 6e6:    90                       nop
 6e7:    5d                       pop    %rbp
 6e8:    c3                       retq   

模块内部函数调用

baz函数调用指令如下

6da: e8 03 00 00 00 callq 6e2 <baz>

E8是相对偏移调用指令call的指令码
03 00 00 00是目的地址相对于下一条指令的偏移,由于机器是小端的,所以偏移量为0x00000003

所以bar的地址为0x6df + 0x03 = 0x6e2

无论该模块装载到哪个位置,这条指令都是有效的,因为foo和bar的相对位置不变

模块内部数据访问

对定义在模块内部的静态变量static_var访问指令如下

6c1: c7 05 5d 09 20 00 42 movl $0x42,0x20095d(%rip) # 201028 <static_var>

由于对数据的寻址方式往往没有相对与当前指令地址的寻址方式,所以使用指令指针寄存器rip加上一个偏移量来达到目的

模块外部数据访问

定义在模块外部的变量global_var的地址要等到装载时才能确定。

假设global_var定义在模块B,相对.data的起始地址为0x00,当模块B的.data的装载地址为0x10000时,变量global_var的地址为0x10000,这时系统遍历模块libmydlib.so中的定位表,把所有对global_var的地址引用都定位至0x10000。

上述做法存在的问题是:其他进程无法共享使用模块libmydlib.so的指令部分,因为其他进程不一定将模块B装载到与上述装载过程一样的地址,对global_var的访问不再是使用0x10000地址。

解决该问题的基本想法就是把指令中那些需要被修改的部分分离出来,跟数据部分放在一起,这样指令部分就可以保持不变,而数据部分可以在每个进程中拥有一个副本。

ELF使用一个全局偏移表(GOT, Global Offset Table)来记录外部变量的地址,GOT就是一个指向外部变量的指针数组,当代码对外部变量进行访问的时候,先访问该变量在GOT中对应的项,再根据该项的值去访问变量。

由于GOT在编译时就可以确定相对于文件的偏移,因此对GOT的访问可以使用上面的模块内部数据访问的方法,使用rip寄存器加上一个偏移量来访问,然后GOT中记录的变量的实际地址等到装载时再修改,这样就完成了指令变化部分的分离。

这时我们可以来看libmydlib.so对外部变量global_var的访问

 6b4:    48 8b 05 1d 09 20 00     mov    0x20091d(%rip),%rax        # 200fd8 <global_var>
 6bb:    c7 00 42 00 00 00        movl   $0x42,(%rax)

可以看到变量global_var的访问地址保存在0x20091d + (%rip) = 0x200fd8

通过查看段表$ readelf -S libmydlib.so

  [20] .got PROGBITS 0000000000200fd0 00000fd0
       0000000000000030 0000000000000008 WA 0 0 8

可知.got段的地址范围为0x200fd0 ~ 0x2001000

因此对0x200fd8的访问实际就是访问GOT,且具体访问的是GOT的第二项

查看动态重定位表$ readelf -r libmdlib.so

readelf -r libmdlib.so 

Relocation section '.rela.dyn' at offset 0x478 contains 9 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000200df8  000000000008 R_X86_64_RELATIVE                    680
000000200e00  000000000008 R_X86_64_RELATIVE                    640
000000201020  000000000008 R_X86_64_RELATIVE                    201020
000000200fd0  000100000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_deregisterTMClone + 0
000000200fd8  000300000006 R_X86_64_GLOB_DAT 0000000000000000 global_var + 0
000000200fe0  000400000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
000000200fe8  000500000006 R_X86_64_GLOB_DAT 0000000000000000 _Jv_RegisterClasses + 0
000000200ff0  000600000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_registerTMCloneTa + 0
000000200ff8  000700000006 R_X86_64_GLOB_DAT 0000000000000000 __cxa_finalize@GLIBC_2.2.5 + 0

可知偏移200fd8的重定位入口最终会被重定位,被修改为global_var的实际地址

模块间函数调用

模块间函数调用的原理与模块间数据访问类似,将指令中变化的部分(外部函数地址)分离出来,用一个.got.plt表来存放函数引用的地址。同时,为了实现延迟绑定,调用函数并不直接通过.got.plt跳转,而是通过一个叫作PLT项的结构来进行跳转,每个外部函数在PLT中都有一个相应的项,比如bar函数对应bar@plt

0000000000000580 <.plt>:
 580:    ff 35 82 0a 20 00     pushq 0x200a82(%rip) # 201008 <_GLOBAL_OFFSET_TABLE_+0x8>
 586:    ff 25 84 0a 20 00     jmpq *0x200a84(%rip) # 201010 <_GLOBAL_OFFSET_TABLE_+0x10>
 58c:    0f 1f 40 00     nopl 0x0(%rax)

0000000000000590 <bar@plt>:
 590:    ff 25 82 0a 20 00     jmpq *0x200a82(%rip) # 201018 <bar>
 596:    68 00 00 00 00     pushq $0x0
 59b:    e9 e0 ff ff ff     jmpq 580 <.plt>

bar@plt第一条指令进行跳转,跳转地址保存在0x201018

查看.got.plt段内容readelf -x .got.plt libmydlib.so

$ readelf -x .got.plt libmydlib.so 

Hex dump of section '.got.plt':
  0x00201000 100e2000 00000000 00000000 00000000 .. .............
  0x00201010 00000000 00000000 96050000 00000000 ................

0x201018是.got.plt的第四项,该项的值为0x0596(小端),刚好是下一条指令的地址,所以bar@plt第一条指令就是跳转到下一条指令

pushq $0x0将0压栈,0是bar这个符号在重定位表.rel.plt中的下标

$ readelf -r libmydlib.so 
...
Relocation section '.rela.plt' at offset 0x550 contains 1 entries:
  Offset Info Type Sym. Value Sym. Name + Addend
000000201018 000200000007 R_X86_64_JUMP_SLO 0000000000000000 bar + 0

接下来将地址0x201008的内容入栈,再进行跳转,跳转地址保存在0x201010,这两个地址分别对应.got.plt的第1项.got.plt[1]和第2项.got.plt[2]

 580:    ff 35 82 0a 20 00     pushq 0x200a82(%rip) # 201008 <_GLOBAL_OFFSET_TABLE_+0x8>
 586:    ff 25 84 0a 20 00     jmpq *0x200a84(%rip) # 201010 <_GLOBAL_OFFSET_TABLE_+0x10>

从上面.got.plt段的内容可以看到这两项的值都为0。

实际上当动态链接器(ld-linux-XXX)执行时才对这两项进行填充,.got.plt[1]存放的是该共享模块的ID,.got.plt[2]存放的是_dl_runtime_resolve函数的地址。

_dl_runtime_resolve函数通过上面两条圧栈指令拿到待决议符号bar在重定位表的下标和模块ID这两个参数就可以完成符号解析和重定位工作,与此同时,_dl_runtime_resolve函数会将bar符号的真正地址填入到.got.plt表中,下次执行bar@plt的第一条指令就直接跳转到bar函数,不再进行符号解析和重定位工作。

PLT的执行过程可以使用GDB进行调试查看,参考再议 PLT 与 GOT这篇文章

拓展阅读
全局符号介入问题

参考
《程序员的自我修养》
Linux动态链接中的PLT和GOT

@chenpengcong chenpengcong changed the title 位置无关代码(PIC).md 位置无关代码(PIC) Feb 7, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant