一、背景
ld将.o连接为.so或者可执行程序,以及可执行程序使用.so时,都会遇到函数重定位的问题,本文对该问题进行分析。
二、静态连接
代码示例:
x.c:
#include <stdio.h>
void foo()
{
printf("foo\n");
}
main.c:
extern void foo(void);
int main(void)
{
foo();
return 0;
}
Makefile:
all: main
main: main.o x.o
$(CC) -m32 -o $@ $^
main.o: main.c
$(CC) -m32 -c -o $@ $<
x.o: x.c
$(CC) -m32 -c -o $@ $<
clean:
rm -f main main.o x.o
调用make进行编译,得到x86 32bit版本的.o和可执行程序
objdump -d main.o得到:
00000000 <main>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 e4 f0 and $0xfffffff0,%esp
6: e8 fc ff ff ff call 7 <main+0x7>
b: b8 00 00 00 00 mov $0x0,%eax
10: c9 leave
11: c3 ret
0xfffffffc是-4的补码, e8是相对跳转指令,e8 fc ff ff ff所跳转的位置,不和任何函数对应,是个假的位置。
这里应当填入的是foo函数的相对地址,但是我们在编译main.o时,foo是外部函数,无法得知foo的地址,所以使用了0xfffffffc这个假地址做代替,等连接时确定foo函数的地址后,再替换这个假地址。
objdump -d main得到:
08048404 <main>:
8048404: 55 push %ebp
8048405: 89 e5 mov %esp,%ebp
8048407: 83 e4 f0 and $0xfffffff0,%esp
804840a: e8 09 00 00 00 call 8048418 <foo>
804840f: b8 00 00 00 00 mov $0x0,%eax
8048414: c9 leave
8048415: c3 ret
8048416: 66 90 xchg %ax,%ax
08048418 <foo>:
8048418: 55 push %ebp
8048419: 89 e5 mov %esp,%ebp
804841b: 83 ec 18 sub $0x18,%esp
804841e: c7 04 24 00 85 04 08 movl $0x8048500,(%esp)
8048425: e8 16 ff ff ff call 8048340 <puts@plt>
804842a: c9 leave
804842b: c3 ret
804842c: 8d 74 26 00 lea 0x0(%esi,%eiz,1),%esi
可以看到0xfffffffc这个假地址,已经被替换为0x00000009了。e8相对地址调用0x00000009,会call到0x804840f+0x00000009=0x8048418这个位置,也就是foo的地址。
那么,main.o连接为main时,到底发生了什么?
2.1 .rel.text .rel.data段
.o中有两个段:.rel.text .rel.data,用于连接时,分别处理函数和数据的重定位的问题。
这里只介绍函数的处理,数据的类似,不再赘述。
.rel.text对应的数据结构为:
typedef struct elf32_rel {
Elf32_Addr r_offset;
Elf32_Word r_info;
} Elf32_Rel;
r_offset,重定位入口的偏移,对于.o来说,是需要修正的位置的第一个字节相对于段起始的偏移;对于.so和可执行程序来说,是需要修正的位置的第一个字节的虚拟地址。
r_info,重定位入口的类型和符号,前三个字节是该入口的符号在符号表中的下标;后一个字节,表示重定位的类型,比如R_386_32、R_386_PC32。
readelf -r main.o得到:
Relocation section '.rel.text' at offset 0x3a0 contains 1 entries:
Offset Info Type Sym.Value Sym. Name
00000007 00000902 R_386_PC32 00000000 foo
6: e8 fc ff ff ff
一致,需要将0xfffffffc替换为foo的相对地址
r_info为0x00000902,0x000009表示该入口符号,在符号表中的下标为0x000009,readelf -s main.o | grep 9:
9: 00000000 0 NOTYPE GLOBAL DEFAULT UND foo
可以看到这个入口处理的是foo这个函数
0x02表示重定位的类型为R_386_PC32
2.2 指令修正
ld在将main.o,x.o连接为main时,可以获得foo的实际地址为0x08048418,然后根据.rel.text中的重定位信息,进行指令修正。
ld在处理到.rel.text中的foo时,根据r_info中的0x000009可以得知,需要处理foo这个符号,foo的实际地址为0x08048418。
根据r_info中的0x02可以得知,处理方式为R_386_PC32,R_386_PC32表示相对寻址修正S+A-P。其中
A = 保存在被修正位置的值。
被修正位置为0x00000007,这个位置的值是0xfffffffc,所以A为0xfffffffc,即A为-4。
P = 被修正的位置,(相对于段开始的位置或者虚拟地址),可以通过r_offset计算得到。
r_offset为0x00000007,当连接为可执行程序时,应该用被修正位置的虚拟地址,也就是0x0804840b(objdump -d main看到被修正位置的虚拟地址为0x0804840a + 1),所以P为0x0804840b。
S = 符号的实际地址,通过r_info中前三个字节计算得到。
r_info前三个字节为0x000009,在readelf -s main.o可以查到是foo这个符号,其实际地址为0x08048418,S为0x08048418。
S+A-P = 0x08048418 + (-4) - 0x0804840b = 0x00000009,这个就是修正后的值,用它来覆盖0x0804840b这个位置,得到
804840a