⚙️ 第十章 通过汇编语言认识程序的真面目
📄 一句话概括

想知道编译器做了什么,最直接的办法就是让它把中间产物吐出来:C 编译器加一个开关,就能拿到汇编源代码。

📝 本节和前面几节的关系

10.1 说了汇编和本机代码一一对应,这一节讲【怎么亲眼看到那份汇编】。

一、编译到底做了几件事

从源文件到可执行文件,第八章讲过要经过好几步(8.4 讲过)。 其中最关键的一步是【翻译】——把 C 语言变成汇编,再变成机器码。 平时这个中间产物一生成就被删掉了,所以你看不见。 让它停下来,就能看见。

二、把中间产物留下来

【📋 实操路径】C 编译器一般都有这个开关: gcc -S main.c——只生成 main.s,不汇编、不链接。 gcc -S -O2 main.c——换个优化级别,可以对比两版汇编的差别。 拿到 main.s 之后用文本编辑器打开,看到的就是一行行汇编。 这一步的意义在于:编译器不再是黑盒,它的产出可以逐行读。

三、一个最小例子看三件事

拿一个求两数之和的函数试一下,编译出来的大致是这样: 函数入口先 pushl %ebp、再 movl %esp, %ebp——建立栈帧。 参数不在函数里,而在栈上按偏移取:8(%ebp)、12(%ebp)。 一行 C 的 a + b,落到汇编只有一条 addl。 三条合起来说明一件事:高级语言里那些【自动加上的东西】,汇编里都摊在明面上。

四、为什么值得常做这件事

判断性能:一段代码到底搬了几次内存,看一眼汇编就知道了。 确认优化有没有生效:改完之后汇编变没变,一目了然。 理解语言特性:为什么这个写法快、那个写法慢,汇编会给出答案。 这不是只有底层工程师才干的事——它是最省事的【验尸】手段。

五、动手 Demo:用C编译器输出汇编语言源代码

【📋 实操路径】gcc -S main.c 会把编译的中间产物留下来。 拿一个两数之和的函数,看它变成的汇编长什么样。

src = "int add(int a, int b) { return a + b; }"
asm = [
    ("add:",              ""),
    ("    pushl %ebp",     "保存调用者的栈底"),
    ("    movl %esp, %ebp", "建立自己的栈底"),
    ("    movl 8(%ebp), %eax",  "把第一个参数取进 eax"),
    ("    addl 12(%ebp), %eax", "加上第二个参数"),
    ("    popl %ebp",      "恢复调用者的栈底"),
    ("    ret",            "返回"),
]
print(src)
for ins, note in asm:
    print(ins, note)

实跑输出:

--------------------------------------------------------------------
用C编译器输出汇编语言源代码
--------------------------------------------------------------------

想知道编译器到底做了什么?**最直接的办法是让它把汇编吐出来。**

【📋 实操路径】C 编译器一般都有这个开关:
    gcc -S main.c          # 只生成 main.s,不汇编、不链接
    gcc -S -O2 main.c      # 换个优化级别,对比两版汇编

  拿一个求两数之和的函数试一下,**编译出来的大致是这样:**

    add:
        pushl %ebp             保存调用者的栈底
        movl %esp, %ebp        建立自己的栈底
        movl 8(%ebp), %eax     把第一个参数取进 eax
        addl 12(%ebp), %eax    加上第二个参数
        popl %ebp              恢复调用者的栈底
        ret                    返回

  三件事一眼就看出来了:
    **C 语言的一行 `a + b`,变成了一条 addl 指令。**
    **函数的进出有固定套路**——先压 ebp、再把 esp 挪到 ebp。
    **参数不在函数里,而在【栈上按偏移取】**——8(%ebp)、12(%ebp)。

  **这就是【认识程序的真面目】的入口:**
  平时看不见的东西,**让它把中间产物吐出来,就全露馅了。**

实测:C 语言的一行 a + b 变成一条 addl;函数的进出有固定套路。 参数不在函数里,而在栈上按偏移取——这就是【认识程序真面目】的入口。

30 秒实验

  1. 在你自己的电脑上装个 gcc 或 clang,跑一次 -S,打开生成的 .s 文件读前二十行。
  2. 同一份代码分别用 -O0 和 -O2 编译,比一比指令条数。

常见误区

  1. 以为编译是【一步到位】的黑盒。中间会生成汇编,只是默认被删掉了。
  2. 以为汇编只是给硬件工程师看的。它是理解性能与语言特性最直接的工具。
自测三问(点开对答案) 1. 怎么让编译器把汇编留下来? —— 用 -S 开关,例如 gcc -S main.c,会生成 main.s 且不继续汇编、链接。 2. 两数之和的函数进入时先做哪两件事? —— pushl %ebp 保存调用者的栈底,再 movl %esp, %ebp 建立自己的栈底。 3. 函数的参数放在哪里? —— 放在栈上,函数里按偏移取,例如 8(%ebp)、12(%ebp)。

相关笔记