ELF之重定位
链接过程
如上图所示在经过编译流程后通过源文件生成可重定位文件 *.o,这里的可重定位文件每个都是独立编译产生,所以没法进行全局的统一地址分配,也无法确定可重定位位文件中引用的外部符号是否真实存在,这些问题的处理统一留到了链接阶段进行。
对于静态链接,在链接阶段中会将所有输入的可重定位文件(包括静态库)进行节合并,并进行统一的地址分配、检查符号是否存在,完成重定位项的重定位工作,生成最终的可执行文件。
链接过程主要步骤包括地址与空间分配、符号查找、重定位。
程序例子:
// source code a.c
extern int global_var;
extern int add(int a, int b);
int global_var1 = 0;
int global_var2;
int main(void)
{
int b = 10;
int sum = 0;
sum = add(global_var, b);
return sum;
}
// source code b.c
int global_var = 1;
int add(int a, int b)
{
return a+b;
}
如上图所示的a.c 和 b.c 使用如下命令进行编译和链接生成a.o 和b.o 两个可重定位文件以及可执行文件 ab。
gcc -c a.c b.c && ld a.o b.o -e main -o ab
空间与地址分配
链接过程中需要将多个输入的可重定位文件进行合并,会扫描所有输入文件获取所有节的位置、长度、属性,并将所有的符号表收集到一起,并将在链接阶段未分配空间的隐式弱符号进行地址空间分配,然后将每个输入文件中的相同节合并到一起。
- a.o 重定位文件的节区信息:
> readelf -S ./out/obj/a.o
There are 11 section headers, starting at offset 0x2a0:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 00000000 000034 000000 00 AX 0 0 1
[ 2] .data PROGBITS 00000000 000034 000000 00 WA 0 0 1
[ 3] .bss NOBITS 00000000 000034 000004 00 WA 0 0 4
[ 4] .text.startup PROGBITS 00000000 000034 000010 00 AX 0 0 4
[ 5] .rel.text.startup REL 00000000 000238 000010 08 I 8 4 4
[ 6] .comment PROGBITS 00000000 000044 00007f 01 MS 0 0 1
[ 7] .ARM.attributes ARM_ATTRIBUTES 00000000 0000c3 00003d 00 0 0 1
[ 8] .symtab SYMTAB 00000000 000100 000100 10 9 11 4
[ 9] .strtab STRTAB 00000000 000200 000037 00 0 0 1
[10] .shstrtab STRTAB 00000000 000248 000057 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
y (purecode), p (processor specific)
- b.o 重定位文件的节区信息:
> readelf -S ./out/obj/b.o
There are 9 section headers, starting at offset 0x208:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 00000000 000034 000004 00 AX 0 0 2
[ 2] .data PROGBITS 00000000 000038 000004 00 WA 0 0 4
[ 3] .bss NOBITS 00000000 00003c 000000 00 WA 0 0 1
[ 4] .comment PROGBITS 00000000 00003c 00007f 01 MS 0 0 1
[ 5] .ARM.attributes ARM_ATTRIBUTES 00000000 0000bb 00003d 00 0 0 1
[ 6] .symtab SYMTAB 00000000 0000f8 0000b0 10 7 9 4
[ 7] .strtab STRTAB 00000000 0001a8 00001a 00 0 0 1
[ 8] .shstrtab STRTAB 00000000 0001c2 000045 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
y (purecode), p (processor specific)
- ab 可执行文件的节区信息:
> readelf -S ./out/ab
There are 9 section headers, starting at offset 0x10358:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 60000000 010000 000014 00 AX 0 0 4
[ 2] .data PROGBITS 80000000 020000 000004 00 WA 0 0 4
[ 3] .bss NOBITS 80000004 020004 000008 00 WA 0 0 4
[ 4] .comment PROGBITS 00000000 020004 00007e 01 MS 0 0 1
[ 5] .ARM.attributes ARM_ATTRIBUTES 00000000 020082 00003d 00 0 0 1
[ 6] .symtab SYMTAB 00000000 0200c0 0001a0 10 7 13 4
[ 7] .strtab STRTAB 00000000 020260 00009d 00 0 0 1
[ 8] .shstrtab STRTAB 00000000 0202fd 000045 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
y (purecode), p (processor specific)
从以上查看的节区信息也能看到节的合并确实是将相同节合并,例如:a.o 中的 .text 节大小为 0x10Bytes,b.o 中的节大小为0x04Bytes,在 ab 可执行文件中 .text 的大小是 a.o 和 b.o 中大小之和 0x14Bytes。
需要注意的是 .bss 节的大小,发现 a.o 中 .bss 节中的大小是 0x04Bytes,b.o 中 .bss 节大小是 0x00,但是在 ab 中的 .bss 大小却是0x08Bytes,查看 a.o 中的符号表会发现存在一个索引为 COM 的 global_var2 变量,这种全局未初始化的变量属于隐式弱符号,在编译时无法确认是否在其他模块还存在强符号,最终大小未知所以不会分配空间,而是在链接阶段查找完符号表后由连接器进行地址空间分配,这就是为什么我们只有在ab可执行文件中才能看到 global_var2 变量的大小放在了 .bss 节中。
> readelf -s ./out/obj/a.o
Symbol table '.symtab' contains 16 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FILE LOCAL DEFAULT ABS a.c
...
11: 00000001 16 FUNC GLOBAL DEFAULT 4 main
12: 00000000 0 NOTYPE GLOBAL DEFAULT UND add
13: 00000000 0 NOTYPE GLOBAL DEFAULT UND global_var
14: 00000004 4 OBJECT GLOBAL DEFAULT COM global_var2
15: 00000000 4 OBJECT GLOBAL DEFAULT 3 global_var1
.bss节虽然在节区信息中可以看到大小,但是在ELF 文件(可以理解为Flash空间)中是不占用空间的,但是在设备的内存上占用对应大小的空间,设备加载ELF 文件后会根据.bss 节的大小,在内存上申请空间,并将整个.bss 内存空间置0,完成初始化。
.text和.rodata节 占用ELF 文件空间,但是如果采用 XIP 方式在NorFlash 上直接运行则可以不占用内存空间,否则的话需要载入内存运行。
.data节 则既占用ELF 文件空间又存在于内存空间,在编译链接完成后初始化的非0变量占用ELF文件空间(可以理解为Flash空间),同时在运行时也需要载入内存。
完成以上所有输入文件的节合并后,会根据 .ld 链接文件中指定的 flash 和 ram 地址确定所有符号和节的新地址,完成整个可执行文件的重新编址,.ld 文件中申明了 flash (rx): ORIGIN = 0x60000000,ram (rwx): ORIGIN = 0x80000000,可以看到ab可执行文件中的.text 和 .rodata 从0x60000000 进行编址,.data 和.bss 则是从0x80000000开始编址。
符号解析
符号的解析主要是在重定位过程中进行,链接器会根据重定项,找到符号表,并通过符号表找到对应的符号位置,使用符号地址完成重定位过程,当我们在程序中使用了不存在的符号时,链接器在重定位时从符号表中找不到时就会报错 “undefined reference to xxx
”
> readelf -s ./out/obj/a.o
Symbol table '.symtab' contains 16 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FILE LOCAL DEFAULT ABS a.c
...
10: 00000000 0 SECTION LOCAL DEFAULT 7
11: 00000001 16 FUNC GLOBAL DEFAULT 4 main
12: 00000000 0 NOTYPE GLOBAL DEFAULT UND add
13: 00000000 0 NOTYPE GLOBAL DEFAULT UND global_var
14: 00000004 4 OBJECT GLOBAL DEFAULT COM global_var2
15: 00000000 4 OBJECT GLOBAL DEFAULT 3 global_var1
> readelf -s ./out/obj/b.o
Symbol table '.symtab' contains 11 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FILE LOCAL DEFAULT ABS b.c
2: 00000000 0 SECTION LOCAL DEFAULT 1
...
8: 00000000 0 SECTION LOCAL DEFAULT 5
9: 00000001 4 FUNC GLOBAL DEFAULT 1 add
10: 00000000 4 OBJECT GLOBAL DEFAULT 2 global_var
> readelf -s ./out/ab
Symbol table '.symtab' contains 26 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 60000000 0 SECTION LOCAL DEFAULT 1
...
12: 80000000 0 NOTYPE LOCAL DEFAULT 2 $d
13: 80000000 4 OBJECT GLOBAL DEFAULT 2 global_var
14: 60000000 0 NOTYPE GLOBAL DEFAULT 1 __text_start
15: 00000000 0 NOTYPE GLOBAL DEFAULT 2 __rodata_end
16: 80000008 0 NOTYPE GLOBAL DEFAULT 3 __bss_end
17: 60000011 4 FUNC GLOBAL DEFAULT 1 add
18: 00000000 0 NOTYPE GLOBAL DEFAULT 2 __rodata_start
19: 80000004 0 NOTYPE GLOBAL DEFAULT 2 __data_end
20: 60000014 0 NOTYPE GLOBAL DEFAULT 1 __text_end
21: 80000004 0 NOTYPE GLOBAL DEFAULT 3 __bss_start
22: 60000001 16 FUNC GLOBAL DEFAULT 1 main
23: 80000004 4 OBJECT GLOBAL DEFAULT 3 global_var1
24: 80000008 4 OBJECT GLOBAL DEFAULT 3 global_var2
25: 80000000 0 NOTYPE GLOBAL DEFAULT 2 __data_start
本地符号解析
编译器在编译时只允许每个模块中每个局部符号有一个定义,静态局部变量也会有本地链接器符号,编译器还要确保它们拥有唯一的名字。
全局符号解析:
当编译器遇到一个不是在当前模块中定义的符号时,会假设该符号是在其他某个模块中定义的,生成一个链接器符号表项,然后链接器会在所有输入文件中的符号表中查找这个被引用的符号,如果找不到就进行报错并停止。
在全局符号的解析时经常会面临多个输入文件中可能会定义相同名字的全局符号。这种情况下,链接器必须要么以某种方法选出最终使用的符号,要么报错终止链接。符号的类型被编译器存放在符合表中,主要有强符号和弱符号两种。
-
强符号:全局函数和初始化的全局变量,初始化为0的放在.bss 节,初始化非0的放在.data节;
-
隐式弱符号:默认情况下未初始化的全局变量在编译时不分配空间,可以使用 -fno-common 转为强符号。
-
显示弱符号:使用__attribute__((weak))修饰的全局变量,-fno-common 对显示申明的弱符号无效,在编译时分配空间。
符号解析时遵循以下规则:
- 不允许出现多个同名强符号,不论类型是否相同,否则报错;
- 存在一个同名强符号,多个弱符号,最终连接器会选择使用强符号;
- 存在多个类型不相同的同名弱符号,会选择类型较大的弱符号使用;
重定位
静态重定位:
静态重定位发生在编译过程的链接阶段,完成符号引用到符号真实地址的修正,编译过程中产生的重定位文件 .o 会将需要重定位的符号存储到 .rel.* 或者 .rela.* 节中,比如 .text节中存在需要重定位的存储单元,那么就会存在一个名为 .rela.text 的节。.rel.text.startup 重定位节中的 Inf = 4,表示重定位所作用的节在节头表中的索引是4,也就是 .text.startup 节。
> readelf -r ./out/obj/a.o
Relocation section '.rel.text.startup' at offset 0x238 contains 2 entries:
Offset Info Type Sym.Value Sym. Name
0000000c 00000d02 R_ARM_ABS32 00000000 global_var
00000006 00000c1e R_ARM_THM_JUMP24 00000000 add
连接器遍历重定位表中的所有重定位项,通过 r_offset 找到受重定位作用的存储单元(即需要指令修正的位置),再通过 r_info 找到重定位需要使用的符号表项和重定位的类型,进行重定位计算。
#define ELF32_R_SYM(i) ((i) >> 8) // 获取重定位所需的符号项
#define ELF32_R_TYPE(i) ((i)& 0xff) // 获取重定位的类型
通过以下命令可以看到反汇编代码中插入的重定位项,
> objdump -dx ./out/obj/a.o
Disassembly of section .text.startup:
00000000 <main>:
0: 4b02 ldr r3, [pc, #8] ; (c <main+0xc>)
2: 210a movs r1, #10
4: 6818 ldr r0, [r3, #0]
6: f7ff bffe b.w 0 <add>
6: R_ARM_THM_JUMP24 add
a: bf00 nop
c: 00000000 .word 0x00000000
c: R_ARM_ABS32 global_var
global_var 重定位计算
global_var 的重定位方式是 R_ARM_ABS32,跳转范围 -2^31 到 2^31-1,指令修正方式为:(S + A) | T
待重定位的指令获取
通过上面的重定位项获取如下信息
rel.offset = 0x0c // 需要重定位的指令在节中的偏移
rel.symbol = global_var // 重定位的符号索引ELF32_R_SYM(r_info),通过这个才能找到符号表项
rel.type = R_ARM_ABS32 // 重定位的类型ELF32_R_TYPE(r_info)
0x0000000c 重定项作用于 .text.startup 节中偏移为 0x0c 的位置,从a.o反汇编中可以看到偏移 0x0c 位置下修正前的指令为0x00000000,所以 A = 0x00000000。
重定位使用的符号地址
0x0000000c 重定项的 Info = 0x00000d02,可以通过 ELF32_R_SYM(0x00000d02) 获取到重定位所使用的符号在 a.o 的符号项索引为 13,符号表如下:
- a.o 中的符号表
Symbol table '.symtab' contains 16 entries:
Num: Value Size Type Bind Vis Ndx Name
...
13: 00000000 0 NOTYPE GLOBAL DEFAULT UND global_var
- b.o 中的符号表
Symbol table '.symtab' contains 16 entries:
Num: Value Size Type Bind Vis Ndx Name
...
10: 00000000 4 OBJECT GLOBAL DEFAULT 2 global_var
- b.o 中的节头表
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
...
[ 2] .data PROGBITS 00000000 000038 000004 00 WA 0 0 4
...
查看当前a.o 中的符号表索引为13的位置发现 Ndx = UND,表示是一个外部定义的符号,连接器会查找b.o符号表,发现符号中表存在 global_var 符号,Ndx = 2 表示符号存储的在节头表索引为 2 的 .data 节中,符号项 Value = 0 表示符号在所属节中的偏移为0,通过以上符号表可以获取以下信息:
sym.st_shndx = 2 // 符号所在节的节头表索引(先获取节名称,然后可以获取节的基地址)
sym.st_value = 0 // 符号所在节内偏移
sym.st_type = OBJECT // 符号类型
从链接文件可知道 ram (rwx): ORIGIN = 0x80000000,所以 .data的节地址为0x80000000,即符号存在于 S = 0x80000000+0 的位置。
重定位指令计算
通过上面的步骤获取 S = 0x80000000,A = 0x00000000,通过重定位公式 (S + A) | T 计算得到0x80000000 位置修正后的指令说为: 0x80000000 + 0 = 0x80000000,其实就是 global_var 符号的地址,通过查看可执行文件 ab 的符号表可以得到验证:
Num: Value Size Type Bind Vis Ndx Name
...
13: 80000000 4 OBJECT GLOBAL DEFAULT 2 global_var
...
add 重定位计算
重定位加数计算
add 的重定位方式是 R_ARM_THM_JUMP24,是Thumb指令集下修正 BL/BLX指令的方式,指令修正计算方式为 ((S + A) | T) - P。
Thumb指令下的BL/BLX指令结构如下:
------------------------------------------------------------------
|15|14|13|12|11|10|9 0|15|14|13|12|11|10 0|
------------------------------------------------------------------
| 1 1 1 1 0|S | imm10 |1 1|J1|1 |J2| imm11 |
------------------------------------------------------------------
指令中的S表示符号位,imm10表示跳转偏移量offset的高10位,imm11表示跳转偏移量offset的低11位,由于Thumb是2字节对齐的,所以这22位可以表示$\pm2^{22}$的偏移。
在ARMv6指令集之后,用于跳转的长度新增J1,J2两个bit,所以最多可以表示$\pm2^{24}$的偏移,跳转范围是(-2^24 ~ 2^24)。
主要步骤就是先把修正前的指令转换成偏移量,然后在使用指令修正计算公式计算出新的偏移量,最后将修正后的偏移量重新安装BL/BLX的指令格式进行拆分,最后写入到待重定位的存储单元下。
0x00000006 重定项作用于 text.startup 节中偏移为 0x06 的位置,从a.o反汇编中可以看到偏移 0x06 位置下修正前的指令为0xf7ffbffe
I1 = NOT(J1 EOR S); I2 = NOT(J2 EOR S);
imm32 = SignExtend(S:I1:I2:imm10:imm11:'0', 32);
指令码结构对照可以得到:
S = 1
imm10 = b'11 1111 1111'
J1 = 1
J2 = 1
imm11 = b'111 1111 1110'
那么,根据imm32 = SignExtend(S:I1:I2:imm10:imm11:'0', 32)的计算规则,可以得到重定位的偏移加数
A = SignExtend('1':'1':'1':'11 1111 1111':'111 1111 1110':'0', 32) = fffffffc = -4
待重定位的指令地址计算
通过上面的重定位项获取如下信息
rel.offset = 0x06 // 需要重定位的指令在节中的偏移
rel.symbol = add // 重定位的符号索引ELF32_R_SYM(r_info),通过这个才能找到符号表项
rel.type = R_ARM_THM_JUMP24 // 重定位的类型ELF32_R_TYPE(r_info)
0x00000006 重定项作用于 .text.startup 节中偏移为 0x06 的位置,.text.startup 节的基地址由链接脚本ld文件指定为 0x80000000,所以待重定位的位置为:P=0x60000000+0x00000006
重定位符号获取:
- a.o 中的符号表
Symbol table '.symtab' contains 16 entries:
Num: Value Size Type Bind Vis Ndx Name
...
12: 00000000 0 NOTYPE GLOBAL DEFAULT UND add
- b.o 中的符号表
Symbol table '.symtab' contains 16 entries:
Num: Value Size Type Bind Vis Ndx Name
...
9: 00000001 4 FUNC GLOBAL DEFAULT 1 add
- a.o 中的节头表
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
...
[ 1] .text PROGBITS 00000000 000034 000000 00 AX 0 0 1
...
[ 4] .text.startup PROGBITS 00000000 000034 000010 00 AX 0 0 4
[ 5] .rel.text.startup REL 00000000 000238 000010 08 I 8 4 4
...
- b.o 中的节头表
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 00000000 000034 000004 00 AX 0 0 2
...
查看符号表后可以获取符号信息如下
sym.st_shndx = 1 // 符号所在节的节头表索引(先获取节名称,然后可以获取节的基地址)
sym.st_value = 1 // 符号所在节内偏移
sym.st_type = FUNC // 符号类型
从 b.o 的符号表项 st_shndx = 1 可以知道,符号所在的节是节头表序号为1的 .text 节,上面我们说过相同的节会被合并到一起,所以 a.o 和 b.o 中的 .text 会合并,然后观察a.o的节头表中还存在 .text.startup 节,大小为0x000010,.text 节和 .text.startup 节会统一从Flash地址 0x60000000 开始编址,并且 .text.startup 在最前面,所以 .text 节的基地址为 0x60000000 + 0x000010,在加上符号表的 st_value, 所以符号存在于 S = 0x60000010 + 0x000001 的位置,实际符号存在于 0x60000010 地址,0x60000011 中的 1表示是 Thumb指令下的函数地址,即公式中的T,在重定位计算时,进行2字节对齐,去除最后的1。
重定位指令计算
通过上面的步骤获取 S = 0x60000011,P = 0x60000006,A = -4,根据 ((S + A) | T) - P 公式计算:
imm32 = 0x60000011 - 4 - 0x60000006 = 7,在将计算后的偏移量还原到 BL/BLX指令格式中:
S = 0
imm10 = b'00 0000 0000'
J1 = 1
J2 = 1
imm11 = b'000 0000 0011'
最后得到0x60000006位置下修正后的指令为 0xf000b803,查看最终的可执行文件 ab 可以验证以上计算:
60000000 <main>:
60000000: 4b02 ldr r3, [pc, #8] ; (6000000c <main+0xc>)
60000002: 2100 movs r1, #0
60000004: 6818 ldr r0, [r3, #0]
60000006: f000 b803 b.w 60000010 <add>
6000000a: bf00 nop
6000000c: 80000000 .word 0x80000000
60000010 <add>:
60000010: 4408 add r0, r1
60000012: 4770 bx lr
动态重定位:
// TODO