高级语言程序需要经过多次转换才能变成可执行的机器代码。
源代码 → 目标代码
↓
翻译程序:编译、汇编、链接
| 类型 | 英文 | 输入 | 输出 | 功能 |
|---|---|---|---|---|
| 预处理器 | Preprocessor | .c | .i | 处理宏指令、文件包含 |
| 编译器 | Compiler | .i | .s | 翻译成汇编语言 |
| 汇编器 | Assembler | .s | .o | 翻译成机器语言 |
| 链接器 | Linker | 多个.o | 可执行文件 | 合并、重定位、符号解析 |
处理内容:
- #include:文件包含,将头文件内容插入源程序
- #define:宏定义替换
- #if/#else/#endif:条件编译
例:
#include <stdio.h> // 被stdio.h的内容替换
#define MAX 100 // 后续所有MAX被替换为100
输入:预处理后的源程序(.i) 输出:汇编语言程序(.s)
编译过程: 1. 词法分析:分解成单词 2. 语法分析:生成语法树 3. 语义分析:类型检查 4. 优化:删除冗余运算 5. 代码生成:生成目标机器的汇编代码
输入:汇编语言程序(.s) 输出:可重定位目标文件(.o)
汇编器的功能: - 将汇编指令翻译成机器指令 - 生成符号表(定义和引用) - 生成重定位信息
输入:多个可重定位目标文件(.o) 输出:可执行目标文件
链接器的功能: 1. 符号解析:将所有符号引用与符号定义配对 2. 重定位:合并节,调整地址 3. 地址分配:为每个符号分配最终运行地址
ELF(Executable and Linkable Format):可执行和可链接格式
Linux/Unix系统标准的可执行文件格式。
┌─────────────────┐
│ ELF头 │ 文件类型、入口地址、程序头偏移
├─────────────────┤
│ 程序头表 │ 段信息(运行时需要)
├─────────────────┤
│ .text │ 代码段(机器指令)
├─────────────────┤
│ .data │ 已初始化数据段
├─────────────────┤
│ .bss │ 未初始化数据段
├─────────────────┤
│ .symtab │ 符号表
├─────────────────┤
│ .rel │ 重定位信息
├─────────────────┤
│ .strtab │ 字符串表
├─────────────────┤
│ 节头表 │ 节信息(链接时需要)
└─────────────────┘
| 节名 | 作用 | 内容 |
|---|---|---|
| ELF头 | 文件基本信息 | 类型(ET_EXEC)、入口地址、程序头偏移 |
| .text | 代码段 | 机器指令,只读 |
| .data | 已初始化数据段 | 全局变量、静态变量初值 |
| .bss | 未初始化数据段 | 未初始化的全局/静态变量(不占文件空间) |
| .rodata | 只读数据段 | 常量数据 |
| .symtab | 符号表 | 所有符号(函数名、变量名) |
| .rel/.rela | 重定位表 | 需要重定位的符号信息 |
| .strtab | 字符串表 | 符号名字符串 |
| .shstrtab | 节名字符串表 | 节名字符串 |
| 类型 | 值 | 说明 |
|---|---|---|
| ET_REL | 1 | 可重定位目标文件(.o) |
| ET_EXEC | 2 | 可执行文件 |
| ET_DYN | 3 | 共享对象文件(.so) |
| ET_CORE | 4 | Core dump文件 |
| 类型 | 说明 | 示例 |
|---|---|---|
| 全局符号定义 | 在本模块定义,可被其他模块引用 | int global_var; |
| 全局符号引用 | 引用其他模块定义的符号 | extern int ext_var; |
| 本地符号 | 仅在模块内部使用 | static int static_var; |
| 函数名 | 函数的起始地址 | void func(){} |
| 符号名 | 地址 | 类型 | 绑定 | 所在节 |
|---|---|---|---|---|
| main | 0x0 | 函数 | 全局 | .text |
| global_var | 0x100 | 数据 | 全局 | .data |
| buf | 0x200 | 数据 | 全局 | .bss |
| temp | - | 未定义 | 外部 | - |
问题:当多个目标文件链接时,如何找到符号的定义位置?
解析过程:
文件A (.o) 文件B (.o)
┌──────────┐ ┌──────────┐
│ call foo │ ←── │ foo定义 │
│ (引用) │ │ (全局符号)│
└──────────┘ └──────────┘
↓ ↑
└─────── 链接器找到foo的定义 ───────┘
步骤: 1. 链接器扫描所有输入文件 2. 建立全局符号表(所有定义) 3. 对每个未解析的引用,在表中查找匹配的定义 4. 若找不到,报"undefined symbol"错误
为什么需要重定位? - 每个.o文件是独立编译的,地址从0开始 - 链接时需要合并所有.o文件,重新计算地址
重定位过程:
1. 合并所有节的同类节
.text节A + .text节B → 新的.text节
2. 分配运行地址
.text节 → 地址0x08048000开始
3. 调整符号地址
foo原在A的0x10 → 新地址0x08048010
4. 修改引用
call指令中的foo地址 → 更新为新地址
| 类型 | 说明 | 示例 |
|---|---|---|
| R_X86_64_PC32 | 相对PC的32位地址 | call指令 |
| R_X86_64_32 | 绝对32位地址 | 全局变量引用 |
| R_X86_64_PLT32 | PLT相关调用 | 函数调用 |
假设: - main.o中调用函数foo,foo定义在foo.o - main.o编译时,假设foo在地址0x00(重定位条目) - 链接后,foo被分配到运行地址0x400500
链接器执行重定位:
原指令:e8 00 00 00 00 (call foo, 目标地址=PC+4+0)
重定位后:e8 xx xx xx xx (目标地址=foo的实际地址)
当在shell中输入命令时:
$ ./hello
↓
Shell调用execve("hello", argv, envp)
↓
内核创建新进程
↓
读取ELF文件头,验证格式
↓
读取程序头表,映射段到进程地址空间
↓
设置初始PC值(入口地址)
↓
开始执行
高地 ┌────────────────┐
│ 栈 │ ← SP寄存器
├────────────────┤
│ ↓ │
│ (未使用) │
│ ↑ │
├────────────────┤
│ ↓ │
│ 堆 │ ← malloc/new
├────────────────┤
│ .data .bss │ ← 全局变量
├────────────────┤
│ .text │ ← 代码
└────────────────┘
低地 0x08048000
// 原型
int execve(const char *filename, char *const argv[], char *const envp[]);
// 示例
execve("./hello", argv, envp);
execve工作步骤: 1. 检查文件是否为可执行格式(ELF) 2. 读取ELF头信息 3. 创建新的代码、数据段映射 4. 设置栈(参数、环境变量) 5. 设置PC指向入口地址
特点: - 编译时链接 - 所有代码加入最终可执行文件 - 文件体积大,更新需重新编译
特点: - 运行时链接 - 多个进程共享同一份库代码 - 节省内存和磁盘空间 - 更新库无需重新编译程序
加载时链接(运行时加载):
1. 加载器读取动态段信息
2. 加载所需的共享库
3. 进行符号解析和重定位
运行时链接(延迟绑定):
1. 首次调用函数时
2. 通过PLT/GOT跳转
3. 解析符号地址
4. 更新GOT,下次直接调用
将汇编语言程序翻译成机器语言程序的是( ) A. 预处理器 B. 编译器 C. 汇编器 D. 链接器
ELF文件中,不占磁盘空间的是( ) A. .text B. .data C. .bss D. .rodata
符号解析的主要工作是( ) A. 合并节 B. 将符号引用与定义配对 C. 调整地址 D. 分配运行地址
可执行文件的入口地址保存在ELF的哪部分?( ) A. .text B. 程序头表 C. ELF头 D. 符号表
动态链接库的文件扩展名通常是( ) A. .o B. .a C. .so D. .exe
翻译程序包括预处理器、编译器、______和链接器。
ELF是______的缩写。
.bss节用于存放______的全局/静态变量。
重定位类型R_X86_64_PC32表示______地址。
Shell执行程序时调用的是______系统调用。
简述从源代码到可执行文件的转换过程。
说明ELF文件中.text、.data、.bss三个节的作用。
什么是符号解析?为什么需要符号解析?
什么是重定位?链接器执行重定位的步骤是什么?
比较静态链接和动态链接的优缺点。
假设有两个源文件main.c和foo.c:
main.c:
extern int result;
int main() {
result = foo(10);
return 0;
}
foo.c:
int result;
int foo(int x) {
return x * 2;
}
请回答: 1. 在foo.o中,result是什么类型的符号?foo呢? 2. 在main.o中,result和foo是什么类型的符号? 3. 链接过程中如何解析这些符号?
result 是全局符号定义(定义并初始化)foo 是全局符号定义(函数定义)
在main.o中:
result 是全局符号引用(外部引用)foo 是全局符号引用(外部引用)
符号解析过程: