首先需要知道的是,一个程序从源码到被执行,当中经历了3个过程:
c程序中引用全局变量的语句,经过编译得到的机器码会包含一个地址值部分,机器码执行时,该值必须为变量在内存中的绝对地址,调用函数的语句,经过编译得到的机器码也包含一个地址值部分,机器码执行时,该值必须为内存中函数地址与下一条指令地址的偏移。但是在编译、静态链接,甚至动态链接之后,该地址值部分可能暂时无法满足最终要求,从而必须相应设置一个重定项,要求后续过程对该值进行修改,重定项一方面标记了地址值的位置,另一方面提供了计算正确地址值的方法和计算参数。
对于局部变量的使用,由于程序执行时,esp寄存器保存的一定是栈顶的内存地址,那么从逻辑上讲,编译阶段就可以确定所有局部变量运行时的内存地址,所以不需要设置重定项。
另外,elf格式中设计了10种不同的重定位类型,是由于不同场合,对地址值进行重新计算的方法和参数不同:
具体来讲,以下表格包含了生成各种类型重定项的情况:
1. R_386_32
公式:S+A
S:重定项中VALUE成员所指符号的内存地址
A:被重定位处原值,表示"引用符号的内存地址"与S的偏移
将g.c编译成g.o文件,观察包含的重定项信息:
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类型时,会详细说明。
上图希望展示的是,在一个进程的创建过程中,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:被重定位处的内存地址
由于调用函数的指令中,要求的是相对地址,并且编译阶段就能确定f3()与fun()的偏移,即"f3加载地址(B+0x05)-下一条指令内存地址(B+1f)=0xe6ffffff",加载到内存也不会发生改变,所以0x1b处不需要被重定位。
将上述f.o文件,链接为libf.so,静态ld无法对R_386_PC32重定项做进一步处理,这样,加载时动态ld会通过搜索动态符号表,确定libf.so镜像中0x53c/0x541处的地址值,保证运行时能调用到到f1()/f2()函数:
将g.c编译成g.o文件,观察包含的重定项信息:
我之前在另外一个论坛发过一篇这样的主题,但是当时还剩下一些疑问没有想清楚,最近利用业余时间再次学习了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
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;
}
- "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
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()函数:
// f.c
extern void f1();
void f2() {}
static void f3() {}
void fun()
{
f1();
f2();
f3();
}
将f.c编译成f.o文件,观察包含的重定项信息:
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.c
extern int g1;
void fun(int a[1])
{
a[0] = g1;
}
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
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2019-1-28 10:23
被admin编辑
,原因: