查看原文
其他

32位elf格式中的10种重定位类型

xinpoo 看雪学院 2019-05-26

我之前在另外一个论坛发过一篇这样的主题,但是当时还剩下一些疑问没有想清楚,最近利用业余时间再次学习了ellf格式,针对10种重定位类型重新做了总结,希望分享出来,可以带给初学者一点帮助。


首先需要知道的是,一个程序从源码到被执行,当中经历了3个过程:


  • 编译:将.c文件编译成.o文件,不关心.o文件之间的联系;

  • 静态链接:将所有.o文件合并成一个.so或a.out文件,处理所有.o文件节区在目标文件中的布局;

  • 动态链接:将.so或a.out文件加载到内存,处理加载文件在的内存中的布局。

        

    

C程序中引用全局变量的语句,经过编译得到的机器码会包含一个地址值部分,机器码执行时,该值必须为变量在内存中的绝对地址,调用函数的语句,经过编译得到的机器码也包含一个地址值部分,机器码执行时,该值必须为内存中函数地址与下一条指令地址的偏移。


但是在编译、静态链接,甚至动态链接之后,该地址值部分可能暂时无法满足最终要求,从而必须相应设置一个重定项,要求后续过程对该值进行修改,重定项一方面标记了地址值的位置,另一方面提供了计算正确地址值的方法和计算参数。


对于局部变量的使用,由于程序执行时,esp寄存器保存的一定是栈顶的内存地址,那么从逻辑上讲,编译阶段就可以确定所有局部变量运行时的内存地址,所以不需要设置重定项。


另外,elf格式中设计了10种不同的重定位类型,是由于不同场合,对地址值进行重新计算的方法和参数不同:


  • 引用变量的指令中,需要使用变量的绝对地址,而函数调用指令,需要使用函数与下一条指令地址的相对地址。

  • -fPIC编译选项,可以决定物理内存中的同一份.so镜像,是否可以被多个进程共享。

  • 静态ld是将.o文件按节区"撕开",将各个.o文件中相同类型的节,合并为.so或a.out文件中的一个段,而动态ld则是维持.so文件"原状",合并到进程的虚拟内存空间。

     

   

 具体来讲,以下表格包含了生成各种类型重定项的情况:

    


  • 全局变量,在不加-fPIC编译生成的.o文件中,每个引用处对应一个R_386_32重定位项,非static全局变量,在不加-fPIC编译生成的.so文件中,每个引用处对应一个R_386_32重定位项;

  • static全局变量,在不加-fPIC编译生成的.so文件中,每个引用处对应一个R_386_RELATIVE重定位项;

  • 非static全局变量,在加-fPIC编译生成的.o文件中,每个引用处对应一个R_386_GOT32重定位项;

  • static全局变量,在加-fPIC编译生成的.o文件中,每个引用处对应一个R_386_GOTOFF重定位项;

  • 非static全局变量,在加-fPIC编译生成的.so文件中,每个引用处对应一个R_386_GOLB_DAT重定位项;

  • a.out中利用extern引用.so中的变量,每个引用处对应一个R_386_COPY重定位项;

  • 非static函数,在不加-fPIC编译生成的.o和.so文件中,每个调用处对应一个R_386_PC32重定位项;

  • 非static函数,在加-fPIC编译生成的.o文件中,每个调用处对应一个R_386_PLT32重定位项;

  • 非static函数,在加-fPIC编译生成的.so文件中,每个调用处对应一个R_386_JMP_SLOT重定位项;

  • 全局变量,在加-fPIC编译生成的.o文件中,会额外生成R_386_PC32和R_386_GOTPC重定位项,非static函数,在加-fPIC编译生成的.o文件中,也会额外 生成R_386_PC32和R_386_GOTPC重定位项。


1.  R_386_32


    公式:S+A

    S:重定项中VALUE成员所指符号的内存地址

    A:被重定位处原值,表示"引用符号的内存地址"与S的偏移


// g.c
extern int g1;
int g2;
int g3 = 0x03030303;
static int g4;
static int g5 = 0x05050505;

