4、汇编阶段:汇编器的作用,从汇编代码到机器码(.o/.obj文件),符号表的生成
好,咱们进入编译的第四站——汇编阶段。
说实话,很多初学者觉得这步没啥好讲的。不就是把汇编代码转成机器码吗?嗯,表面上看确实是这样。但我在项目中踩过几次坑之后才发现,这步要是理解不透,后面链接出问题你根本不知道从哪查起。
4.1 汇编器到底在干什么?
汇编器,说白了就是个翻译官。它把咱们写的汇编指令,一条条翻译成CPU能直接执行的二进制机器码。
你想想看,你在汇编里写了个 mov eax, 0x10,CPU可不认识这串字符。它只认0和1。汇编器的工作就是查指令编码表,把 mov eax, imm32 这个操作码翻译成 0xB8,再把立即数 0x10 拼在后面。
我个人习惯把汇编器的工作分成两大部分:
- 指令翻译:把助记符转成操作码,把寄存器、立即数、内存地址转成对应的二进制编码
- 符号处理:处理标签、变量名,生成符号表,为链接阶段做准备
这里有个关键点——汇编器是逐行翻译的。它不会像编译器那样做全局优化,也不会像链接器那样跨文件分析。它就是个老实人,你给它一行,它翻译一行。
核心认知:汇编器输出的 .o 文件(Linux)或 .obj 文件(Windows),并不是一个完整的可执行程序。它里面还有很多"坑"等着链接器去填。
4.2 从汇编到机器码:一条指令的旅程
咱们拿个具体的例子走一遍。假设你有这么一段汇编代码:
section .text
global _start
_start:
mov eax, 1 ; sys_write 的系统调用号
mov edi, 1 ; 文件描述符 stdout
lea rsi, [msg] ; 字符串地址
mov edx, 13 ; 字符串长度
syscall
section .data
msg db 'Hello, World!', 0
汇编器处理 mov eax, 1 时,它会查表找到 mov eax, imm32 的编码是 0xB8,然后后面跟上4字节的立即数 0x00000001。最终生成 B8 01 00 00 00。
但注意 lea rsi, [msg] 这行——msg 是个符号,它的地址现在还不知道!汇编器只能先占个位置,填个临时值,然后在符号表里记一笔:"msg 这个符号,在 .data 节区,偏移量是 0"。
为什么会这样?因为汇编器处理的是单个文件。它不知道 msg 这个符号会不会被其他文件引用,也不知道最终加载到内存的哪个地址。这些事,得留给链接器。
我的经验:调试汇编代码时,可以用 objdump -d 查看反汇编结果。你会看到那些未确定的地址被标成了 0x0 或者 R_X86_64_PC32 这样的重定位标记。看到这些标记别慌,说明链接器还没干活呢。
4.3 符号表:汇编器的"通讯录"
符号表,你可以把它理解成汇编器给链接器留的一本通讯录。里面记录了当前文件定义和引用了哪些符号,每个符号在哪个节区,偏移量是多少。
咱们用 readelf -s 看看上面那段代码生成的符号表:
Symbol table '.symtab' contains 7 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 SECTION LOCAL DEFAULT 1
2: 0000000000000000 0 SECTION LOCAL DEFAULT 2
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 NOTYPE GLOBAL DEFAULT 1 _start
5: 0000000000000000 13 OBJECT GLOBAL DEFAULT 3 msg
6: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
注意看几个关键字段:
| 字段 | 含义 | 我的理解 |
|---|---|---|
| Value | 符号的地址或偏移量 | 当前文件内的相对位置,链接后会变成绝对地址 |
| Bind | LOCAL 或 GLOBAL | LOCAL 只在本文件可见,GLOBAL 其他文件也能用 |
| Ndx | 符号所在的节区索引 | UND 表示未定义,需要链接器从别处找 |
| Name | 符号名称 | 就是变量名或函数名 |
这里有个坑,我曾经栽过——全局符号和局部符号的区别。如果你在汇编里用 global 声明了一个符号,它就会出现在符号表里,并且 Bind 是 GLOBAL。其他文件可以通过 extern 引用它。但如果你忘了加 global,默认就是 LOCAL,链接器在其他文件里根本找不到它。
避坑指南:我曾经在一个项目里,把关键函数忘了加 global,结果链接时报"未定义引用"。我查了半小时,最后发现是汇编文件里少写了一个 global 声明。嗯,从那以后我写汇编的第一件事就是检查符号声明。
4.4 重定位表:给链接器的"待办清单"
除了符号表,汇编器还会生成一个重定位表。这个表记录了所有需要链接器帮忙"填空"的位置。
拿上面的 lea rsi, [msg] 来说,汇编器在生成机器码时,会在 msg 地址的位置填上 0x00000000,然后在重定位表里记一笔:
- 位置:.text 节区偏移量 0x12 处
- 类型:R_X86_64_PC32(相对寻址)
- 符号:msg
- 加数:-4(x86_64 的 PC 相对寻址需要减掉指令长度)
链接器拿到这个表后,就知道:"哦,在 .text 的 0x12 位置,有个对 msg 的引用,我得把 msg 的最终地址算出来填进去。"
你可以用 readelf -r 查看重定位表:
Relocation section '.rela.text' at offset 0x250 contains 1 entry:
Offset Info Type Sym. Value Sym. Name + Addend
000000000012 000500000002 R_X86_64_PC32 0000000000000000 msg - 4
4.5 汇编器输出的文件结构
最后,咱们看看 .o 文件里面到底长什么样。用 readelf -h 可以查看 ELF 文件头,但更直观的是看节区分布:
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
000000000000001c 0000000000000000 AX 0 0 16
[ 2] .data PROGBITS 0000000000000000 0000005c
000000000000000d 0000000000000000 WA 0 0 4
[ 3] .symtab SYMTAB 0000000000000000 00000070
00000000000000a8 0000000000000018 4 5 8
[ 4] .strtab STRTAB 0000000000000000 00000118
0000000000000023 0000000000000000 0 0 1
[ 5] .rela.text RELA 0000000000000000 00000140
0000000000000018 0000000000000018 3 1 8
看到没?.o 文件里不光有代码和数据,还附带了符号表、字符串表、重定位表。这些"额外信息"在最终的可执行文件里是不需要的,但链接器必须靠它们才能完成工作。
小技巧:想快速查看 .o 文件的符号表,用 nm 命令比 readelf 更直观。比如 nm test.o 会直接列出所有符号及其类型。
4.6 本章核心流程图
下面这张图总结了汇编器的工作流程,从汇编代码到 .o 文件的完整路径:
这张图把汇编器的三个核心产出物串起来了。你注意看,机器码节区、符号表、重定位表是并行生成的。汇编器扫一遍代码,同时干三件事——翻译指令、记录符号、标记重定位点。
好了,汇编阶段就聊到这儿。记住一句话:汇编器输出的 .o 文件,是个"半成品"。它里面有完整的机器码,但地址还没定下来。符号表和重定位表就是给链接器留的"施工图纸"。下一阶段,链接器就要拿着这些图纸,把各个 .o 文件拼成一个完整的可执行程序。
一句话总结:汇编器把汇编代码翻译成机器码,同时生成符号表和重定位表。符号表是"通讯录",重定位表是"待办清单"。这两张表,是链接器能干活的基础。