代码如何编译成CPU指令的?

11 阅读7分钟

我们要把编译器处理 2 + 3 * 4 的全过程拆解到“原子级”的精细度。


第一步:词法分析 (Lexical Analysis)

任务:将字符流切分成 Token(词法单元)。

  • 输入 (Input):字符串 "2 + 3 * 4"
  • 推导过程
    1. 指针指向 2:识别为数字,生成 Token(NUM, 2)
    2. 指针指向 +:识别为加号,生成 Token(PLUS, '+')
    3. 指针指向 3:识别为数字,生成 Token(NUM, 3)
    4. 指针指向 *:识别为乘号,生成 Token(STAR, '*')
    5. 指针指向 4:识别为数字,生成 Token(NUM, 4)
  • 输出 (Output):一个 Token 序列(数组): Tokens = [T1(NUM,2), T2(PLUS,+), T3(NUM,3), T4(STAR,*), T5(NUM,4)]

第二步:定义语法规则 (Grammar Rules)

在开始分析前,必须先定好“剧本”。为了处理优先级,我们使用 EBNF 规则:

  1. Expression (加法层): E -> T { + T } (一个 T 后面跟着 0 到多个 "+ T")
  2. Term (乘法层): T -> F { * F } (一个 F 后面跟着 0 到多个 "* F")
  3. Factor (基础层): F -> NUMBER | ( E ) (数字或者括号里的表达式)

第三步:语法分析与语法树生成 (Parsing & AST Generation)

这是你要求的重点。递归下降会通过函数调用栈,一边匹配 Token,一边像搭积木一样返回节点。

  • 输入:上一步的 Token 序列。
  • 当前 Token 指针:指向 T1(NUM,2)

详细推导步骤:

  1. 入口:调用 parse_E()
  2. 下降到乘法层parse_E 第一步就是调用 parse_T()
  3. 下降到基础层parse_T 第一步就是调用 parse_F()
  4. 识别数字 2
    • parse_F 看到 T1(NUM,2),匹配成功。
    • 生成节点:创建一个叶子节点 Node(val:2)
    • 返回parse_F 将该节点向上返回给 parse_T
  5. 尝试乘法循环
    • parse_T 拿到 Node(2),看下一个 Token 是 T2(PLUS,+)
    • parse_T 的规则是 T -> F { * F },它不认识 +,所以循环不执行。
    • 返回parse_TNode(2) 向上返回给 parse_E
  6. 识别加法循环
    • parse_E 拿到 Node(2),看下一个 Token 是 T2(PLUS,+)
    • 匹配成功parse_E 认识 +,进入循环。
    • 保存左手:它把 Node(2) 存为 Left
    • 匹配右手:它跳过 +,再次调用 parse_T() 来解析加号右边的部分。
  7. 右侧下降
    • 新的 parse_T 调用 parse_F,匹配到 T3(NUM,3)
    • parse_F 返回 Node(3)parse_T
  8. 右侧乘法循环
    • parse_T 拿到 Node(3),看下一个 Token 是 T4(STAR,*)
    • 匹配成功parse_T 进入循环!
    • 保存乘法左手:把 Node(3) 存为 Mul_Left
    • 匹配乘法右手:跳过 *,再次调用 parse_F()
    • parse_F 匹配到 T5(NUM,4),返回 Node(4)
  9. 生成乘法子树
    • parse_T 循环结束,创建一个乘法节点:Node(op:*, left:Node(3), right:Node(4))
    • 返回:这个乘法子树被返回给了 parse_E
  10. 生成最终加法树
    • parse_E 现在手里有左手的 Node(2) 和刚才拿到的右手 MulNode(*)
    • 创建一个最终的加法节点。

语法分析推导详表

准备工作:
  • 输入 Token 流[2], [+], [3], [*], [4], [EOF]
  • 语法规则简记
    • E (加法层) -> T {+ T}
    • T (乘法层) -> F {* F}
    • F (基础层) -> 数字
  • 核心逻辑:函数调用 = 下降;函数返回 = 上升/回溯