void fun(int a[5])
{
   a[0] = g1;
   a[1] = g2;
   a[2] = g3;
   a[3] = g4;
   a[4] = g5;
}

   

 将g.c编译成g.o文件,观察包含的重定项信息:

   


  • "00000005 R_386_32 g1":编译器连g1在哪个.o文件都不知道,当然更不知道g1运行时的地址,所以在g.o文件中设置一个重定项,要求后续过程根据"S(g1内存地址)+A(0)",修改g.o镜像中0x05偏移处的值;

  • "0000002f R_386_32 .bss":g4在g.o文件.bss节的0偏移处(由于加载时必然知道.bss的内容为全0,就是说elf文件只需要记录.bss的位置和大小,不需要安排空间记录.bss内容,而且就算文件中为.bss节安排了空间,也无法区分g4在.bss节的什么位置,所以g4在.bss节中的偏移,要通过查看.bss节起始位置和g4符号的位置来验证),要求后续过程根据"S(g.o镜像中.bss的内存地址)+A(0)",修改g.o镜像中0x2f偏移处的值;

  • "0000003c R_386_32 .data":g5在g.o文件.data节的0x04偏移处,要求后续过程根据"S(g.o镜像中.data的内存地址)+A(0x04)",修改g.o镜像中0x3c偏移处的值;

  • "00000015 R_386_32 g2":g2在g.o文件的.bss节,要求后续过程根据"S(g2内存地址)+A(0)",修改g.o镜像中0x15偏移处的值;

  • "00000022 R_386_32 g3":g3在g.o文件的.data节,要求后续过程根据"S(g3内存地址)+A(0)",修改g.o镜像中0x22偏移处的值。


g1与g4/g5重定项区别:当前没有g1位置的任何线索,所以希望延迟到加载时,通过搜索动态符号表确定g1的内存地址,而g4/g5在g.o的.bss/.data节中,并且有static属性,不可能被外部引用,加载到内存必然还在g.o镜像的.bss/.data节中,所以编译器使用.bss/.data作为重定位计算参数,可以避免后续过程搜索动态符号表,提高重定位效率;

    

g2/g3与g4/g5重定项区别:g2/g3虽然和g4/g5一样,也在g.o的.bss/.data节中,但g2/g3可以被外部引用,在一种特殊情况下,g2/g3会被安排到其它地方,如果仍然使用在g.o镜像中.bss/.data的地址进行重定位,就会导致进程运行的逻辑错误,稍后介绍R_386_COPY类型时,会详细说明。


2.  R_386_RELATIVE

    公式:B+A

    B:.so文件加载到内存中的基地址

    A:被重定位处原值,表示引用符号在.so文件中的偏移

    将上述g.o文件,链接成libg.so文件,重定位信息如下:

    

  • "00000560 R_386_32 g1":任然没有g1位置的任何线索,所以重定项保持原有的计算方法和参数;

  • "00000570 R_386_32 g2":不确定是否需要放弃.bss中的位置,所以仍然使用g2的内存地址进行重定位计算;

  • "0000057d R_386_32 g3":不确定是否需要放弃.data中的位置,所以仍然使用g3的内存地址进行重定位计算;

  • "0000058a R_386_RELATIVE *ABS*":.so文件.bss段的第一项用于保存.bss本身的位置,g.o的.bss节被安排在了libg.so的0x2024处,所以静态ld根据g.o中的R_386_32重定项,进一步精确了g4在libg.so的0x2024偏移处,但g4的内存地址,还需要加上libg.so的加载地址,所以重定位类型转换为R_386_RELATIVE;

  • "00000597 R_386_RELATIVE *ABS*":.so文件.data段的第一项用于保存.data本身的位置,g.o的.bss节被安排在了libg.so的0x2018处,所以静态ld根据g.o中的R_386_32重定项,进一步精确了g4在libg.so的0x201c偏移处,但g5的内存地址,还需要加上libg.so的加载地址,所以重定位类型转换为R_386_RELATIVE。

3.  R_386_COPY

    公式:无


// g.c
int g = 1;

// main.c
extern int g;

void fun(int *a)
{
   *a = g;
}

int main()
{
   return 0;
}

将g.c编译为libg.so,main.c编译为a.out,由于a.out引用了libg.so中的全局变量g,从而可以出现说明R_386_32类型时提到的特殊情况:

    


a.out中使用了libg.so中的全局变量g,这样就必须等到执行阶段,确定了libg.so在进程空间的位置后,才能知道g的绝对地址,不细想的话,可能会认为通过设置一个R_386_32或R_386_RELATIVE重定项,就能解决问题了。

    

