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 文件的完整路径:

汇编器工作流程 汇编代码 (.s / .asm) 汇编器逐行翻译 指令编码 + 符号处理 + 重定位记录 机器码节区 .text / .data / .rodata 符号表 .symtab + .strtab 重定位表 .rela.text / .rela.data 目标文件 (.o / .obj) 生成 生成 生成

这张图把汇编器的三个核心产出物串起来了。你注意看,机器码节区、符号表、重定位表是并行生成的。汇编器扫一遍代码,同时干三件事——翻译指令、记录符号、标记重定位点。

好了,汇编阶段就聊到这儿。记住一句话:汇编器输出的 .o 文件,是个"半成品"。它里面有完整的机器码,但地址还没定下来。符号表和重定位表就是给链接器留的"施工图纸"。下一阶段,链接器就要拿着这些图纸,把各个 .o 文件拼成一个完整的可执行程序。

一句话总结:汇编器把汇编代码翻译成机器码,同时生成符号表和重定位表。符号表是"通讯录",重定位表是"待办清单"。这两张表,是链接器能干活的基础。

公众号:蓝海资料掘金营,微信 deep3321