文章目录
- 静态链接
- 符号解析 (symbolresolution)
- 重定位 (relocation)
- 目标文件
- 1.可重定位目标文件
- 2.可执行目标文件
- 3.共享目标文件
- 可重定位目标文件
- text:
- rodata:
- .data
- .bss
- .symtab
- .rel.text
- .rel.data:
- debug:
- line:
- strtab:
- 符号和符号表
- 由m定义并能被其他模块引用的全局符号
- 由其他模块定义并被模块 m引用的全局符号
- 只被模块 m 定义和引用的本地符号
- 本地链接器符号和本地程序变量的区别
静态链接
像Unix ld程序这样的静态链接器 (static linker)以一组可重定位目标文件和命令行参数作为转入,生成一个完全链接的可以加载和运行的可执行目标文件作为输出。
输入的可重定位目标文件由各种不同的代码和数据节 (section) 组成。指令在一个节中,初始化的全局变量在另一个节中,而未初始化的变量又在另外一个节中。
为了创建可执行文件,链接器必须完成两个主要任务:
符号解析 (symbolresolution)
目标文件定义和引用符号。符号解析的目的是将每个符号用和一个符号定义联系起来。
重定位 (relocation)
编译器和汇编器生成从地址零开始的代码和数据节。链接器通过把每个符号定义与一个存储器位置联系起来,然后修改所有对这些符号的引用,使得它们指向这个存储器位置,从而重定位这些节。
目标文件纯粹是字节块的集合。
这些块中,有些包含程序代码,有些则包含程序数据,
而其他的则包含指导链接器和加载器的数据结构。
链接器将这些块连接起来,确定被链接块的运行时位置并且修改代码和数据块中的各种位置。链接器对目标机器了解甚少,产生目标文件的编译器和汇编器已经完成了大部分工作。
目标文件
目标文件有三种形式:
1.可重定位目标文件
包含二进制代码和数据,其形式可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件
2.可执行目标文件
包含二进制代码和数据,其形式可以被直接拷贝到存储器并执行。
3.共享目标文件
一种特殊类型的可重定位目标文件,可以在加载或者运行时,被动态地加载到存储器并链接。
编译器和汇编器生成可重定位目标文件(包括共享目标文件)。
链接器生成可执行目标文件。
从技术上来说,一个目标模块 (obiect module)就是一个字节序列,而一个目标文件 (object file)就是一个存放在磁盘文件中的目标模块。
各个系统之间,目标文件格式都不相同。第一个从贝尔实验室诞生的 Umix 系统使用的是 a.out格式(直到今天,可执行文件仍然指的是 a.out 文件)。
System V Unix 的早期版本使用的是 COFF(Common Obiect File
forat,一般目标文件格式)。Windows 使用的是 COFF 的一个变种,叫做PE(Portable
Executable,可移植可执行)格式。现代 Unix 系统-比如 Linux,还有 System V Unix后来的版本,各种 BSD
Unix,以及 SUN Solaris 一使用的是 Unix ELF (Executable and
LinkableFommat,可执行和可链接格式)。尽管我们的讨论集中在 ELF 上,但是不管是哪种格式,基本的概念是相似的。
可重定位目标文件
下图展示了一个典型的ELF 可重定位目标文件。
ELF头(ELF header)以一个 16字节的序列开始,这序列描述了字的大小和生成该文件的系统的字节顺序。
ELF 头剩下的部分包含帮助链接器解析和解释目标文件的信息。
其中包括 ELF 头的大小、目标文件的类型(比如,可重定位、可执行或者是共享的)、机器类型(比如,IA32)、节头部表 (section header table) 的文件偏移,以及节头部表中的表目大小和数量。
不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的表目 (entry)。
夹在 ELF 头和节头部表之间的都是节。一个典型的 ELF 可重定位目标文件包含下面几个节:
text:
已编译程序的机器代码
rodata:
只读数据,比如 printf 语句中的格式串和开关(switch)语句的跳转表。
.data
已初始化的全局 C 变量。局部 C 变量在运行时被保存在栈中,既不出现在.data 节中,也不出现在.bss 节中。
.bss
未初始化的全局C 变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。目标文件格式区分初始化和未初始化变量是为了空间效率:在目标文件中,未初始化变量不需要占据任何实际的磁盘空间。
.symtab
一个符号表 (symbol table),它存放在程序中被定义和引用的函数和全局变量的信息一些程序员错误地认为必须通过-g 选项来编译一个程序,得到符号表信息。实际上,每人可重定位目标文件在.symtab 中都有一张符号表。然而,和编译器中的符号表不同,symtab 符号表不包含局部变量的表目。
.rel.text
当链接器把这个目标文件和其他文件结合时,.text 节中的许多位置都需要修改。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面,调用本地函数的指令则不需要修改。注意,可执行目标文件中并不需要重定位信息,因此通常省略,除非使用者显式地指示链接器包含这些信息。
.rel.data:
被模块定义或引用的任何全局变量的信息。一般而言,任何已初始化全局变量的初始值是全局变量或者外部定义函数的地址都需要被修改。
debug:
一个调试符号表,其有些表目是程序中定义的局部变量和类型定义,有些表目是程序中定义和引用的全局变量,有些是原始的 C 源文件。只有以- 选项调用编译驱动程序时,才会得到这张表。
line:
原始 C 源程序中的行号和text 节中机器指令之间的映射。只有以-g 选项调用编译驱动程序时,才会得到这张表。
strtab:
一个字符串表,其内容包括symtab 和debug 节中的符号表,以及节头部中的节名字。字符串表就是以 null 结尾的字符串序列。
符号和符号表
每个可重定位目标模块 m 都有一个符号表,它包含 m 所定义和引用的符号的信息。在链接器的上下文中,有三种不同的符号:
由m定义并能被其他模块引用的全局符号
由m定义并能被其他模块引用的全局符号。全局链接器符号对应于非静态的 C 函数以及被定义为不带 C的 static 属性的全局变量。
由其他模块定义并被模块 m引用的全局符号
由其他模块定义并被模块 m引用的全局符号。这些符号称为外部符号 (exteal),对应于定义在其他模块中的 C函数和变量。
只被模块 m 定义和引用的本地符号
只被模块 m 定义和引用的本地符号。有的本地链接器符号对应于带 static 属性的 C 函数和全局变量。这些符号在模块 m 中的任何地方都是可见的,但是不能被其他模块引用。目标文件中对应于模块 m 的节和相应的源文件的名字也能获得本地符号。
本地链接器符号和本地程序变量的区别
认识到本地链接器符号和本地程序变量的不同是很重要的。.symtab 中的符号表不包含对应于本地非静态程序变量的任何符号。这些符号在运行时在栈中被管理,链接器对此类符号不感兴趣。
有趣的是,定义为带有 C static 属性的本地过程变量是不在栈中管理的。取而代之,编译器在.data和.bss 中为每个定义分配空间,并在符号表中创建一个有惟一名字的本地链接器符号。
比如,假设在同一模块中的两个函数定义了一个静态本地变量 x:
int f() {
static int x = 0;
return x;
}
int g() {
static int x = 1;
return x;
}
在这种情况中,编译器在bss 中为两个整数分配空间,并引出 (export)两个惟一的本地链接器符号给汇编器。
比如,它可以用 x.1 表示函数f中的定义,而用 x.2 表示数 g中的定义。
C 程序员使用 static 属性在模块内部隐藏变量和函数声明,就像你在 Java 和 C++中使用 public和 private 声明一样。C 源代码文件扮演模块的角色。任何声明带有 static 属性的全局变量或者函数都是模块私有的。类似地,任何声明为不带 staic 属性的全局变量和函数都是公共的,可以被其他莫块访问,尽可能用 static 属性来保护你的变量和函数是很好的编程习惯。
符号表是由汇编器构造的,使用编译器输出到汇编语言s 文件中的符号。symtab 节中包含 ELF符号表。这张符号表包含一个关于表目的数组。图7.4 展示了每个表目 (entry)的格式
typedef struct {
int name; /* string table offset */
int value; /* section offset, or VM address */
int size; /* object size in bytes */
char type:4; /* data, func, section, or src file name (4 bits) */
char binding:4; /* local or global (4 bits) */
char reserved; /* unused */
char section; /* section header index, ABS, UNDEF, or COMMON */
} Elf_Symbol;
这段代码定义了一个名为Elf_Symbol
的结构体,用于表示ELF格式的符号表中的一个符号。该结构体包含了以下成员:
name
:整型变量,表示符号名在字符串表中的偏移量。value
:整型变量,表示符号在节区(Section)中的偏移量,或者在虚拟内存地址中的地址。size
:整型变量,表示符号所占空间的大小。type
:一个占据4个比特位的字符变量,表示符号的类型。共有四种取值:data(数据)、func(函数)、section(节区)和src file name(源文件名)。binding
:一个占据4个比特位的字符变量,表示符号的绑定类型。共有两种取值:local(局部)和global(全局)。reserved
:一个字节的保留字段。section
:一个字节的变量,表示符号所在的节区的索引,或者特殊的值如ABS(绝对符号)、UNDEF(未定义符号)或COMMON(常量符号)。
好的,下面是一个简单的GUN READELF工具显示例子:
- 首先,我们需要创建一个简单的可执行文件,例如下面一个C语言程序:
// main.c
#include <stdio.h>
int main(void) {
printf("Hello, World!\n");
return 0;
}
我们可以使用以下命令将其编译成可执行文件:
gcc -o hello main.c
- 然后,我们可以使用readelf工具来查看可执行文件的头部信息,命令如下:
readelf -h hello
输出:
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: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x400440
Start of program headers: 64 (bytes into file)
Start of section headers: 9552 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 30
Section header string table index: 27
该命令将会显示可执行文件的ELF头部信息,包括识别码(Magic)、文件类型(Type)、目标机器(Machine)、入口点地址(Entry point address)等等。
- 我们还可以使用readelf来查看可执行文件的符号表信息,命令如下:
readelf -s hello
输出:
Symbol table '.symtab' contains 72 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000400230 0 SECTION LOCAL DEFAULT 1
2: 0000000000400250 0 SECTION LOCAL DEFAULT 2
3: 0000000000400270 0 SECTION LOCAL DEFAULT 3
4: 0000000000400290 0 SECTION LOCAL DEFAULT 4
5: 00000000004002b0 0 SECTION LOCAL DEFAULT 5
6: 00000000004002d0 0 SECTION LOCAL DEFAULT 6
7: 00000000004002f0 0 SECTION LOCAL DEFAULT 7
8: 0000000000400310 0 SECTION LOCAL DEFAULT 8
9: 0000000000400330 0 SECTION LOCAL DEFAULT 9
10: 0000000000400350 0 SECTION LOCAL DEFAULT 10
11: 0000000000400370 0 SECTION LOCAL DEFAULT 11
12: 0000000000400390 0 SECTION LOCAL DEFAULT 12
13: 00000000004003b0 0 SECTION LOCAL DEFAULT 13
14: 00000000004003d0 0 SECTION LOCAL DEFAULT 14
15: 00000000004003f0 0 SECTION LOCAL DEFAULT 15
16: 0000000000400410 0 SECTION LOCAL DEFAULT 16
17: 0000000000400430 0 SECTION LOCAL DEFAULT 17
18: 0000000000400450 0 SECTION LOCAL DEFAULT 18
19: 0000000000400470 0 SECTION LOCAL DEFAULT 19
20: 0000000000400490 0 SECTION LOCAL DEFAULT 20
21: 00000000004004b0 0 SECTION LOCAL DEFAULT 21
22: 00000000004004d0 0 SECTION LOCAL DEFAULT 22
23: 00000000004004f0 0 SECTION LOCAL DEFAULT 23
24: 0000000000400510 0 SECTION LOCAL DEFAULT 24
25: 0000000000400530 0 SECTION LOCAL DEFAULT 25
26: 0000000000400550 0 SECTION LOCAL DEFAULT 26
27: 0000000000400570 0 SECTION LOCAL DEFAULT 27
28: 0000000000400590 0 SECTION LOCAL DEFAULT 28
29: 00000000004005b2 0 FUNC GLOBAL DEFAULT 14 main
30: 0000000000601008 0 OBJECT GLOBAL DEFAULT 23 stdout
31: 0000000000000000 0 OBJECT GLOBAL DEFAULT ABS __bss_start
32: 0000000000601008 0 NOTYPE GLOBAL DEFAULT 23 _edata
33: 0000000000601018 0 OBJECT GLOBAL DEFAULT 24 _end
34: 0000000000400600 0 FUNC GLOBAL DEFAULT 14 _start
35: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _Jv_RegisterClasses
36: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@@GLIBC_2.2.5
37: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
38: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTable
39: 0000000000000000 0 FUNC WEAK DEFAULT UND __libc_start_main@@GLIBC_2.2.5
40: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
41: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __cxa_finalize@@GLIBC_2.2.5
该命令将会显示可执行文件的符号表信息,包括符号的值(Value)、大小(Size)、类型(Type)、绑定类型(Bind)、可见性(Vis)以及所在的节区(Ndx)等等。
当然,readelf工具还有很多其他的选项和功能,以上只是其中的一些示例,你可以通过查阅文档来了解更多信息。