目标文件
在 Windows 系统中,编译和链接这两个步骤被 IDE 高度封装,我们通常只需要一键构建就能完成,使用起来非常方便。
但一旦出现错误,尤其是链接阶段的错误,很多人就会变得束手无策。
在 Linux 环境下,我们之前已经学习过如何直接通过gcc编译器手动完成编译、链接等一系列完整操作。
.o/.obj:可重定位目标文件,简称:目标文件
接下来我们深入探讨编译与链接的完整过程,以便更好地理解动静态库的使用原理。
先来回顾一下:什么是编译?编译的过程,其实就是把我们写的源代码,翻译成 CPU 能够直接运行的机器代码。
但是这里说的“翻译成 CPU 能够直接运行的机器代码”,严格来说是“汇编”做的事。
而“编译”通常指:把高级语言(C/C++)翻译成汇编代码 或 另一种中间表示,然后再由汇编器、链接器完成后续步骤。
不过,在实际工程语境里,大家常把“编译”作为一个笼统的、包含预处理、编译、汇编、链接的完整过程来用。所以你的回顾并不是错,只是在严谨的定义上有模糊。
举个例子:在源文件hello.c中简单输出"hello world!",并且调用一个run函数,而这个函数定义在另一个源文件code.c中。这时我们就可以使用gcc -c分别编译这两个源文件。
// hello.c #include<stdio.h> void run(); int main() { printf("hello world!\n"); run(); return 0; }// code.c #include<stdio.h> void run() { printf("running...\n"); }编译两个源文件:
$ gcc -c hello.c $ gcc -c code.c $ ls code.c code.o hello.c hello.o可以看到,在编译之后会生成两个扩展名为.o的文件,它们被称为目标文件。要注意的是,如果我们修改了一个源文件,那么只需要单独编译它,而不需要浪费时间重新编译整个工程。目标文件是一个二进制文件,文件的格式是 ELF,是对二进制代码的一种封装。(我们可以使用 file 来观察)
注意:文件格式是ELF,它本质上就是对二进制代码、数据、符号信息的一种标准封装格式。
.o目标文件是 ELF 格式.so动态库是 ELF 格式- Linux 下的可执行程序也是 ELF 格式
它们内部结构类似,只是存放的内容和属性不一样。
.a静态库不是 ELF 文件,它是:.a= 把多个.o(ELF 可重定位目标文件)打包在一起的压缩包 / 归档文件
$ file hello.o hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not strippedfile命令用于辨识文件类型。
更具体来说就是:
.o 文件(目标文件)本质上是对机器码、符号表、重定位信息等的封装,且在类 Unix 系统(如 Linux)中,其格式通常就是 ELF(Executable and Linkable Format)。
ELF 文件
代码和数据不能 “一股脑” 塞进二进制文件,而是要采用复杂的 ELF 格式。我们可以从下面几个方面来理解:
为什么不能简单地 "一股脑" 塞进去?
如果把代码和数据简单地 "一股脑" 塞进二进制文件,就像把一堆杂物随意扔进一个大箱子,而不进行任何分类和整理,那么在使用这些代码和数据时就会遇到很多问题:
难以管理:没有结构化的组织方式,很难找到特定的代码或数据。
无法重定位:代码和数据的位置固定,无法在不同环境中灵活使用。
浪费空间:没有优化存储,可能导致大量空间浪费。
难以共享:无法高效地共享代码和数据,每个程序都需要独立携带所有内容。
ELF 文件的类型
ELF 文件主要有三种类型,可以通过 ELF Header 中的e_type成员进行区分。
1. 可重定位文件(Relocatable File):ETL_REL。一般为.o文件。可以被链接成可执行文件或共享目标文件。注意:静态库(.a)内部装的就是这类文件。
2. 可执行文件(Executable File):ET_EXEC。可以直接执行的程序。
3. 共享目标文件(Shared Object File):ET_DYN。一般为.so文件。有两种情况可以使用。
- 链接器将其与其他可重定位文件、共享目标文件链接成新的目标文件;
- 动态链接器将其与其他共享目标文件、结合一个可执行文件,创建进程映像。
ELF 文件的结构
一个 ELF 文件主要由以下几部分组成:
ELF 头(ELF Header):位于文件的开头,描述了文件的主要特性,如文件类型、目标机器架构、入口点地址等。它还包含了程序头表和节头表的偏移量,用于定位文件的其他部分。
程序头表(Program Header Table):描述了文件的段(segments)信息,包括段的类型、偏移量、虚拟地址、文件大小等。段是文件在内存中的映射单元,一个段可以包含多个节。【可以看出:程序头表(Program Header)主要就是给【运行时、动态加载、动态库】服务的!】
节头表(Section Header Table):描述了文件的节(sections)信息,包括节的名称、类型、偏移量、大小等。节是文件的逻辑单元,用于存储不同类型的数据。
也就是说:
ELF 头告诉文件这是 ELF,指明:程序头表在哪?节头表在哪?
程序头表(Program Header)给运行时加载用,和符号表无关。
节头表(Section Header)里面会管理两个关键节:
- .symtab—— 完整符号表(链接用)
- .dynsym—— 动态符号表(运行时用)
节(Section):是 ELF 文件的基本组成单位,包含特定类型的数据。常见的节包括:
lfz@HUAWEI:~/lesson/lesson22/win$ size a.out text data bss dec hex filename 3965 776 16 4757 1295 a.out代码节(.text):存储可执行代码。
数据节(.data):存储已初始化的全局变量和静态变量。(初始化要记录数据)
未初始化数据节(.bss):存储未初始化的全局变量和静态变量,程序加载时会自动初始化为零。(只有在内存展开的时候才开辟空间,然后内容清0,节省的是可执行程序所占据的磁盘空间,因为不用记录初始值 = better save space)
只读数据节(.rodata):存储常量字符串等只读数据。
字符串表(.strtab 和 .shstrtab):存储字符串信息,如节名、符号名等。
说到符号表:
在 Section 当中,我们来简单认识一下.symtab节:【完整符号表】
符号表(Symbol Table)是 ELF 文件格式中的一个重要组成部分,它维护着源码中的函数名、变量名、依赖库与对应地址、代码之间的映射关系。
具体来说就是个 char类型的数组:
char label[] = "helloworld\0func\0libc\0a\0obj\0.....";不过这不是符号表,这是字符串表(.strtab 或 .shstrtab)。
它就是一长串连续的字符数组,所有符号名字都塞在这里,用\0分开。
比如:
"helloworld""func""libc""a""obj"
它们在内存里是连在一起的。
那符号表.symtab存什么?符号表里每条记录,有一个字段叫st_name。
它不存字符串,只存一个数字:偏移量。
比如:
func在字符串表里从第12个字节开始- 那符号表中
func对应的st_name = 12
链接器一看st_name=12,就去字符串表第 12 个位置读,读到func。
st_name(偏移量) | 其他信息(地址、大小等) ------------------------------------------------ 1 | main 函数的信息 5 | printf 函数的信息 16 | func 函数的信息符号表里只存数字(偏移量)
main→ 存数字1printf→ 存数字5
想要知道这个符号叫什么名字:
- 拿到偏移量 → 去字符串表里读字符串
┌─────────────────────────────────────────────────────────────┐ │ .strtab (字符串表) │ │ ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐ │ │ │ \0 │ main \0 │printf \0 │func \0 │he...│ │ │ └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘ │ │ 0 1 5 12 16 │ │ ↑ ↑ ↑ ↑ │ │ "main" "printf" "func" "hello" │ └─────────────────────────────────────────────────────────────┘ ↑ │ 通过偏移量关联 ↓ ┌─────────────────────────────────────────────────────────────┐ │ .symtab (符号表) │ │ ┌──────────────┬──────────────┬──────────────┬──────────┐ │ │ │ st_name = 1 │ st_name = 5 │ st_name = 12 │ st_name=16│ │ │ │ st_value=... │ st_value=... │ st_value=... │ st_value=...│ │ │ │ st_size=... │ st_size=... │ st_size=... │ st_size=...│ │ │ └──────────────┴──────────────┴──────────────┴──────────┘ │ └─────────────────────────────────────────────────────────────┘ 链接器要做的事:读取符号表第2个条目,知道它叫什么名字 步骤1: 看符号表 ┌─────────────────┐ │ st_name = 5 │ ← 拿到一个数字:5 └─────────────────┘ 步骤2: 拿着数字5,去字符串表 ┌─────────────────────────────────────────┐ │ 字符串表