从实践理解《程序员的自我修养》(1)
前言
这篇文档主要从实践的角度充分理解《程序员的自我修养》一书中提到的细节。书中提到的各种机制、数据结构,我都将在实际系统中找到并理解它们。
环境:Ubuntu 20.04
目标文件里有什么
示例代码
# a.c
#include <stdio.h>
extern int shared;
int main()
{
int a = 100;
printf("hello, World.");
swap(&a, &shared);
}
# b.c
int shared = 1;
void swap(int *a, int *b)
{
*a ^= *b ^= *a ^= *b;
}
可执行文件ab为a.o和b.o链接后得到。接下来查看的ELF文件都是ab。
ELF文件的结构
ELF文件头
readelf -h ab
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
Version: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: DYN (共享目标文件)
系统架构: Advanced Micro Devices X86-64
版本: 0x1
入口点地址: 0x1080
程序头起点: 64 (bytes into file)
Start of section headers: 14848 (bytes into file)
标志: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 13
Size of section headers: 64 (bytes)
Number of section headers: 31
Section header string table index: 30
Sections(节)
我尽量不用段去描述Section这个概念,因为后面还有Segment的定义,容易混淆。
readelf -t ab
There are 31 section headers, starting at offset 0x3a00:
节头:
[号] 名称
类型 地址 偏移量 链接
大小 全体大小 信息 对齐
旗标
[ 0]
NULL 0000000000000000 0000000000000000 0
0000000000000000 0000000000000000 0 0
[0000000000000000]:
[ 1] .interp
PROGBITS 0000000000000318 0000000000000318 0
000000000000001c 0000000000000000 0 1
[0000000000000002]: ALLOC
...
[16] .text
PROGBITS 0000000000001080 0000000000001080 0
0000000000000215 0000000000000000 0 16
[0000000000000006]: ALLOC, EXEC
...
[25] .data
PROGBITS 0000000000004000 0000000000003000 0
0000000000000014 0000000000000000 0 8
[0000000000000003]: WRITE, ALLOC
[26] .bss
NOBITS 0000000000004014 0000000000003014 0
0000000000000004 0000000000000000 0 1
[0000000000000003]: WRITE, ALLOC
[27] .comment
PROGBITS 0000000000000000 0000000000003014 0
000000000000002a 0000000000000001 0 1
[0000000000000030]: MERGE, STRINGS
[28] .symtab
SYMTAB 0000000000000000 0000000000003040 29
0000000000000678 0000000000000018 47 8
[0000000000000000]:
[29] .strtab
STRTAB 0000000000000000 00000000000036b8 0
000000000000022d 0000000000000000 0 1
[0000000000000000]:
[30] .shstrtab
STRTAB 0000000000000000 00000000000038e5 0
000000000000011a 0000000000000000 0 1
[0000000000000000]:
Section header table
我们之前读取的section信息应该都是通过Section header table得到的。文件的主体应该是一个个section,它们关系到程序的具体功能,但是如果只想了解程序的大致结构信息,也许Section header table就足够概括这些section的特性了。
书中给出了Section header table在Linux中的实现,它是一个结构体数组,结构体名称为Elf32_Shdr,也就是所谓Section header。我使用的机器是64位的,所以我的/usr/include/elf.h里还有64位的Elf64_Shdr。
/* Section header. */
typedef struct
{
Elf32_Word sh_name; /* Section name (string tbl index) */
Elf32_Word sh_type; /* Section type */
Elf32_Word sh_flags; /* Section flags */
Elf32_Addr sh_addr; /* Section virtual addr at execution */
Elf32_Off sh_offset; /* Section file offset */
Elf32_Word sh_size; /* Section size in bytes */
Elf32_Word sh_link; /* Link to another section */
Elf32_Word sh_info; /* Additional section information */
Elf32_Word sh_addralign; /* Section alignment */
Elf32_Word sh_entsize; /* Entry size if section holds table */
} Elf32_Shdr;
typedef struct
{
Elf64_Word sh_name; /* Section name (string tbl index) */
Elf64_Word sh_type; /* Section type */
Elf64_Xword sh_flags; /* Section flags */
Elf64_Addr sh_addr; /* Section virtual addr at execution */
Elf64_Off sh_offset; /* Section file offset */
Elf64_Xword sh_size; /* Section size in bytes */
Elf64_Word sh_link; /* Link to another section */
Elf64_Word sh_info; /* Additional section information */
Elf64_Xword sh_addralign; /* Section alignment */
Elf64_Xword sh_entsize; /* Entry size if section holds table */
} Elf64_Shdr;
可以看到我们之前用readelf -t ab
命令读取到的信息都能在这里的结构体上对应到。32位和64位的成员定义除了类型几乎没有区别。
重点Section
字符串表
.strtab是字符串表(STRING TABLE)
.shstrtab是段表字符串表(Section Header String Table),针对段表
.symtab是符号表,一般是变量、函数
shstrtab及symtab经常引用strtab中的字符串。
readelf -x 29 ab
“.strtab”节的十六进制输出:
0x00000000 00637274 73747566 662e6300 64657265 .crtstuff.c.dere
0x00000010 67697374 65725f74 6d5f636c 6f6e6573 gister_tm_clones
0x00000020 005f5f64 6f5f676c 6f62616c 5f64746f .__do_global_dto
0x00000030 72735f61 75780063 6f6d706c 65746564 rs_aux.completed
...
0x00000210 66696e61 6c697a65 4040474c 4942435f finalize@@GLIBC_
0x00000220 322e322e 35007368 61726564 00 2.2.5.shared.
readelf -p 29 ab
String dump of section '.strtab':
[ 1] crtstuff.c
[ c] deregister_tm_clones
[ 21] __do_global_dtors_aux
[ 37] completed.8060
[ 46] __do_global_dtors_aux_fini_array_entry
[ 6d] frame_dummy
[ 79] __frame_dummy_init_array_entry
[ 98] a.c
[ 9c] b.c
[ a0] __FRAME_END__
[ ae] __init_array_end
[ bf] _DYNAMIC
[ c8] __init_array_start
[ db] __GNU_EH_FRAME_HDR
[ ee] _GLOBAL_OFFSET_TABLE_
[ 104] __libc_csu_fini
[ 114] _ITM_deregisterTMCloneTable
[ 130] _edata
[ 137] __stack_chk_fail@@GLIBC_2.4
[ 153] printf@@GLIBC_2.2.5
[ 167] __libc_start_main@@GLIBC_2.2.5
[ 186] __data_start
[ 193] __gmon_start__
[ 1a2] __dso_handle
[ 1af] _IO_stdin_used
[ 1be] __libc_csu_init
[ 1ce] __bss_start
[ 1da] main
[ 1df] __TMC_END__
[ 1eb] _ITM_registerTMCloneTable
[ 205] swap
[ 20a] __cxa_finalize@@GLIBC_2.2.5
[ 226] shared
ELF文件中,有些Sectoin会被装载,有些不会,在之前的Sections信息中可以看到.strtab并没有装载地址信息,说明它确实不会被装载。那么它应该只有在链接等环节才会起作用。
字符串表所谓的字符串并不包含代码中出现的字符串,甚至变量名也不会出现在其中,不过函数名倒是可以在其中看到。
重定位表
链接器在处理目标文件时,须要对目标文件中某些部位进行重定位,即代码段和数据段中那些对绝对地址的引用的位置。这些重定位的信息都记录在ELF 文件的重定位表里面,对于每个须要重定位的代码段或数据段,都会有一个相应的重定位表。
readelf -r a.o
重定位节 '.rela.text' at offset 0x2a8 contains 3 entries:
偏移量 信息 类型 符号值 符号名称 + 加数
000000000029 000a00000002 R_X86_64_PC32 0000000000000000 shared - 4
000000000036 000c00000004 R_X86_64_PLT32 0000000000000000 swap - 4
00000000004f 000d00000004 R_X86_64_PLT32 0000000000000000 __stack_chk_fail - 4
重定位节 '.rela.eh_frame' at offset 0x2f0 contains 1 entry:
偏移量 信息 类型 符号值 符号名称 + 加数
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
readelf -x 2 a.o
“.rela.text”节的十六进制输出:
0x00000000 29000000 00000000 02000000 0a000000 )...............
0x00000010 fcffffff ffffffff 36000000 00000000 ........6.......
0x00000020 04000000 0c000000 fcffffff ffffffff ................
0x00000030 4f000000 00000000 04000000 0d000000 O...............
0x00000040 fcffffff ffffffff ........
具体功能后面再说。
符号表
readelf -s ab
Symbol table '.dynsym' contains 8 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __stack_chk_fail@GLIBC_2.4 (2)
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5 (3)
4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (3)
5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
6: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
7: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (3)
Symbol table '.symtab' contains 69 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000318 0 SECTION LOCAL DEFAULT 1
2: 0000000000000338 0 SECTION LOCAL DEFAULT 2
3: 0000000000000358 0 SECTION LOCAL DEFAULT 3
4: 000000000000037c 0 SECTION LOCAL DEFAULT 4
5: 00000000000003a0 0 SECTION LOCAL DEFAULT 5
6: 00000000000003c8 0 SECTION LOCAL DEFAULT 6
7: 0000000000000488 0 SECTION LOCAL DEFAULT 7
8: 0000000000000528 0 SECTION LOCAL DEFAULT 8
9: 0000000000000538 0 SECTION LOCAL DEFAULT 9
10: 0000000000000568 0 SECTION LOCAL DEFAULT 10
11: 0000000000000628 0 SECTION LOCAL DEFAULT 11
12: 0000000000001000 0 SECTION LOCAL DEFAULT 12
13: 0000000000001020 0 SECTION LOCAL DEFAULT 13
14: 0000000000001050 0 SECTION LOCAL DEFAULT 14
15: 0000000000001060 0 SECTION LOCAL DEFAULT 15
16: 0000000000001080 0 SECTION LOCAL DEFAULT 16
17: 0000000000001298 0 SECTION LOCAL DEFAULT 17
18: 0000000000002000 0 SECTION LOCAL DEFAULT 18
19: 0000000000002014 0 SECTION LOCAL DEFAULT 19
20: 0000000000002060 0 SECTION LOCAL DEFAULT 20
21: 0000000000003db0 0 SECTION LOCAL DEFAULT 21
22: 0000000000003db8 0 SECTION LOCAL DEFAULT 22
23: 0000000000003dc0 0 SECTION LOCAL DEFAULT 23
24: 0000000000003fb0 0 SECTION LOCAL DEFAULT 24
25: 0000000000004000 0 SECTION LOCAL DEFAULT 25
26: 0000000000004014 0 SECTION LOCAL DEFAULT 26
27: 0000000000000000 0 SECTION LOCAL DEFAULT 27
28: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
29: 00000000000010b0 0 FUNC LOCAL DEFAULT 16 deregister_tm_clones
30: 00000000000010e0 0 FUNC LOCAL DEFAULT 16 register_tm_clones
31: 0000000000001120 0 FUNC LOCAL DEFAULT 16 __do_global_dtors_aux
32: 0000000000004014 1 OBJECT LOCAL DEFAULT 26 completed.8060
33: 0000000000003db8 0 OBJECT LOCAL DEFAULT 22 __do_global_dtors_aux_fin
34: 0000000000001160 0 FUNC LOCAL DEFAULT 16 frame_dummy
35: 0000000000003db0 0 OBJECT LOCAL DEFAULT 21 __frame_dummy_init_array_
36: 0000000000000000 0 FILE LOCAL DEFAULT ABS a.c
37: 0000000000000000 0 FILE LOCAL DEFAULT ABS b.c
38: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
39: 0000000000002184 0 OBJECT LOCAL DEFAULT 20 __FRAME_END__
40: 0000000000000000 0 FILE LOCAL DEFAULT ABS
41: 0000000000003db8 0 NOTYPE LOCAL DEFAULT 21 __init_array_end
42: 0000000000003dc0 0 OBJECT LOCAL DEFAULT 23 _DYNAMIC
43: 0000000000003db0 0 NOTYPE LOCAL DEFAULT 21 __init_array_start
44: 0000000000002014 0 NOTYPE LOCAL DEFAULT 19 __GNU_EH_FRAME_HDR
45: 0000000000003fb0 0 OBJECT LOCAL DEFAULT 24 _GLOBAL_OFFSET_TABLE_
46: 0000000000001000 0 FUNC LOCAL DEFAULT 12 _init
47: 0000000000001290 5 FUNC GLOBAL DEFAULT 16 __libc_csu_fini
48: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
49: 0000000000004000 0 NOTYPE WEAK DEFAULT 25 data_start
50: 0000000000004014 0 NOTYPE GLOBAL DEFAULT 25 _edata
51: 0000000000001298 0 FUNC GLOBAL HIDDEN 17 _fini
52: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __stack_chk_fail@@GLIBC_2
53: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@@GLIBC_2.2.5
54: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@@GLIBC_
55: 0000000000004000 0 NOTYPE GLOBAL DEFAULT 25 __data_start
56: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
57: 0000000000004008 0 OBJECT GLOBAL HIDDEN 25 __dso_handle
58: 0000000000002000 4 OBJECT GLOBAL DEFAULT 18 _IO_stdin_used
59: 0000000000001220 101 FUNC GLOBAL DEFAULT 16 __libc_csu_init
60: 0000000000004018 0 NOTYPE GLOBAL DEFAULT 26 _end
61: 0000000000001080 47 FUNC GLOBAL DEFAULT 16 _start
62: 0000000000004014 0 NOTYPE GLOBAL DEFAULT 26 __bss_start
63: 0000000000001169 102 FUNC GLOBAL DEFAULT 16 main
64: 0000000000004018 0 OBJECT GLOBAL HIDDEN 25 __TMC_END__
65: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
66: 00000000000011cf 79 FUNC GLOBAL DEFAULT 16 swap
67: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@@GLIBC_2.2
68: 0000000000004010 4 OBJECT GLOBAL DEFAULT 25 shared
筛选出我们自己定义或者关心的符号:
36: 0000000000000000 0 FILE LOCAL DEFAULT ABS a.c
66: 00000000000011cf 79 FUNC GLOBAL DEFAULT 16 swap
68: 0000000000004010 4 OBJECT GLOBAL DEFAULT 25 shared
符号表的C实现也是结构体数组,结构体为Elf32_Sym(或Elf64_Sym):
/* Symbol table entry. */
typedef struct
{
Elf32_Word st_name; /* Symbol name (string tbl index) */
Elf32_Addr st_value; /* Symbol value */
Elf32_Word st_size; /* Symbol size */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;
typedef struct
{
Elf64_Word st_name; /* Symbol name (string tbl index) */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf64_Section st_shndx; /* Section index */
Elf64_Addr st_value; /* Symbol value */
Elf64_Xword st_size; /* Symbol size */
} Elf64_Sym;
小实验
做一个关于符号表和字符串表的小实验。首先选取符号:
66: 00000000000011cf 79 FUNC GLOBAL DEFAULT 16 swap
符号名是swap,根据文档,st_name其实是swap这个字符串在字符串表里的下标。我们看之前的字符串表,知道swap的下标(字节为单位)是
[ 205] swap
直接查看.symtab对应位置的hex dump。
05020000(st_name) 12(st_info)00(st_other)1000(st_shndx:.text) cf110000 00000000(st_value) 4f000000 00000000(st_size)
可以看到uint32类型的st_name对应0x05020000,对其应用小端序也就是0x205,与字符串表下标一致。
最后验证一下st_value(在可执行文件中,这个表示虚拟地址):
objdump -D ab
...
0000000011cf <swap>:
11cf: f3 0f 1e fa endbr64
11d3: 55 push %rbp
11d4: 48 89 e5 mov %rsp,%rbp
11d7: 48 89 7d f8 mov %rdi,-0x8(%rbp)
11db: 48 89 75 f0 mov %rsi,-0x10(%rbp)
...
静态链接
完成两项任务:
-
空间与地址分配
虚拟地址空间是针对可执行文件来说的,目标文件更多使用文件偏移或段偏移来定位。所以这一步其实是将几个目标文件的内容先按照一定的规则(相似段合并)编排,再塞进一个虚拟地址空间,这样每个符号都有了属于自己的虚拟地址。
-
符号解析与重定位
前一步已经形成了一个全局的符号表,那么每个目标文件中的符号与全局符号表是怎样的对应关系就需要在这里搞清楚。并且因为每个符号都拥有了新的地址,所以之前所有的绝对地址引用都要被修改,也就是重定位。
空间与地址分配
示例代码:
// a.c
#include <stdio.h>
extern int shared;
extern int global_uinit;
int main()
{
int a = 100;
static int static_init = 20;
static int static_uinit;
global_uinit = 20;
printf("hello, World.");
swap(&a, &shared);
}
// b.c
int shared = 1;
int global_uinit;
void swap(int *a, int *b)
{
*a ^= *b ^= *a ^= *b;
}
下面对a.o、b.o、ab
三个ELF文件进行考察,考察的对象是几个主要的符号shared、global_uinit、static_init、static_uinit、swap
。
readelf -s a.o
Symbol table '.symtab' contains 19 entries:
Num: Value Size Type Bind Vis Ndx Name
...
// 下面两个都对外部文件不可见,在链接时几乎不用考虑
// Ndx = 4 对应了.bss,因为.bss无实际空间,所以偏移value为0
// Ndx = 3 对应了.data,它的十六进制输出:0x00000000 14000000,对应value为0
6: 0000000000000000 4 OBJECT LOCAL DEFAULT 4 static_uinit.2319
7: 0000000000000000 4 OBJECT LOCAL DEFAULT 3 static_init.2318
...
12: 0000000000000000 112 FUNC GLOBAL DEFAULT 1 main
// Ndx = UND表示引用外部定义符号,需要在链接时进行重定位
13: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND global_uinit
...
16: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND shared
17: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND swap
...
readelf -s b.o
Symbol table '.symtab' contains 12 entries:
Num: Value Size Type Bind Vis Ndx Name
...
9: 0000000000000000 4 OBJECT GLOBAL DEFAULT 2 shared
// Ndx = COM表示未初始化的全局符号
10: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM global_uinit
11: 0000000000000000 79 FUNC GLOBAL DEFAULT 1 swap
注:
COMMON--------------未初始化的全局变量
.bss ---------------------未初始化的静态变量,以及初始化为0的全局变量或静态变量
查看a.o对这几处符号是如何引用的。
objdump -D a.o
...
// global_uinit和shared的位置上就是0
22: c7 05 00 00 00 00 1e movl $0x1e,0x0(%rip) # 2c <main+0x2c>
...
41: 48 8d 35 00 00 00 00 lea 0x0(%rip),%rsi # 48 <main+0x48>
...
// swap的地址也是0,但是根据观察,目标文件的代码部分对所有函数(包括文件内函数)地址的引用好像都是0
ab: e8 00 00 00 00 callq b0 <main+0x78>
查看ab的符号表。
readelf -s ab
...
// Ndx = 26 对应了.bss,static_uinit和global_uinit都被归入.bss
// Ndx = 25 对应了.data
37: 000000000000401c 4 OBJECT LOCAL DEFAULT 26 static_uinit.2319
38: 0000000000004010 4 OBJECT LOCAL DEFAULT 25 static_init.2318
...
// Ndx = 16 对应了.text
65: 0000000000001169 112 FUNC GLOBAL DEFAULT 16 main
...
68: 00000000000011d9 79 FUNC GLOBAL DEFAULT 16 swap
69: 0000000000004020 4 OBJECT GLOBAL DEFAULT 26 global_uinit
...
71: 0000000000004014 4 OBJECT GLOBAL DEFAULT 25 shared
每个符号都有了虚拟地址,说明完成了空间与地址分配。
符号解析与重定位
参与符号解析与重定位的主要有swap、global_uinit、shared
,它们三者在a.o中引用时都是全0,查看一下它们在ab中的引用。
objdump -D ab
...
118b: c7 05 8b 2e 00 00 1e movl $0x1e,0x2e8b(%rip) # 4020 <global_uinit>
...
11aa: 48 8d 35 63 2e 00 00 lea 0x2e63(%rip),%rsi # 4014 <shared>
...
11b9: e8 1b 00 00 00 callq 11d9 <swap>
...
这些符号都被修正到了正确的地址上,那么问题是链接过程中如何完成寻址?
从代码来看,重定位基本上就是修正a.o中的外部符号引用,查看a.o的代码段重定位表。
在此之前,看一下重定位表的C实现:
/* Relocation table entry without addend (in section of type SHT_REL). */
typedef struct
{
Elf32_Addr r_offset; /* Address */
Elf32_Word r_info; /* Relocation type and symbol index */
} Elf32_Rel;
/* I have seen two different definitions of the Elf64_Rel and
Elf64_Rela structures, so we'll leave them out until Novell (or
whoever) gets their act together. */
/* The following, at least, is used on Sparc v9, MIPS, and Alpha. */
typedef struct
{
Elf64_Addr r_offset; /* Address ,符号的第一个字节相对段的偏移,uint64_t*/
Elf64_Xword r_info; /* Relocation type and symbol index,uint64_t */
} Elf64_Rel;
/* Relocation table entry with addend (in section of type SHT_RELA). */
typedef struct
{
Elf32_Addr r_offset; /* Address */
Elf32_Word r_info; /* Relocation type and symbol index */
Elf32_Sword r_addend; /* Addend */
} Elf32_Rela;
typedef struct
{
Elf64_Addr r_offset; /* Address */
Elf64_Xword r_info; /* Relocation type and symbol index */
Elf64_Sxword r_addend; /* Addend */
} Elf64_Rela;
书中提到的是Rel,但是我的系统中使用的是Rela,这两者的差别主要在于多了个成员变量r_addend,下面简单说明一下两者的区别。
As specified previously, only Elf32_Rela
and Elf64_Rela
entries contain an explicit addend. Entries of type Elf32_Rel
and Elf64_Rel
store an implicit addend in the location to be modified. Depending on the processor architecture, one form or the other might be necessary or more convenient. Consequently, an implementation for a particular machine may use one form exclusively or either form depending on context.
也就是说,r_addend其实就是Rel形式中被修改位置保存的值,它们的原理一致。
objdump -r a.o
a.o: 文件格式 elf64-x86-64
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
// VALUE
0000000000000024 R_X86_64_PC32 global_uinit-0x0000000000000008
...
0000000000000044 R_X86_64_PC32 shared-0x0000000000000004
0000000000000051 R_X86_64_PLT32 swap-0x0000000000000004
...
重定位表为我们提供的线索总结来说就是:
- 待重定位的外部符号是什么?r_info高32位指出它在符号表的下标。
- 待重定位的外部符号在哪里?r_offset指出。
- 重定位方法是什么?r_info低32位指出。
- 重定位计算中需要用到的VALUE是多少?r_addend指出。
那么实际重定位的时候肯定已经完成了虚拟地址分配,每个符号有了自己的虚拟地址,也就可以开始重定位的计算了。
R_386_32:值1,绝对寻址修正S+A
R_X86_64_32
R_386_PC32:值2,相对寻址修正S+A-P
R_X86_64_PC32
R_X86_64_PLT32
A:被修正位置的值。在Rela形式中,就是r_addend的值。对应objdump -r命令查出的VALUE值。
P:被修正的位置。可以通过相对于段开始的偏移量,也就是r_offset加上该段第一个函数在可执行文件中的虚拟地址计算出来。(在实验中,a.o的代码段起始就是main函数,所以r_offset就是相对于main来说的,但是链接之后的代码段起始就不一定是main函数了,那么直接用.text的虚拟地址加r_offset就无法定位到shared)
S:符号的实际虚拟地址。r_info高32位指出它在目标文件符号表的下标,链接时可以换算到可执行文件符号表的下标,进而可以得到VMA。
根据上文的重定位类型,知道是相对寻址修正,所以分别计算A,P,S。
以shared为例:A = -4h,P = 44h + 1169h = 11ADh,S = 4014h。
S + A - P = 2E63h,所以原本shared的位置上应该由0变成2E63h。
objdump -d ab
...
11aa: 48 8d 35 63 2e 00 00 lea 0x2e63(%rip),%rsi # 4014 <shared>
...
其他
之前我认为需要重定位的都是外部符号,后来实验中发现一些本地函数以及本地定义的全局变量、局部静态变量同样需要重定位,它们在目标文件中的引用也是0,只有在可执行文件中才修改为相对寻址,我认为这样做的原因是目标文件中如果就使用了相对寻址,那么在链接的时候,如果代码段数据段等等被重排,之前的相对寻址就可能失效。
强弱符号
在C语言中,编译器默认函数和初始化了的全局变量为强符号(Strong Symbol),未初始化的全局变量为弱符号(Weak Symbol)。强符号之所以强,是因为它们拥有确切的数据,变量有值,函数有函数体;弱符号之所以弱,是因为它们还未被初始化,没有确切的数据。
链接器会按照如下的规则处理被多次定义的强符号和弱符号:
- 不允许强符号被多次定义,也即不同的目标文件中不能有同名的强符号;如果有多个强符号,那么链接器会报符号重复定义错误。
- 如果一个符号在某个目标文件中是强符号,在其他文件中是弱符号,那么选择强符号。
- 如果一个符号在所有的目标文件中都是弱符号,那么选择其中占用空间最大的一个。
现在我们再回头总结性地思考关于未初始化的全局变量的问题:在目标文件中,编译器为什么不直接把未初始化的全局变量也当作未初始化的局部静态变量样处理,为它在 BSS段分配空间,而是将其标记为一个COMMON类型的变量?
通过了解链接器处理多个弱符号的过程,我们可以想到,当编译器将一个编译单元编译成目标文件的时候,如果该编译单元包含了弱符号(未初始化的全局变量就是典型的弱符号),那么该弱符号最终所占空间的大小在此时是未知的,因为有可能其他编译单元中该符号所占的空间比本编译单元该符号所占的空间要大。所以编译器此时无法为该弱符号在.BSS段分配空间,因为所须要空间的大小未知。但是链接器在链接过程中可以确定弱符号的大小,因为当链接器读取所有输入目标文件以后,任何一个弱符号的最终大小都可以确定了,所以它可以在最终输出文件的 BSS 段为其分配空间。所以总体来看,未初始化全局变量最终还是被放在BSS 段的。