想知道编译器做了什么,最直接的办法就是让它把中间产物吐出来: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 秒实验
- 在你自己的电脑上装个 gcc 或 clang,跑一次 -S,打开生成的 .s 文件读前二十行。
- 同一份代码分别用 -O0 和 -O2 编译,比一比指令条数。
常见误区
- 以为编译是【一步到位】的黑盒。中间会生成汇编,只是默认被删掉了。
- 以为汇编只是给硬件工程师看的。它是理解性能与语言特性最直接的工具。
自测三问(点开对答案)
1. 怎么让编译器把汇编留下来? —— 用 -S 开关,例如 gcc -S main.c,会生成 main.s 且不继续汇编、链接。 2. 两数之和的函数进入时先做哪两件事? —— pushl %ebp 保存调用者的栈底,再 movl %esp, %ebp 建立自己的栈底。 3. 函数的参数放在哪里? —— 放在栈上,函数里按偏移取,例如 8(%ebp)、12(%ebp)。- ☐ 下一节(10.3 伪指令与注释)
相关笔记
- 汇编语言和本机代码是一一对应的 — 为什么要看汇编
- 伪指令与注释 — 拿到汇编清单之后怎么读
- 仅靠编译无法得到可执行文件 — 编译只是其中一步
- 编译器负责翻译源代码 — 编译器把源代码翻译成什么
- ⚙️ 程序是怎样跑起来的 · 索引