但遗憾的是,a.out的.text段,不可以有重定项:

    

    

上图希望展示的是,在一个进程的创建过程中,a.out是最先映射到该进程的虚拟空间,然后才会映射所依赖的.so。换句话说,在a.out加载的时候,仍然不知道g的地址,而如果等加载libg.so时再处理重定项,虽然知道g的地址了,但a.out的.text段所在内存页,这时已经被设置为只读,也无法进行重定位。

    

所以,针对这种情况,静态ld会将g转移到a.out的.bss段。由于a.out的加载地址,是在静态链接阶段就确定的(通过链接脚本设置,32位系统默认设置为0x8048000),从而静态ld也可以知道g的运行时地址,那么就不需要重定项了,但同时又带来2个新的问题:

    

a. 毕竟libg.so中的g才是是原生的,怎么保证遵循libg.so中g的初始值?

        

其实这就是设计R_386_COPY类型的用意,它表示让动态ld加载libg.so时知道g的初始值后,将值复制到内存中a.out的.bss段。

         

但是如果再仔细想想,其实静态链接阶段,就有机会从libg.so中读取g的初始值,并且如果不将g安排在a.out的.bss段,而是安排在.data段,存储空间也具备了,按道理就不需要R_386_COPY类型了。个人猜测,可能是设计者本着.data只存储显式赋初值的变量的原则,而没有这样实现。


b. g既然已经转移到新地地方了,怎么保证lig.so和a.out的.text段使用同一处的g?

        

分析R_386_32类型时,已经看到g2/g3和g4/g5一样,分别在g.o的.bss/.data节,重定项中却仍然使用g2/g3作为计算参数,其实就是为了在这种情况下,放弃使用本身.bss/.data段中的g,而使用a.out中的g。


4.  R_386_PC32

    公式:S+A-P

    S:重定项中VALUE成员所指符号的内存地址

    A:被重定位处原值,表示"被重定位处"与"下一条指令"的偏移

    P:被重定位处的内存地址


// f.c
extern void f1();
void f2() {}
static void f3() {}

void fun()
{
   f1();
   f2();
   f3();
}


将f.c编译成f.o文件,观察包含的重定项信息:

    


  • "00000011 R_386_PC32 f1":编译器连f1指令块在哪个.o文件都不知道,当然更不知道f1运行时的地址,所以在g.o文件中设置一个重定项,要求后续过程根据"S(f1内存地址)+A(-4)-P(被重定位处内存地址)",即"S(f1内存地址)-(p+4)(下一条指令内存地址)",修改g.o文件中0x11偏移处的值;

  • "00000016 R_386_PC32 f2":f2类似分析R_386_32类型时的g2/g3,虽然在g.o文件,但有可能被外部调用,所以和f1一样,编译器在g.o文件中设置一个重定项,要求后续过程根据"S(f2内存地址)+A(-4)-P(被重定位处内存地址)",即"S(f2内存地址)-(p+4)(下一条指令内存地址)",修改g.o文件中的0x16偏移处的值。


由于调用函数的指令中,要求的是相对地址,并且编译阶段就能确定f3()与fun()的偏移,即"f3加载地址(B+0x05)-下一条指令内存地址(B+1f)=0xe6ffffff",加载到内存也不会发生改变,所以0x1b处不需要被重定位。


将上述f.o文件,链接为libf.so,静态ld无法对R_386_PC32重定项做进一步处理,这样,加载时动态ld会通过搜索动态符号表,确定libf.so镜像中0x53c/0x541处的地址值,保证运行时能调用到到f1()/f2()函数:

    


5.  R_386_GOTPC

    公式:GOT+A-P

    GOT:运行时,.got段的结束地址

    A:被重定位处原值,表示"被重定位处"在机器码中的偏移

    P:被重定位处的内存地址

    

// g.c
extern int g1;

void fun(int a[1])
{
   a[0] = g1;
}


由于程序执行时,eip寄存器保存的一定是当前指令的内存地址,虽然eip寄存器不直接提供给软件使用,但是有间接的方法可以获取,那么从逻辑上讲,所有跟代码区有固定偏移的内容,编译和静态链接阶段,就可以确定它们的内存地址。利用这个特点,可以将代码区中的被重定位处转移出去,加-fPIC选项将g.c编译为g.o,并链接为libg.so,可以验证这一点:

    

    

