Skip to content

Commit 56d7794

Browse files
committed
docs: detailed explanations on linking, ELF format, etc.
1 parent 75dfc18 commit 56d7794

File tree

2 files changed

+114
-78
lines changed

2 files changed

+114
-78
lines changed

README.md

Lines changed: 75 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,18 @@
3030
3131
[![GitHub Issues](https://img.shields.io/github/issues/RUCICS/LinkLab-2024-Assignment?style=for-the-badge&logo=github)](https://github.com/RUCICS/LinkLab-2024-Assignment/issues)
3232

33+
## 什么是链接?
34+
35+
链接是将多个目标文件组合成一个可执行程序的过程。在现代软件开发中,我们很少会把所有代码都写在一个文件里 —— 这样既不利于代码复用,也不便于团队协作。相反,我们会将程序分解成多个源文件,分别编译成目标文件,再通过链接器将它们"拼接"在一起。
36+
37+
链接器的主要工作包括:
38+
39+
1. 符号解析:将代码中的符号(如函数调用、全局变量)与它们的定义对应起来
40+
2. 重定位:调整符号的地址,确保程序在内存中正确运行
41+
3. 布局规划:合理安排各个部分在内存中的位置,并设置适当的访问权限
42+
43+
这个过程看似简单,但实际上涉及许多复杂的细节:如何处理符号冲突?如何保护代码不被篡改?如何优化内存布局?在这个实验中,你将亲手实现这些功能,深入理解现代程序是如何被组装起来的。
44+
3345
## 环境要求
3446

3547
- 操作系统:Linux(推荐 Ubuntu 22.04 或更高版本)
@@ -172,21 +184,22 @@ FLE 使用表情符号来标记不同类型的信息:
172184

173185
```cpp
174186
struct FLEObject {
175-
std::string type; // 文件类型(.obj/.exe)
176-
std::map<std::string, FLESection> sections; // 各个节
177-
std::vector<Symbol> symbols; // 符号表
178-
std::vector<ProgramHeader> phdrs; // 程序头
179-
std::vector<SectionHeader> shdrs; // 节头
180-
size_t entry = 0; // 入口点(仅可执行文件)
187+
std::string name; // Object name
188+
std::string type; // ".obj" or ".exe"
189+
std::map<std::string, FLESection> sections; // Section name -> section data
190+
std::vector<Symbol> symbols; // Global symbol table
191+
std::vector<ProgramHeader> phdrs; // Program headers (for .exe)
192+
std::vector<SectionHeader> shdrs; // Section headers
193+
size_t entry = 0; // Entry point (for .exe)
181194
};
182195

183196
struct FLESection {
184-
std::vector<uint8_t> data; // 节数据(按字节存储)
185-
std::vector<Relocation> relocs; // 重定位表
186-
bool has_symbols; // 是否包含符号
197+
std::vector<uint8_t> data; // Section data (stored as bytes)
198+
std::vector<Relocation> relocs; // Relocation table for this section
199+
bool has_symbols; // Whether section contains symbols
187200
};
188201

189-
// 其他结构体定义请参考 include/fle.hpp,此处略
202+
// ……
190203
```
191204
192205
我们的实验框架会自动将 FLE 文件加载为内存中的数据结构 `FLEObject`。
@@ -214,15 +227,16 @@ struct FLESection {
214227
3. 在生成可执行文件时确定最终的内存布局
215228
216229
> [!NOTE]
217-
> 在 ELF 格式中,有两种不同的视图:
218-
> - 链接视图:由节头表(Section Headers)描述,包含了链接时需要的详细信息,如 `.text`、`.data`、`.bss` 等节
219-
> - 运行视图:由程序头表(Program Headers)描述,定义了程序运行时的内存段(segment)
230+
> ELF格式中存在两种重要的视图:
231+
>
232+
> - **链接视图** 由节头(Section Headers)表描述,包含链接时需要的详细信息,如 `.text`、`.data`、`.bss` 等节。这些信息主要用于链接和调试,在最终的可执行文件中并非必需。
233+
> - **运行视图** 由程序头(Program Headers)表描述,定义了程序运行时的内存段(segment)。它描述如何将文件映射到内存,是加载程序(loader)必需的信息,在可执行文件中不可或缺。
220234
>
221-
> 在标准的 ELF 中,一个内存段通常会包含多个具有相同权限需求的节。例如,可读写的数据段通常同时包含 `.data` 和 `.bss` 节,它们会被映射到同一块连续的内存区域中。这种设计可以减少内存碎片,简化程序的加载过程。
235+
> 在标准的ELF中,一个内存段通常会包含多个具有相同权限需求的节。例如,可读写的数据段通常同时包含 `.data` 和 `.bss` 节,它们会被映射到同一块连续的内存区域中。这种设计可以减少内存碎片,简化程序的加载过程。
222236
>
223-
> 为了让同学们专注于理解链接的基本概念,我们的 FLE 格式采用了简化的设计
237+
> 为了让同学们专注于理解链接的基本概念,我们的FLE格式采用了简化的设计
224238
> - 在任务六之前,所有内容都放在一个名为 `.load` 的节中
225-
> - 在任务六中,我们将代码和数据分开放入不同的节`.text`、`.data` 等
239+
> - 在任务六中,我们将代码和数据分开放入不同的节(`.text`、`.data` 等)
226240
> - 但始终保持节和段的一对一映射:每个节都被单独映射为一个内存段
227241
>
228242
> 这种简化虽然不够优化,但更容易理解和实现。在完成实验后,你会对这两种视图都有深入的认识。
@@ -285,9 +299,20 @@ void count() { // 全局函数
285299

286300
提示:
287301

288-
- 使用 `std::setw``std::setfill` 格式化输出
289-
- 根据 section 字段判断符号位置
290-
- 未定义符号的 section 为空
302+
- 格式化输出可以使用:
303+
```c
304+
printf("%016lx", addr); // C 风格,输出16位的十六进制数,左侧补0
305+
```
306+
307+
```cpp
308+
std::cout << std::setw(16) << std::setfill('0') << std::hex << addr; // C++ 风格
309+
```
310+
- 根据符号的 section 字段判断其位置:
311+
- ".text" - 代码段
312+
- ".data" - 数据段
313+
- ".bss" - BSS段
314+
- 空字符串 - 未定义符号
315+
- 未定义符号的 section 为空字符串
291316

292317
### 验证
293318

@@ -415,20 +440,15 @@ Relocation {
415440
- 验证截断后的值是否合法
416441
- 将 0x401000 写入重定位位置
417442

418-
最后,生成可执行文件。在内存中,我们需要创建一个新的 `FLEObject`,设置其 `type` `.exe`,并生成正确的程序头(`ProgramHeader` 结构)以便加载器能够正确地加载程序。
443+
最后,生成可执行文件。在内存中,我们需要创建一个新的 `FLEObject``name` 可以设置为任意字符串(如 "a.out"),但其 `type` 必须为 `.exe`并且需要生成正确的程序头`ProgramHeader` 结构)以便加载器能够正确地加载程序。
419444

420445
在这个阶段,我们只生成一个程序头来描述 `.load` 段。它需要指定:
421446
- 段名(name):`.load`
422447
- 加载地址(vaddr):我们使用 0x400000
423448
- 段的大小(size):合并后的总大小
424449
- 权限标志(flags):在这个阶段,我们简单地赋予 RWX(可读、可写、可执行)权限(0b111,即十进制的 7)
425450

426-
> [!IMPORTANT]
427-
> 注意程序头(Program Headers)和节头(Section Headers)的区别:
428-
> - 节头(Section Headers)描述了文件中各个节的属性,主要用于链接和调试。
429-
> - 程序头(Program Headers)描述了程序运行时如何将文件映射到内存,是加载程序(loader)必需的信息。
430-
>
431-
> 在最终的可执行文件中,节头并不是必须的,程序头是必须的。
451+
> [!NOTE] 由于在最终的可执行文件中,节头并不是必须的,所以在这里你可以不生成节头。在这里我们只介绍程序头怎么填。
432452
433453
此外,可执行文件必须指定一个入口点(entry point),也就是程序开始执行的位置。在 x86-64 程序中,这个入口点通常是名为 `_start` 的函数。链接器需要在合并后的符号表中找到 `_start` 符号,并将其地址(基地址 + 符号偏移)设置为程序的入口点。如果找不到 `_start` 符号,应该报错,因为这意味着程序缺少必需的入口点。
434454

@@ -437,14 +457,19 @@ Relocation {
437457
```json5
438458
{
439459
"type": ".exe", // 表明这是一个可执行文件
440-
"phdrs": [{ // 程序头
441-
"name": ".load",
442-
"vaddr": 0x400000, // 固定的加载地址
443-
"size": <总大小>, // 合并后的总大小
444-
"flags": 7 // 可读、可写、可执行
445-
}],
446-
".load": [/* ... */], // 合并后的 .load 节内容,应该只包含机器码
447-
"entry": <入口地址> // 程序的入口点
460+
"phdrs": [
461+
{
462+
"name": ".text", // 程序头
463+
"vaddr": 4194304, // 固定的加载地址 0x400000
464+
"size": 19, // 合并后的总大小
465+
"flags": 5 // 可读、可执行
466+
}
467+
],
468+
".text": [ // 合并后的节内容,只包含机器码
469+
"🔢: 55 48 89 e5 bf 64 00 00 00 b8 3c 00 00 00 0f 05 ",
470+
"🔢: 90 5d c3 "
471+
],
472+
"entry": 4194304 // 程序的入口点 0x400000
448473
}
449474
```
450475

@@ -524,7 +549,7 @@ Relocation {
524549
1. 代码可以加载到内存的任何位置,因为跳转距离是相对的
525550
2. 指令更短,因为偏移量只需要 32 位(而不是完整的 64 位地址)
526551

527-
计算公式很简单:
552+
计算公式���简单���
528553

529554
```
530555
偏移量 = 目标地址 + 附加值 - 重定位位置
@@ -842,11 +867,13 @@ void hack() {
842867
- 合理安排节的顺序以减少内存碎片
843868

844869
> [!IMPORTANT]
845-
> `.bss` 节在链接过程中需要特殊处理。在目标文件中,`.bss` 节并不实际存储数据,而只在节头中记录所需的大小。这是因为 `.bss` 节专门用于存放未初始化或初始化为 0 的全局变量,没有必要在文件中实际存储这些 0 值
870+
> `.bss` 节的处理相对特殊。由于它专门用于存放未初始化或初始化为 0 的全局变量,目标文件中只需在节头(`shdrs`)记录所需的总大小,而无需存储实际数据
846871
>
847-
> 链接器在处理 `.bss` 节时,主要工作是计算符号的新位置。由于符号在 `.bss` 节中的排列可能并不是连续的,因此每个输入文件的符号表都会显式记录该符号在其所属 `.bss` 节中的位置(即偏移量 `offset`)。链接器在合并所有输入文件的节时,需要根据各个节在最终文件中的位置以及每个符号的偏移量,计算符号的新地址
872+
> 链接器在处理 `.bss` 节时,主要是计算符号的新位置。可以直接使用符号在原文件中的偏移量(`offset`),配合节在最终文件中的位置来计算符号的新地址。这些地址会用于后续的重定位计算,但 `.bss` 节本身在最终的可执行文件中仍然不包含实际数据
848873
>
849-
> 这些新计算出的位置会被用于重定位时的地址计算,但 `.bss` 节本身在最终的可执行文件中仍然不包含实际数据。操作系统会在加载程序时,自动将 `.bss` 节对应的内存区域初始化为 0。
874+
> 计算节的最终位置很简单:由于 `.bss` 节只关心总大小,我们只需要从节头获取 size 信息,然后在布局时为其分配相应大小的空间即可。最终每个符号的绝对地址就是:节的基地址 + 符号在节内的偏移量。
875+
>
876+
> 操作系统会在加载程序时,自动将 `.bss` 节对应的内存区域初始化为 0。
850877
851878
完成后,你的链接器就能生成具有正确内存布局和访问权限的可执行文件了。运行测试来验证:
852879

@@ -954,6 +981,16 @@ $$
954981
- 打印中间过程的重要信息
955982
- 分步骤实现,先确保基本功能正确
956983

984+
### 手动测试
985+
每个测试用例目录下都有 `config.toml` 文件描述了测试的执行步骤。建议的手动测试方法是:
986+
987+
1. 进入测试用例目录(如 `tests/cases/2-single-file/`
988+
2. 先执行 `make test` 生成所需的 `.fle` 文件(在 `build` 目录下)
989+
3. 然后就可以直接运行你的链接器:`./ld build/a.fle build/b.fle -o build/out.fle`
990+
4. 使用 `readfle` 检查输出文件:`./readfle build/out.fle`
991+
992+
这样可以更方便地调试单个测试用例。如果需要完整的测试流程,可以参考对应测试目录下的 `config.toml` 文件。
993+
957994
## 进阶内容
958995

959996
完成了基本任务后,你可以尝试:

include/fle.hpp

Lines changed: 39 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,73 +8,72 @@
88

99
using json = nlohmann::ordered_json;
1010

11-
// 重定位类型
11+
// Relocation types
1212
enum class RelocationType {
13-
R_X86_64_32, // 32 位绝对寻址
14-
R_X86_64_PC32, // 32 位相对寻址
15-
R_X86_64_64, // 64 位绝对寻址
16-
R_X86_64_32S, // 32 位有符号绝对寻址
13+
R_X86_64_32, // 32-bit absolute addressing
14+
R_X86_64_PC32, // 32-bit PC-relative addressing
15+
R_X86_64_64, // 64-bit absolute addressing
16+
R_X86_64_32S, // 32-bit signed absolute addressing
1717
};
1818

19-
// 重定位项
19+
// Relocation entry
2020
struct Relocation {
2121
RelocationType type;
22-
size_t offset; // 重定位位置
23-
std::string symbol; // 重定位符号
24-
int64_t addend; // 重定位加数
22+
size_t offset; // Relocation position
23+
std::string symbol; // Symbol to relocate
24+
int64_t addend; // Relocation addend
2525
};
2626

27-
// 符号类型
27+
// Symbol types
2828
enum class SymbolType {
29-
LOCAL, // 局部符号 (🏷️)
30-
WEAK, // 弱全局符号 (📎)
31-
GLOBAL, // 强全局符号 (📤)
32-
UNDEFINED // 未定义符号
29+
LOCAL, // Local symbol (🏷️)
30+
WEAK, // Weak global symbol (📎)
31+
GLOBAL, // Strong global symbol (📤)
32+
UNDEFINED // Undefined symbol
3333
};
3434

35-
// 符号项
35+
// Symbol entry
3636
struct Symbol {
3737
SymbolType type;
38-
std::string section; // 符号所在的节名
39-
size_t offset; // 在节内的偏移
40-
size_t size; // 符号大小
41-
std::string name; // 符号名称
38+
std::string section; // Section containing the symbol
39+
size_t offset; // Offset within section
40+
size_t size; // Symbol size
41+
std::string name; // Symbol name
4242
};
4343

44-
// FLE memory structure
4544
struct FLESection {
46-
std::vector<uint8_t> data; // Raw data
47-
std::vector<Relocation> relocs; // Relocations for this section
48-
bool has_symbols = false;
45+
std::vector<uint8_t> data; // Section data (stored as bytes)
46+
std::vector<Relocation> relocs; // Relocation table for this section
47+
bool has_symbols; // Whether section contains symbols
4948
};
5049

5150
enum class PHF { // Program Header Flags
52-
X = 1, // 可执行
53-
W = 2, // 可写
54-
R = 4 // 可读
51+
X = 1, // Executable
52+
W = 2, // Writable
53+
R = 4 // Readable
5554
};
5655

5756
enum class SHF { // Section Header Flags
58-
ALLOC = 1, // 需要在运行时分配内存
59-
WRITE = 2, // 可写
60-
EXEC = 4, // 可执行
61-
NOBITS = 8, // 不占用文件空间(如BSS)
57+
ALLOC = 1, // Needs memory allocation at runtime
58+
WRITE = 2, // Writable
59+
EXEC = 4, // Executable
60+
NOBITS = 8, // Takes no space in file (like BSS)
6261
};
6362

6463
struct SectionHeader {
65-
std::string name; // 节名
66-
uint32_t type; // 节类型
67-
uint32_t flags; // 节标志
68-
uint64_t addr; // 虚拟地址
69-
uint64_t offset; // 在文件中的偏移
70-
uint64_t size; // 节大小
64+
std::string name; // Section name
65+
uint32_t type; // Section type
66+
uint32_t flags; // Section flags
67+
uint64_t addr; // Virtual address
68+
uint64_t offset; // File offset
69+
uint64_t size; // Section size
7170
};
7271

7372
struct ProgramHeader {
74-
std::string name; // 段名
75-
uint64_t vaddr; // 虚拟地址(64位)
76-
uint64_t size; // 段大小
77-
uint32_t flags; // 权限
73+
std::string name; // Segment name
74+
uint64_t vaddr; // Virtual address (64-bit)
75+
uint64_t size; // Segment size
76+
uint32_t flags; // Permissions
7877
};
7978

8079
struct FLEObject {

0 commit comments

Comments
 (0)