前面有篇文章分析了ELF格式,也只是让我们对目标文件有了一个大概的了解,并没有说明一个十分重要的问题:重定位,今天重新看了下重定位的资料,终于弄懂了重定位的过程,下面来做一个分析。
我们将使用下面两个源代码中的文件a.c和b.c展开分析:
//a.c extern int shared; int main() { int a=100; swap(&a,&shared); } //b.c int shared=1; void swap(int *a,int *b) { *a^=*b^=*a^=*b; }
使用命令:gcc -c a.c b.c生成两个可重定位目标文件a.o和b.o,下面就要使用链接器将a.o和b.o链接起来生成执行文件ab,我们分析的就是这个过程。
1.链接的过程
现在的链接器一般都是两步链接,也就是说整个链接过程分两步。
1)符号解析,主要使用ELF里面的符号表节来完成,不做描述。
2)重定位:一旦链接器完成符号解析这一步,它就把代码中的每个符号引用和定义联系起来。在此时,链接器就知道它的输入目标文件中的代码节和数据节的确切大小。现在就可以重定位了,在这个步骤中将合并输入模块,并为每个符号分配运行时地址。重定位由两步组成:
- 重定位节和符号定义:在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节。例如输入文件a.o的.text节和b.o中的.text节合并为可执行文件ab中的.text节。然后链接器将运行时存储器地址赋值给新的聚合节。当这一步完成时,程序中的每个指令和全局变量都有唯一的运行时存储器地址。
- 重定位节中的符号引用:在这一步中,链接器修改代码节和数据节中对每个符号的引用,试的他们指向正确的运行时地址。为了执行这一步,链接器依赖于重定位条目。稍后我们来重点分析这一步骤的具体实现过程。
我们看到上面是a.o的各个段的分布情况,看到VMA列全都是0,VMA就是虚拟地址的意思,说明a.o中的节确实没有分配存储器运行时地址,根据上面描述,可执行文件ab中应该分配了虚拟地址,事实确实证明了这点:
我们发现VMA不再是0。可执行文件的代码段映射到08048094,数据段映射到08049108。
2.重定位的具体实现分析
先来看下重定位条目的内容:
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
0000001c R_386_32 shared
00000027 R_386_PC32 swap
OFFSET的意义:要修改的位置在.text节的偏移量
TYPE:重定位类型
VALUE:重定位符号的名称
R_386_32(绝对寻址修正): objdump -d a.o
18: c7 44 24 04 00 00 00 movl $0x0,0x4(%esp)
1f: 00
movl指令要将shared的地址送到esp+4的位置,但是此时并不知道shared的地址,所以就先放00000000。在重定位条目中的第一项偏移是1c,刚好对应00000000。所以重定位的过程就是修改这个偏移处的内容。其实这个决定寻址修正很简单,只是书上各种符号公式看的让人迷惑。
修正过程:
假设shared符号的运行时地址是0x8049108,即ADDR(shared)=0x8049108。
要该修的位置的地址:就将00000000替换为ADDR(shared)即可。
R_386_PC32(相对寻址xiuzheng):ojbdump -d a.o
26: e8 fc ff ff ff call 27 <main+0x27>
相对寻址的意思就是相对当前IP位置跳转,call实现近转移,当前指令的ip+跳转位移=目标地址
我们先看可执行文件ab的反汇编代码:
也就是说0x8048bf+x=0x80480c8,算得x=9,正好是指令的操作数。
假设P是待修改的位置的虚拟地址,S是符号实际虚拟地址,那么P+4+x=S,所以x=S-P-4,即x=0x8048c8-4-0x8048bb=9.