g.o中包含3个重定项:

  • "00000004 R_386_PC32 __x86.get_pc_thunk.cx":R_386_PC32重定位类型已经介绍过了,这条重定项可以保证,运行时当前指令可以调用到编译器自动生成的__x86.get_pc_thunk.cx()函数,由于call指令会将下一条指令内存地址B+0x513压栈,这样从该函数经过一次再回到B+0x513处的指令时,当前指令的内存地址B+0x513就存到了ecx寄存器;

  • "0000000a R_386_GOTPC _GLOBAL_OFFSET_TABLE_":要求静态ld根据"GOT(B+.got结束位置在libg.so中的偏移)+A(2)-P(B+0x515)",即libg.so文件中.got结束位置相对0x515处机器码的偏移,修改g.o文件中0x0a偏移处的值,这样运行时加上ecx寄存器中当前指令的内存地址后,就是.got段结束位置的内存地址;

  • "00000010 R_386_GOT32 g1":要求静态ld在目标文件中生成.got表,并在.got表中安排4字节存储g1地址,这样代码区就可以从.got表中获取g1地址,而.got表运行时的结束地址,以及存储g1地址的位置在.got表中的偏移,静态链接阶段都是知道的,从而就不需要对代码区进行重定位。

    静态ld在libg.so中设置了.got段,并将代码区中的重定位处,转移到.got段中:

  • "00001fec R_386_GLOB_DAT g1":0x1fec处用于保存运行g1的内存地址,但当前没有g1位置的任何线索,所以留下重定项,要求后续过程进行修改。

    

对于R_386_32、R_386_RELATIVE类型的重定项,由于被重定位处在代码区,而重定项计算参数的地址,在不同进程中是不同的,所以不同进程对.so代码区的修改要求就不同,这样就不能共享同一份物理内存中的.so镜像。

    

R_386_GLOB_DAT的优势就在于,它将"散落"在代码区的被重定位处,集中转移到.got表中,从而大大减小了不可共享区域,如下图所示,进程B希望加载.so文件时,发现内存中已经存在该.so的镜像了,就直接映射到自己的虚拟空间,动态ld在处理重定项时,仅需要修改小小的.got段,并通过COW(写时复制)机制,创建了一个.got副本,从而也可以保证与其它进程互不干扰。

    


6.  R_386_GOT32

    公式:G

    G:引用符号的地址指针,相对于GOT的偏移


// g.c
extern int g1;
int g2;
int g3 = 0x03030303;

void fun(int a[3])
{
   a[0] = g1;
   a[1] = g2;
   a[2] = g3;
}


将g.c编译成g.o文件,观察包含的重定项信息:

    


  • "00000010 R_386_GOT32 g1":要求静态ld根据"G(g1地址指针相对GOT的偏移)",修改g.o镜像0x10偏移处的值;

  • "00000023 R_386_GOT32 g2":要求静态ld根据"G(g2地址指针相对GOT的偏移)",修改g.o镜像0x23偏移处的值;

  • "00000033 R_386_GOT32 g3":要求静态ld根据"G(g3地址指针相对GOT的偏移)",修改g.o镜像0x33偏移处的值;


7.  R_386_GOLB_DAT

    公式:S

    S:重定项中VALUE成员所指符号的内存地址

    将上述g.o链接为libg.so文件,发现被重定位处都被集中转移到.got段中:

    


8.  R_386_GOTOFF

    公式:S-GOT

    S:重定项中VALUE成员所指符号的内存地址

    GOT:运行时,.got段的结束地址


// g.c
static int g4;
static int g5 = 0x05050505;

void fun(int a[2])
{
   a[0] = g4;
   a[1] = g5;
}


将g.c编译为g.o文件,并且链接为libg.so文件,观察包含的重定项信息:

    

    

编译阶段不知道g.o中的.bss/.data节会被链接到libg.so中的什么位置,所以设置了R_386_GOTOFF重定项,要求静态ld根据"S(.bss/.data内存地址)-GOT(运行时.got结束地址)",修改被重定位处的值。

    

前面已经介绍过,运行时ecx寄存器保存的一定是.got的结束地址,再加上g4/g5具有static属性,加载到内存后,仍然在libg.so镜像的.bss/.data段中,那么通过g4/g4在.bss/.data中的偏移,以及.bss/.data与.got结束位置的偏移,在静态链接阶段就能知道运行时g4/g5的内存地址,从而libg.so中就不存在对g4/g5引用处的重定项了。


9.  R_386_PLT32

    公式:L+A-P

    L:<重定项中VALUE成员所指符号@plt>的内存地址

    A:被重定位处原值,表示"被重定位处"相对于"下一条指令"的偏移

    P:被重定位处的内存地址


// f.c
extern void f1();
void f2() {}
static void f3() {}

void fun()
{
   f1();
   f2();
   f3();
}


将f.c编译成f.o文件,观察包含的重定项信息:

    

  • "0000001d R_386_PLT32 f1":要求静态ld生成<f1@plt>函数,并根据"L(<f1@plt>函数地址)+A(-4)-P",即<f1@plt>相对于下一条指令的相对地址,修改f.o镜像中0x1d偏移处的值;

  • "00000022 R_386_PLT32 f2":要求静态ld生成<f2@plt>函数,并根据"L(<f2@plt>函数地址)+A(-4)-P",即<f2@plt>相对于下一条指令的相对地址,修改f.o镜像中0x22偏移处的值。

    

可以看出,源码中调用f1()、f2()函数的语句,对应的机器码,并没有直接跳转到f1、f2指令块,而是调用了<f1@plt>、<f2@plt>函数,接下来通过分析R_386_JMP_SLOT类型就会知道,这两个函数相当于"中间跳板",用于实现重定项的延迟处理。


10.R_386_JMP_SLOT

    

公式:S(与R_386_GLOB_DAT的公式一样,但对于动态ld,R_386_JMP_SLOT类型与R_386_RELATIVE等价)

    

S:重定项中VALUE成员所指符号的内存地址

    

将上述f.o,链接为libf.so文件,观察包含的重定项信息:

    

  • "00002018 R_386_JUMP_SLOT f1":被重定位处在libf.so镜像的0x2018偏移处(.got.plt段中),0x567处的指令第一次被执行时,由_dl_runtime_resolve()函数根据"S(f1内存地址)",修改被重定位处的值;

  • "0000200c R_386_JUMP_SLOT f2":被重定位处在libf.so镜像的0x200c偏移处(.got.plt段中),0x56c处的指令第一次被执行时,由_dl_runtime_resolve()函数根据"S(f2内存地址)",修改被重定位处的值;

    R_386_JMP_SLOT是10种类型中最复杂的,必须先要了解.got.plt前三项的含义:

  • 第1项:用于保存.dynamic段的内存地址,初始值为.dynamic段在libg.so文件中的偏移;

  • 第2项:用于保存内存中libf.so模块的id,用于区分各个已加载的.so模块;

  • 第3项:用于保存_dl_runtime_resolve()函数的内存地址,由动态ld完成填写。

    

另外,通过前面的介绍可以知道,ebx寄存器存的一定是.got结束地址B+0x2000,然后按照图中标记的执行顺序,在大脑中连续2次模拟执行B+0x567处的指令,就会看出如下规律:

  • 第一次执行时,B+0x410处的jmp指令,会跳转到B+0x416处(因为0x18(%ebx)指向B+0x2018处,而此处初始值为B+0x416),接着将被重定位处地址压栈,并再跳转到0x3d0将libf.so模块id压栈,最终进入_dl_runtime_resolve()函数,确定f1地址后覆盖到B+0x2018处;

  • 第二次执行时,由于B+0x2018处已经是f1的地址了,从而B+0x410处的jmp指令,就会直接进入f1()函数。

    

对f2()函数的调用同理,这样做虽然会多一次跳转,但是保证了程序执行的平滑性,避免大量调用libg.so中的函数时,在加载.so时出现"卡顿"的现象,而且有时候很多分支根本没有机会被执行,所以这是一种折衷的处理。


参考:

    https://docs.oracle.com/cd/E23824_01/html/819-0690/

    http://www.cnblogs.com/catch/p/3857964.html

    https://www.cnblogs.com/lanrenxinxin/p/5573018.html





看雪ID:xinpoo

bbs.pediy.com/user-813298



本文由看雪论坛 xinpoo 原创

转载请注明来自看雪社区





热门技术文章推荐:








戳原文,看看大家都是怎么说的?

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存