1. 目标文件格式
从结构上来讲,目标文件是经过编译后的可执行文件格式,只是还没有经过链接,其中有些符号或地址还没有被调整。当前,主流 PC 平台的可执行文件格式包括 Windows 下的 PE 和 Linux 下的 ELF,它们都是 COFF 格式变种。
ELF 文件标准把采用 ELF 格式的文件归为可重定位文件、可执行文件、共享目标文件和核心转储文件四类。在 Linux 下,可使用 file 命令查看相应的文件格式。可重定位文件:
$ file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
可执行文件:
$ file TinyHelloWorld
TinyHelloWorld: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, not stripped
共享目标文件:
$ file hello
hello: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l,
for GNU/Linux 3.2.0, BuildID[sha1]=847ece746b56cba30c2cca74ef5fb73245b351c5, not stripped
核心转储文件:
$ file core
core: ELF 64-bit LSB core file x86-64, version 1 (SYSV), SVR4-style, from './core_test', real uid: 0,
effective uid: 0, real gid: 0, effective gid: 0, execfn: './core_test', platform: 'x86_64'
2. 目标文件内容
目标文件按照数据的属性,以段的形式存储各部分内容。源代码编译后的机器指令通常存放在代码段,命名为 .code 或 .text;全局变量和局部静态变量通常存放在数据段,命名为 .data。以下面程序为例,介绍各个段的内容:
int printf(const char* format, ...);
int global_init_var = 84;
int global_uninit_var;
void func1(int i)
{
printf("%d\n", i);
}
int main(void)
{
static int static_var = 85;
static int static_var2;
int a = 1;
int b;
func1(static_var + static_var2 + a + b);
return a;
}
使用 gcc 编译上述代码,得到一个大小为 1936 字节的目标文件 SimpleSection.o。使用 objdump 工具查看该目标文件的内容:
$ objdump -h SimpleSection.o
SimpleSection.o: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000057 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000008 0000000000000000 0000000000000000 00000098 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000004 0000000000000000 0000000000000000 000000a0 2**2
ALLOC
3 .rodata 00000004 0000000000000000 0000000000000000 000000a0 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .comment 0000002a 0000000000000000 0000000000000000 000000a4 2**0
CONTENTS, READONLY
5 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000000ce 2**0
CONTENTS, READONLY
6 .eh_frame 00000058 0000000000000000 0000000000000000 000000d0 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
由结果可知,该目标文件共有 6 个段。各项表示段的特性,Size 表示段大小,File off 项表示段偏移等。每个段的第二行显示了该段的属性,如 CONTENTS 表示段存在于目标文件中,READONLY 表示只读段等。
.bss 段不含 CONTENTS 表示该段不存在于目标文件。.note.GNU-stack 包含 CONTENTS 但是其 Size 为零,把它当作不存在于目标文件。根据上述内容,可以绘出各段在目标文件中的位置:
2.1 代码段
objdump 的 -s 选项将所有段的内容以十六进制的方式打印出来,-d 选项将所有包含指令的段反汇编。
objdump -s -d SimpleSection.o
将代码段 .text 部分提取出来:
Contents of section .text:
0000 554889e5 4883ec10 897dfc8b 45fc89c6 UH..H....}..E...
0010 488d3d00 000000b8 00000000 e8000000 H.=.............
0020 0090c9c3 554889e5 4883ec10 c745f801 ....UH..H....E..
0030 0000008b 15000000 008b0500 00000001 ................
0040 c28b45f8 01c28b45 fc01d089 c7e80000 ..E....E........
0050 00008b45 f8c9c3 ...E...
该十六进制对应于源代码中的 func1 部分和 main 部分:
0000000000000000 <func1>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: 89 7d fc mov %edi,-0x4(%rbp)
b: 8b 45 fc mov -0x4(%rbp),%eax
e: 89 c6 mov %eax,%esi
10: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 17 <func1+0x17>
17: b8 00 00 00 00 mov $0x0,%eax
1c: e8 00 00 00 00 callq 21 <func1+0x21>
21: 90 nop
22: c9 leaveq
23: c3 retq
0000000000000024 <main>:
24: 55 push %rbp
25: 48 89 e5 mov %rsp,%rbp
28: 48 83 ec 10 sub $0x10,%rsp
2c: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%rbp)
33: 8b 15 00 00 00 00 mov 0x0(%rip),%edx # 39 <main+0x15>
39: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 3f <main+0x1b>
3f: 01 c2 add %eax,%edx
41: 8b 45 f8 mov -0x8(%rbp),%eax
44: 01 c2 add %eax,%edx
46: 8b 45 fc mov -0x4(%rbp),%eax
49: 01 d0 add %edx,%eax
4b: 89 c7 mov %eax,%edi
4d: e8 00 00 00 00 callq 52 <main+0x2e>
52: 8b 45 f8 mov -0x8(%rbp),%eax
55: c9 leaveq
56: c3 retq
左边标号为各指令的偏移地址,代码段大小为 0x56;中间部分为机器码;右边部分为汇编代码。
2.2 数据段
.data 段保存已经初始化了的全局静态变量和局部静态变量,即源代码中的 global_init_var 和 static_var。这两个变量均为整型,共 8 个字节,所以 .data 段大小为 8 字节。
调用 printf 中用到的 %d\n 是一种只读数据,它被放到 .rodata 段,大小为 4 字节。查看数据段的内容:
objdump -x -s -d SimpleSection.o
将数据段 .data 和 .rodata 部分提取出来:
Contents of section .data:
0000 54000000 55000000 T...U...
Contents of section .rodata:
0000 25640a00 %d..
54000000,0x00000054 对应于十进制的 84,刚好是 global_init_var 的值;55000000,0x00000055 对应于十进制的 85,刚好是 static_var 的值;25640a00,0x25 和 0x64为 %d ASCII 码,0x000a 为换行键的 ASCII 码。
2.3 BSS 段
.bss 段存放未初始化的全局变量和局部静态变量,如源代码中的 global_uninit_var 和 static_var2。但是该段大小仅有 4 字节,实际上只存放了 static_var2,而 global_uninit_var 是一个未定义的 COMMON 符号,没有被放在任何段。不同编译器对 .bss 存放内容的规定不同。
2.4 其他段
.comment 段存放编译器的版本信息,如字符串:GCC: (GNU) 7.5.0;.eh_frame 段用于程序调试等。
3. ELF 文件结构
3.1 文件头
ELF 目标文件的最前部是 ELF 文件头,其中包含了 ELF 文件版本、目标机器型号、程序入口地址等。紧接着是 ELF 文件的各个段。ELF 文件使用段表描述各个段的信息,如段名、段长度、在文件中的偏移、读写权限等。使用 readelf 命令查看 ELF 文件的文件头:
readelf -h SimpleSection.o
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 1104 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 13
Section header string table index: 12
由结果可知,ELF 文件的文件头包含了 ELF 魔数(Magic 对应的 16 字节内容)、文件机器的字节长度(ELF 64)、数据存储方式(小端序)、版本、运行平台(UNIX)、ABI 版本(0)、文件类型(REL)等。
3.2 段表
ELF 文件的段表用于保存各段的基本属性。使用 readelf 命令查看 ELF 文件的段表:
readelf -S SimpleSection.o
There are 13 section headers, starting at offset 0x450:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000057 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 00000340
0000000000000078 0000000000000018 I 10 1 8
[ 3] .data PROGBITS 0000000000000000 00000098
0000000000000008 0000000000000000 WA 0 0 4
[ 4] .bss NOBITS 0000000000000000 000000a0
0000000000000004 0000000000000000 WA 0 0 4
[ 5] .rodata PROGBITS 0000000000000000 000000a0
0000000000000004 0000000000000000 A 0 0 1
[ 6] .comment PROGBITS 0000000000000000 000000a4
000000000000002a 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 000000ce
0000000000000000 0000000000000000 0 0 1
[ 8] .eh_frame PROGBITS 0000000000000000 000000d0
0000000000000058 0000000000000000 A 0 0 8
[ 9] .rela.eh_frame RELA 0000000000000000 000003b8
0000000000000030 0000000000000018 I 10 8 8
[10] .symtab SYMTAB 0000000000000000 00000128
0000000000000198 0000000000000018 11 11 8
[11] .strtab STRTAB 0000000000000000 000002c0
000000000000007c 0000000000000000 0 0 1
[12] .shstrtab STRTAB 0000000000000000 000003e8
0000000000000061 0000000000000000 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),
l (large), p (processor specific)
各项的含义如下:
typedef struct
{
Elf64_Word sh_name;
Elf64_Word sh_type;
Elf64_Xword sh_flags;
Elf64_Addr sh_addr;
Elf64_Off sh_offset;
Elf64_Xword sh_size;
Elf64_Word sh_link;
Elf64_Word sh_info;
Elf64_Xword sh_addralign;
Elf64_Xword sh_entsize;
} Elf64_Shdr;
3.3 重定位表
ELF 文件的 .rela.text 段类型为 RELA,它是一个重定位表。链接器在处理目标文件时,须对目标文件中某些部位进行重定位,这些重定位的信息都记录在 ELF 文件的重定位表里。
3.4 字符串表
ELF 文件中用到了很多字符串,比如段名、变量名等。因为字符串的长度不固定,用固定结构存放较困难。常见做法是把字符串集中存放到一个表,然后使用偏移来表示字符串在表中的位置。比如:
使用 0、1、6 和 12 来引用空字符串、helloword、world 和 Myvariable。
4. 链接的接口——符号
在链接中,将函数和变量统称为符号。在 Linux 下使用 nm 查看符号:
nm SimpleSection.o
U _GLOBAL_OFFSET_TABLE_
0000000000000000 T func1
0000000000000000 D global_init_var
0000000000000004 C global_uninit_var
0000000000000024 T main
U printf
0000000000000004 d static_var.1802
0000000000000000 b static_var2.1803
4.1 ELF 符号表结构
ELF 文件的 .symtab 段表示符号表,它是一个 Elf64_Sym 结构,定义如下:
typedef struct
{
Elf64_Word st_name;
unsigned char st_info;
unsigned char st_other;
Elf64_Section st_shndx;
Elf64_Addr st_value;
Elf64_Xword st_size;
} Elf64_Sym;
4.2 符号修饰与函数签名
为了防止符号名冲突,C 语言规定全局变量和函数经过编译后,相应的符号名前加上下划线。但对于大型程序来说,这种方法不能从根本上解决问题。C++ 增加了命名空间的方法来解决多模块的符号冲突问题。
C++ 的符号修饰方法如下:所有的符号以 _Z 开头;对于嵌套(命名空间或类)的名字,后面紧跟 N,然后是类或命名空间的名字,每个名字前是字符串长度;再以 E 结尾。如 N::C::func 经过修饰后得到 _ZN1N1C4funcE。对于函数来说,它的参数列表紧跟在 E 后,如果是整型则为 i 等。使用 c++filt 工具可以用来解析被修饰过的名称:
$ c++filt _ZN1N1C4funcEi
N::C::func(int)
4.3 extern “C”
C++ 为了兼容 C,在符号的管理上,使用关键字 extern “C” 来声明或定义 C 的符号。C++ 编译器会将 extern “C” 大括号内部的代码当作 C 语言来处理。
extern "C"
{
int func(int);
int var;
}
4.4 弱符号和强符号
在符号的定义中,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。针对强弱符号的概念,链接器按如下规则选择多次被定义的全局符号:
- 不允许强符号被多次定义,如果有多个强符号定义,则链接器报符号重复定义的错误。
- 如果一个符号在某个目标文件中是强符号,则在其他文件中都是弱符号,且选择强符号。
- 如果一个符号在所有目标文件中都是弱符号,那么选择内存占用较大的符号。
5. 总结
- 目标文件 ELF 是 Linux 下的常见文件类型,主要包括可重定位文件、可执行文件、共享目标文件和核心转储文件四类。
- 目标文件的内容以段的形式组织,各段内数据属性相似。
- ELF 文件主要包括文件头、段表、重定位表和字符串表等。
- 在链接过程中,符号用于耦合各目标文件,链接器根据符号类型选择合适的强符号或弱符号。
|