步骤当前指针 (Token)执行动作 (函数调用栈)说明生成的中间结果 (AST片段)
1[2] + 3 * 4调用 parse_E()从最顶层“加法层”开始解析(等待 T 返回)
2[2] + 3 * 4E 内部调用 parse_T()加法是由项组成的,下降到“乘法层”(等待 F 返回)
3[2] + 3 * 4T 内部调用 parse_F()乘法是由因子组成的,下降到“基础层”(寻找数字)
4[2] + 3 * 4F 匹配到数字 2匹配成功!指针右移到 +返回 Node(2)
52 [+] 3 * 4F 执行完毕,返回 TF 把数字 2 交给“上级” TT 拿到 Node(2)
62 [+] 3 * 4T 检查下一个 Token下一个是 +T 只管 *循环不成立T 完成,返回 Node(2)
72 [+] 3 * 4T 执行完毕,返回 ET2 交还给“最上级” EE 拿到左手 Node(2)
82 [+] 3 * 4E 检查下一个 Token下一个是 +匹配成功!进入循环指针右移到 3
92 + [3] * 4E 调用 parse_T()E 要找加号右边的“项”,再次下降(等待右侧 T 返回)
102 + [3] * 4T 内部调用 parse_F()同样,右侧的项也要先找基础因子(寻找数字)
112 + [3] * 4F 匹配到数字 3匹配成功!指针右移到 *返回 Node(3)
122 + 3 [*] 4F 执行完毕,返回 TF3 交给右侧的 TT 拿到左手 Node(3)
132 + 3 [*] 4T 检查下一个 Token下一个是 *匹配成功!进入循环指针右移到 4
142 + 3 * [4]T 内部调用 parse_F()T 要找乘号右边的因子,再次下降(寻找数字)
152 + 3 * [4]F 匹配到数字 4匹配成功!指针右移到 EOF返回 Node(4)
162 + 3 * 4 [EOF]F 返回 TT 结束循环T34 缝合。返回给 E返回 Node(*, 3, 4)
172 + 3 * 4 [EOF]E 拿到右边结果,结束循环E 把左手的 2 和右手的 3*4 缝合*返回 Node(+, 2, )
18解析完成根节点已生成此时系统栈清空,拿到整棵语法树最终 AST 根节点

关键细节剖析:

  1. 为什么 + 不会先被处理?

    • 看步骤 5-7:虽然指针到了 +,但 parse_Tparse_F 发现自己处理不了,会逐层把结果“向上抛”。只有回到 parse_E 时,+ 才会被正式认领。这体现了优先级
  2. 为什么 * 会被先缝合?

    • 看步骤 13-16:当 parse_T 拿到 3 发现后面是 * 时,它没有急着把 3 还给加法层,而是自己扣下了 3 并继续往下找 4。这保证了乘法节点先于加法节点组装完成。
  3. 递归的“高度”

    • 在步骤 9,parse_E 并没有直接找数字,而是再次启动了完整的 parse_T 流程。这种“重新开始”的机制,使得程序可以处理无限复杂的结构(比如 2 + 3 * 4 + 5 * 6...)。

最终生成的 AST 结构:

      (+)  <-- 这是步骤 17 生成的根
     /   \
   (2)   (*) <-- 这是步骤 16 生成的子树
        /   \
      (3)   (4)

第四步:代码生成 (Code Generation)

任务:将 AST 转换成指令序列。

  • 输入:生成的 AST。

  • 方法:对树进行后序遍历 (左-右-中)

  • 推导过程

    1. 遍历根节点 +:先去左边。
    2. 左边是 2:叶子节点,输出 PUSH 2
    3. 回到 +:再去右边 *
    4. 遍历 *:先去左边 3
    5. 左边是 3:输出 PUSH 3
    6. 回到 *:再去右边 4
    7. 右边是 4:输出 PUSH 4
    8. 回到 * 节点:左右都完了,输出自己:MUL
    9. 最后回到根节点 +:左右都完了,输出自己:ADD
  • 输出 (指令集)

    PUSH 2
    PUSH 3
    PUSH 4
    MUL
    ADD
    

第五步:指令执行 (Execution)

任务:模拟 CPU 运行这些指令。

  • 输入:指令序列。
  • 推导过程
    1. PUSH 2: 栈 = [2]
    2. PUSH 3: 栈 = [2, 3]
    3. PUSH 4: 栈 = [2, 3, 4]
    4. MUL: 弹出 4, 3,计算 3 * 4 = 12,压回。栈 = [2, 12]
    5. ADD: 弹出 12, 2,计算 2 + 12 = 14,压回。栈 = [14]
  • 输出:结果 14

细节总结:

  1. 语法分析的本质:是通过函数嵌套调用,利用“系统调用栈”记录了还未处理完的优先级。
  2. AST生成的关键:每个 parse_X 函数在退出前,必须把自己的成果(节点)包好交给“上级”。
  3. 优先级的实现:因为 parse_E 必须等 parse_T 返回,而 parse_T 内部可能处理了 *。这意味着乘法节点在树中被创建的时间点比加法节点更早、位置更深,从而保证了先算乘法。