实验目的
理解一个基本问题:当乘数或除数是常量时,编译器到底做了什么?
我们写的 C 代码是 a * 3、a / 9 这种形式,
但 CPU 执行时并不一定是「乘法指令」和「除法指令」。
这个实验就是用 IDA 把编译结果翻出来,看看编译器背地里做了什么手脚。
实验环境:Windows + VS2022 + IDA
实验内容
乘法:a * 3
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int a = 0;
printf("请输入数字\n");
scanf("%d", &a);
int b = a * 3;
printf("%d*3=%d", a, b);
return 0;
}
除法:a / 9
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int a = 0;
printf("请输入数字\n");
scanf("%d", &a);
int b = a / 9;
printf("%d/9=%d", a, b);
return 0;
}
更极端的两个:a / 7 与 a * 49
unsigned int div_data(unsigned int a){
return a / 7;
}
unsigned int mul_data(unsigned int a){
return a * 49;
}
用 IDA 打开后,把这几处的汇编指令抄录下来对比。
背后的原理
这部分是我做完实验后补的笔记,理解了它,实验里看到的那些奇怪的指令就说得通了。
为什么乘法不用 mul
x86 的 mul / imul 指令延迟较高,而 lea(Load Effective Address)虽然名字上是"取地址",
实际上可以做加法、乘小常数、加偏移的组合运算,且不改变标志位,流水线友好得多。
所以 a * 3 常常被编译成类似:
lea eax, [eax+eax*2] ; eax = eax + eax*2 = eax*3
一条指令搞定,比 mul 快。
a * 49 则可以拆成 49 = 7 * 7 或 49 = 64 - 15 之类的组合,
用移位加加减来实现。实验里那个 a * 49 就是被优化过的典型例子。
除法为什么更麻烦
除法指令 div / idiv 是 x86 里最慢的整数指令之一,延迟可达几十个时钟周期。
而除以一个常量时,编译器可以用一个经典技巧:乘以"魔数",再右移。
原理是:
a / d ≈ (a * M) >> s
其中 M 和 s 是编译器预先算好的常数。因为无符号除法可以这样精确等价,
所以 a / 7 很可能被编译成一条 imul 加一条 shr——比 idiv 快一个数量级。
这也是为什么实验里
div_data明明写的是除法, 反汇编里看到的却可能是乘法指令——不是看错了,是真的没有除法。
有符号 vs 无符号的差别
这一点在复习时也强调过:
- 加减法:因为采用补码表示,有符号与无符号的运算逻辑是一致的,硬件不需要区分
- 乘除法:必须区分,因为要处理符号扩展和舍入方向
所以编译器为 int a / 7 和 unsigned a / 7 生成的代码是不一样的——
有符号版本还要额外处理负数向下取整的边界情况。
实验总结
这个实验本身代码量很小,但信息量不小:
- 编译器是个激进的优化者。你写的是"乘法",它生成的可能是移位和加法; 你写的是"除法",它生成的可能是乘法和移位。
- "可读性"和"可执行性"是两回事。高级语言的一个运算符, 底层可能完全没有对应的指令。
- 理解了这一点,再看逆向出来的汇编,就不会被那些"莫名其妙的乘法和移位"困惑了—— 那其实是编译器在替你省时间。
对我个人来说,最直接的收获是:以后看到 imul + shr 的组合,
第一反应应该是"这里原本可能是个除法"。
📌 本文由大学时期的课程实验报告整理而来,已移除姓名、学号、班级、指导教师等个人信息。 原始报告中的截图因可能包含个人环境信息(文件路径、用户名)未一并收录。
