算术操作符 逆向汇编 一 - 指南
文章目录
在 C 语言中,支持的算术操作符主要包括以下几种:
1. 基本算术运算符 (二元运算符,需要两个操作数):
- `+`:加法运算符。例如:`a + b`
- `-`:减法运算符。例如:`a - b`
- `*`:乘法运算符。例如:`a * b`
- `/`:除法运算符。
- 当两个操作数都是整数时,执行**整数除法**,结果会**丢弃小数部分**(向零截断)。例如:`5 / 2`的结果是 `2`。
- 当至少有一个操作数是浮点数时,执行**浮点数除法**,结果保留小数部分。例如:`5.0 / 2`或 `5 / 2.0`的结果是 `2.5`。
- `%`:模运算符(求余运算符)。
- **只适用于整数类型** (`int`, `char`, `long`, `short`, 及其 `unsigned`变体)。
- 计算两个整数相除后的**余数**。结果的符号与被除数(左操作数)相同。
- 例如:`5 % 2`结果是 `1`,`-5 % 2`结果是 `-1`,`5 % -2`结果是 `1`。
2. 自增和自减运算符 (一元运算符,作用于单个操作数):
- `++`:自增运算符。将操作数的值增加 1。
- `i++`:**后置自增**。先使用 `i`的当前值进行表达式计算,然后再将 `i`的值增加 1。
- `++i`:**前置自增**。先将 `i`的值增加 1,然后再使用 `i`的新值进行表达式计算。
- `--`:自减运算符。将操作数的值减少 1。
- `i--`:**后置自减**。先使用 `i`的当前值进行表达式计算,然后再将 `i`的值减少 1。
- `--i`:**前置自减**。先将 `i`的值减少 1,然后再使用 `i`的新值进行表达式计算。
- **注意:** 在同一个表达式中对同一个变量多次使用自增/自减运算符(如 `i = i++ + ++i;`)会导致**未定义行为**,应避免这样做。
3. 复合赋值运算符:
- 这些运算符将算术运算和赋值结合起来。
- `+=`:加法赋值。例如:`a += b`等价于 `a = a + b`
- `-=`:减法赋值。例如:`a -= b`等价于 `a = a - b`
- `*=`:乘法赋值。例如:`a *= b`等价于 `a = a * b`
- `/=`:除法赋值。例如:`a /= b`等价于 `a = a / b`
- `%=`:模赋值。例如:`a %= b`等价于 `a = a % b`(仅适用于整数类型)
- **注意:** 这些运算符会先计算右边的表达式,然后再进行运算和赋值。例如 `a *= b + c`等价于 `a = a * (b + c)`,而不是 `a = a * b + c`。
重要注意事项:
运算符优先级和结合性: 算术运算符有不同的优先级(例如
*,/,%优先级高于+,-)和结合性(大部分是左结合)。使用括号()可以明确指定运算顺序,提高代码可读性并避免错误。类型转换: 当操作数的类型不同时(例如
int和float),C 语言会进行隐式类型转换(提升)。了解这些规则对于理解计算结果至关重要。例如9 / 2(整数除法) 结果是4,而9 / 2.0(浮点除法) 结果是4.5。整数溢出: 对整数类型进行运算时,结果可能超出该类型所能表示的范围(上溢或下溢),这会导致未定义行为。程序员需要自己注意处理这种情况。
这些运算符是 C 语言中进行数值计算的基础。理解它们的含义、优先级、结合性以及类型转换规则对于编写正确的 C 程序至关重要。
算术运算逆向
加法
代码
#include <stdio.h>
int main() {
int a = 10, b = 3;
int add_result = a + b; // 10 + 3 = 13
return 0;
}
汇编
int main() {
00411770 push ebp
00411771 mov ebp,esp
00411773 sub esp,0E4h
00411779 push ebx
0041177A push esi
0041177B push edi
0041177C lea edi,[ebp-24h]
0041177F mov ecx,9
00411784 mov eax,0CCCCCCCCh
00411789 rep stos dword ptr es:[edi]
0041178B mov ecx,offset _C66D3399_simple_cpp@cpp (041C008h)
00411790 call @__CheckForDebuggerJustMyCode@4 (041131Bh)
00411795 nop
int a = 10, b = 3;
00411796 mov dword ptr [a],0Ah
0041179D mov dword ptr [b],3
int add_result = a + b; // 10 + 3 = 13
004117A4 mov eax,dword ptr [a]
004117A7 add eax,dword ptr [b]
004117AA mov dword ptr [add_result],eax
return 0;
004117AD xor eax,eax
}
004117AF pop edi
004117B0 pop esi
004117B1 pop ebx
004117B2 add esp,0E4h
004117B8 cmp ebp,esp
004117BA call __RTC_CheckEsp (041123Fh)
004117BF mov esp,ebp
004117C1 pop ebp
004117C2 ret
这段代码是一个简单的 C 程序,计算 a + b并将结果存储在 add_result中。我们来看一下对应的 x86 汇编代码(32位,MSVC 编译器风格)的分析:
1. 函数入口和栈帧设置
00411770 push ebp ; 保存旧的基址指针 (ebp)
00411771 mov ebp,esp ; 设置新的基址指针 (ebp = esp)
00411773 sub esp,0E4h ; 为局部变量分配栈空间 (0xE4 字节)
00411779 push ebx ; 保存寄存器 ebx
0041177A push esi ; 保存寄存器 esi
0041177B push edi ; 保存寄存器 edi
0041177C lea edi,[ebp-24h] ; edi = ebp - 0x24 (栈初始化起始地址)
0041177F mov ecx,9 ; 循环次数 (ecx = 9)
00411784 mov eax,0CCCCCCCCh ; 填充值 (0xCCCCCCCC 是调试模式下的未初始化标记)
00411789 rep stos dword ptr es:[edi] ; 用 0xCCCCCCCC 填充栈空间 (调试模式初始化)
0041178B mov ecx,offset _C66D3399_simple_cpp@cpp (041C008h)
00411790 call @__CheckForDebuggerJustMyCode@4 (041131Bh) ; 调试检查
00411795 nop ; 空操作 (对齐或调试占位)
push ebp/mov ebp, esp:建立栈帧(函数调用惯例)。
sub esp, 0E4h:为局部变量分配栈空间(a、b、add_result等)。
rep stos:在调试模式下用0xCCCCCCCC填充栈空间(帮助检测未初始化内存)。
@__CheckForDebuggerJustMyCode@4:调试相关检查(MSVC 特有)。
2. 变量初始化
00411796 mov dword ptr [a],0Ah ; a = 10 (0xA)
0041179D mov dword ptr [b],3 ; b = 3
dword ptr [a]:将a存储到栈地址[ebp - ?](具体偏移由编译器决定)。
0Ah 是十六进制的10,3是直接值。
3. 加法运算
004117A4 mov eax,dword ptr [a] ; eax = a
004117A7 add eax,dword ptr [b] ; eax += b
004117AA mov dword ptr [add_result],eax ; add_result = eax
mov eax, [a]:将变量a的值加载到寄存器eax。
add eax, [b]:将b的值加到eax中。
mov [add_result], eax:将结果存回add_result。
4. 函数返回
004117AD xor eax,eax ; eax = 0 (返回值)
004117AF pop edi ; 恢复 edi
004117B0 pop esi ; 恢复 esi
004117B1 pop ebx ; 恢复 ebx
004117B2 add esp,0E4h ; 释放栈空间
004117B8 cmp ebp,esp ; 检查栈指针是否正常
004117BA call __RTC_CheckEsp ; 栈检查(调试模式)
004117BF mov esp,ebp ; 恢复 esp
004117C1 pop ebp ; 恢复 ebp
004117C2 ret ; 返回
xor eax, eax:将返回值设为0(C 语言的return 0)。
pop/add esp:恢复寄存器和栈指针。
__RTC_CheckEsp:调试模式下检查栈平衡(防止栈溢出)。
关键点总结
栈帧管理:
通过
ebp和esp维护栈帧。局部变量(
a、b、add_result)存储在栈上。
加法实现:
- 使用
eax寄存器作为临时存储,直接通过add指令完成加法。
- 使用
调试支持:
0xCCCCCCCC填充未初始化内存(调试模式下)。__RTC_CheckEsp检查栈平衡。
返回值:
xor eax, eax是高效清零寄存器的常见方式。
优化对比
如果开启编译器优化(如 -O2),生成的汇编代码会更简洁:
main:
xor eax, eax ; 直接返回 0
ret ; 忽略未使用的变量和计算
编译器会删除未使用的变量(add_result)和冗余操作。
减法
代码
#include <stdio.h>
int main() {
int a = 10, b = 3;
int sub_result = a - b; // 10 - 3 = 7
return 0;
}
汇编
int main() {
00411770 push ebp
00411771 mov ebp,esp
00411773 sub esp,0E4h
00411779 push ebx
0041177A push esi
0041177B push edi
0041177C lea edi,[ebp-24h]
0041177F mov ecx,9
00411784 mov eax,0CCCCCCCCh
00411789 rep stos dword ptr es:[edi]
0041178B mov ecx,offset _C66D3399_simple_cpp@cpp (041C008h)
00411790 call @__CheckForDebuggerJustMyCode@4 (041131Bh)
00411795 nop
int a = 10, b = 3;
00411796 mov dword ptr [a],0Ah
0041179D mov dword ptr [b],3
int sub_result = a - b; // 10 - 3 = 7
004117A4 mov eax,dword ptr [a]
004117A7 sub eax,dword ptr [b]
004117AA mov dword ptr [sub_result],eax
return 0;
004117AD xor eax,eax
}
004117AF pop edi
004117B0 pop esi
004117B1 pop ebx
004117B2 add esp,0E4h
004117B8 cmp ebp,esp
004117BA call __RTC_CheckEsp (041123Fh)
004117BF mov esp,ebp
004117C1 pop ebp
004117C2 ret
这段代码是一个简单的 C 程序,计算 a - b并将结果存储在 sub_result中。对应的 x86 汇编代码(32位,MSVC 编译器风格)与之前的加法示例非常相似,只是将 add指令替换为 sub指令。以下是详细分析:
1. 函数入口和栈帧设置
00411770 push ebp ; 保存旧的基址指针 (ebp)
00411771 mov ebp,esp ; 设置新的基址指针 (ebp = esp)
00411773 sub esp,0E4h ; 为局部变量分配栈空间 (0xE4 字节)
00411779 push ebx ; 保存寄存器 ebx
0041177A push esi ; 保存寄存器 esi
0041177B push edi ; 保存寄存器 edi
0041177C lea edi,[ebp-24h] ; edi = ebp - 0x24 (栈初始化起始地址)
0041177F mov ecx,9 ; 循环次数 (ecx = 9)
00411784 mov eax,0CCCCCCCCh ; 填充值 (0xCCCCCCCC 是调试模式下的未初始化标记)
00411789 rep stos dword ptr es:[edi] ; 用 0xCCCCCCCC 填充栈空间 (调试模式初始化)
0041178B mov ecx,offset _C66D3399_simple_cpp@cpp (041C008h)
00411790 call @__CheckForDebuggerJustMyCode@4 (041131Bh) ; 调试检查
00411795 nop ; 空操作 (对齐或调试占位)
- 与加法示例完全一致,是标准的函数入口和调试模式初始化。
2. 变量初始化
00411796 mov dword ptr [a],0Ah ; a = 10 (0xA)
0041179D mov dword ptr [b],3 ; b = 3
- 将
a和b的值存储到栈中(a在[ebp - ?],b在[ebp - ?],具体偏移由编译器决定)。
3. 减法运算
004117A4 mov eax,dword ptr [a] ; eax = a
004117A7 sub eax,dword ptr [b] ; eax -= b
004117AA mov dword ptr [sub_result],eax ; sub_result = eax
mov eax, [a]:将变量a的值加载到寄存器eax。
sub eax, [b]:从eax中减去b的值(关键变化:add→sub)。
mov [sub_result], eax:将结果存回sub_result。
4. 函数返回
004117AD xor eax,eax ; eax = 0 (返回值)
004117AF pop edi ; 恢复 edi
004117B0 pop esi ; 恢复 esi
004117B1 pop ebx ; 恢复 ebx
004117B2 add esp,0E4h ; 释放栈空间
004117B8 cmp ebp,esp ; 检查栈指针是否正常
004117BA call __RTC_CheckEsp ; 栈检查(调试模式)
004117BF mov esp,ebp ; 恢复 esp
004117C1 pop ebp ; 恢复 ebp
004117C2 ret ; 返回
- 与加法示例完全一致,是标准的函数退出流程。
关键差异
运算指令:
加法示例使用
add eax, [b],而减法示例使用sub eax, [b]。其他指令(如数据移动、栈操作)完全相同。
结果存储:
- 加法结果存到
add_result,减法结果存到sub_result(逻辑一致)。
- 加法结果存到
优化对比
如果开启编译器优化(如 -O2),生成的汇编代码会大幅简化:
main:
xor eax, eax ; 直接返回 0
ret ; 忽略未使用的变量和计算
- 编译器会删除未使用的变量(
sub_result)和冗余计算。
总结
减法操作在汇编层通过
sub指令实现,与add指令对称。调试模式的额外操作(如栈填充
0xCCCCCCCC)会增加开销,但有助于检测错误。实际开发中应避免未使用的变量,或开启优化以提高效率。
有符号乘法
代码
#include <stdio.h>
int main() {
int a = 10, b = 3;
int signed_mul = a * b; // 10 * 3 = 30
int c = -5, d = 4;
int signed_mul_neg = c * d; // -5 * 4 = -20
return 0;
}
汇编
int main() {
00411770 push ebp
00411771 mov ebp,esp
00411773 sub esp,108h
00411779 push ebx
0041177A push esi
0041177B push edi
0041177C lea edi,[ebp-48h]
0041177F mov ecx,12h
00411784 mov eax,0CCCCCCCCh
00411789 rep stos dword ptr es:[edi]
0041178B mov ecx,offset _C66D3399_simple_cpp@cpp (041C008h)
00411790 call @__CheckForDebuggerJustMyCode@4 (041131Bh)
00411795 nop
int a = 10, b = 3;
00411796 mov dword ptr [a],0Ah
0041179D mov dword ptr [b],3
int signed_mul = a * b; // 10 * 3 = 30
004117A4 mov eax,dword ptr [a]
004117A7 imul eax,dword ptr [b]
004117AB mov dword ptr [signed_mul],eax
int c = -5, d = 4;
004117AE mov dword ptr [c],0FFFFFFFBh
004117B5 mov dword ptr [d],4
int signed_mul_neg = c * d; // -5 * 4 = -20
004117BC mov eax,dword ptr [c]
004117BF imul eax,dword ptr [d]
004117C3 mov dword ptr [signed_mul_neg],eax
return 0;
004117C6 xor eax,eax
}
004117C8 pop edi
004117C9 pop esi
004117CA pop ebx
004117CB add esp,108h
004117D1 cmp ebp,esp
004117D3 call __RTC_CheckEsp (041123Fh)
004117D8 mov esp,ebp
004117DA pop ebp
004117DB ret
这段代码演示了 C 语言中的有符号整数乘法,并通过汇编代码展示了 imul指令的使用。以下是详细分析:
1. 函数入口和栈帧设置
00411770 push ebp ; 保存旧的基址指针 (ebp)
00411771 mov ebp,esp ; 设置新的基址指针 (ebp = esp)
00411773 sub esp,108h ; 为局部变量分配栈空间 (0x108 字节)
00411779 push ebx ; 保存寄存器 ebx
0041177A push esi ; 保存寄存器 esi
0041177B push edi ; 保存寄存器 edi
0041177C lea edi,[ebp-48h] ; edi = ebp - 0x48 (栈初始化起始地址)
0041177F mov ecx,12h ; 循环次数 (ecx = 18)
00411784 mov eax,0CCCCCCCCh ; 填充值 (调试模式标记)
00411789 rep stos dword ptr es:[edi] ; 用 0xCCCCCCCC 填充栈空间
0041178B mov ecx,offset _C66D3399_simple_cpp@cpp (041C008h)
00411790 call @__CheckForDebuggerJustMyCode@4 (041131Bh) ; 调试检查
00411795 nop ; 空操作
栈空间分配:
sub esp, 108h为局部变量(a、b、c、d等)预留空间。调试初始化:
rep stos用0xCCCCCCCC填充栈(帮助检测未初始化内存)。
2. 第一个乘法:a * b(正数相乘)
00411796 mov dword ptr [a],0Ah ; a = 10 (0xA)
0041179D mov dword ptr [b],3 ; b = 3
004117A4 mov eax,dword ptr [a] ; eax = a
004117A7 imul eax,dword ptr [b] ; eax *= b
004117AB mov dword ptr [signed_mul],eax ; signed_mul = eax
imul指令:这是 有符号整数乘法(Signed Integer Multiply)。
计算
eax * [b],结果存储在eax中。示例:
10 * 3 = 30(结果正确存储到signed_mul)。
3. 第二个乘法:c * d(负数相乘)
004117AE mov dword ptr [c],0FFFFFFFBh ; c = -5 (补码: 0xFFFFFFFB)
004117B5 mov dword ptr [d],4 ; d = 4
004117BC mov eax,dword ptr [c] ; eax = c
004117BF imul eax,dword ptr [d] ; eax *= d
004117C3 mov dword ptr [signed_mul_neg],eax ; signed_mul_neg = eax
负数表示:
-5的补码是0xFFFFFFFB(32位有符号整数)。
imul处理负数:计算
-5 * 4 = -20,结果以补码形式存储(0xFFFFFFEC即-20)。imul会自动处理符号位,确保结果正确。
4. 函数返回
004117C6 xor eax,eax ; eax = 0 (返回值)
004117C8 pop edi ; 恢复寄存器
004117C9 pop esi
004117CA pop ebx
004117CB add esp,108h ; 释放栈空间
004117D1 cmp ebp,esp ; 检查栈平衡
004117D3 call __RTC_CheckEsp ; 调试模式栈检查
004117D8 mov esp,ebp ; 恢复 esp
004117DA pop ebp ; 恢复 ebp
004117DB ret ; 返回
- 标准函数退出流程,与之前示例一致。
关键点总结
imul指令:用于有符号整数乘法,可处理正数和负数。
语法:
imul dest, src(dest *= src)。如果结果溢出,高位部分会存入
edx(但本例未使用扩展结果)。
负数存储:
-5的补码是0xFFFFFFFB(32位),imul能正确解析并计算。
调试模式开销:
- 栈填充 (
0xCCCCCCCC) 和__RTC_CheckEsp是调试模式特有的安全措施。
- 栈填充 (
优化对比:
若开启优化(如
-O2),未使用的变量会被删除,代码简化为:main: xor eax, eax ; return 0 ret
扩展知识
无符号乘法:使用
mul指令(但本例未涉及)。乘法溢出:若结果超出 32 位,
imul会设置标志位(可通过jo指令检测)。
无符号乘法
代码
#include <stdio.h>
int main() {
unsigned int a = 10, b = 3;
unsigned int unsigned_mul = a * b; // 10 * 3 = 30
unsigned int c = 5, d = 4;
unsigned int unsigned_mul2 = c * d; // 5 * 4 = 20
return 0;
}
汇编
int main() {
00411770 push ebp
00411771 mov ebp,esp
00411773 sub esp,108h
00411779 push ebx
0041177A push esi
0041177B push edi
0041177C lea edi,[ebp-48h]
0041177F mov ecx,12h
00411784 mov eax,0CCCCCCCCh
00411789 rep stos dword ptr es:[edi]
0041178B mov ecx,offset _C66D3399_simple_cpp@cpp (041C008h)
00411790 call @__CheckForDebuggerJustMyCode@4 (041131Bh)
00411795 nop
unsigned int a = 10, b = 3;
00411796 mov dword ptr [a],0Ah
0041179D mov dword ptr [b],3
unsigned int unsigned_mul = a * b; // 10 * 3 = 30
004117A4 mov eax,dword ptr [a]
004117A7 imul eax,dword ptr [b]
004117AB mov dword ptr [unsigned_mul],eax
unsigned int c = 5, d = 4;
004117AE mov dword ptr [c],5
004117B5 mov dword ptr [d],4
unsigned int unsigned_mul2 = c * d; // 5 * 4 = 20
004117BC mov eax,dword ptr [c]
004117BF imul eax,dword ptr [d]
004117C3 mov dword ptr [unsigned_mul2],eax
return 0;
004117C6 xor eax,eax
}
004117C8 pop edi
004117C9 pop esi
004117CA pop ebx
004117CB add esp,108h
004117D1 cmp ebp,esp
004117D3 call __RTC_CheckEsp (041123Fh)
004117D8 mov esp,ebp
004117DA pop ebp
004117DB ret
在您提供的汇编代码中,无符号乘法(unsigned int)使用的是 imul 指令,而不是理论上更符合无符号运算的 mul 指令。这看似矛盾,但实际上是由编译器的优化策略和指令特性共同决定的。以下是具体原因分析:
1. imul与 mul的指令特性对比
| 指令 | 操作数类型 | 结果存储方式 | 效率(现代 CPU) |
|---|---|---|---|
mul | 无符号整数 | edx:eax = eax * src(64位结果) | 较低(延迟高) |
imul | 有符号整数 | eax = eax * src(32位结果) | 更高(优化更好) |
imul | 也可用于无符号 | 低 32 位结果与 mul相同 | 更高 |
关键点:当乘法结果的 高 32 位不需要 时(即结果未溢出 32 位),
imul和mul的低 32 位结果 完全相同。编译器优先选择
imul的原因是:更灵活:
imul支持单操作数(eax * src)和双操作数(dest *= src)形式。更高效:现代 CPU 对
imul的优化更好,延迟更低。
2. 为什么本例使用 imul而不是 mul?
(1)结果未溢出 32 位
示例中的乘法:
10 * 3 = 30(0x1E)5 * 4 = 20(0x14)
结果均远小于 32 位上限(
0xFFFFFFFF),无需关心高 32 位。
imul的低 32 位结果与mul完全一致,故编译器选择更高效的imul。
(2)编译器优化策略
当操作数均为无符号整数,但结果不溢出时,编译器会默认使用
imul:避免检查是否需要高 32 位。
减少指令复杂度(
mul必须操作edx:eax,而imul可直接操作通用寄存器)。
(3)调试模式的影响
在调试模式下(如您的代码),编译器可能更保守,但依然优先选择
imul:- 调试模式的额外检查(如
__RTC_CheckEsp)已占用资源,无需再为乘法选择低效指令。
- 调试模式的额外检查(如
3. 何时会使用 mul指令?
如果以下条件满足,编译器会生成 mul指令:
显式需要高 32 位结果:
unsigned long long result = (unsigned long long)a * b; // 需要 64 位结果对应汇编:
mov eax, [a] mul dword ptr [b] ; 结果在 edx:eax操作数可能溢出 32 位:
unsigned int a = 0xFFFFFFFF, b = 2; unsigned int c = a * b; // 溢出,高 32 位丢失某些编译器可能选择
mul并忽略高 32 位(但主流编译器仍会用imul)。
4. 验证:无符号乘法的正确性
即使使用 imul,无符号乘法的结果也是正确的:
数学本质:
对于无符号数
a和b,其二进制乘积的低 32 位与有符号数乘积的低 32 位相同。例如:
无符号
0xFFFFFFFF * 2 = 0xFFFFFFFE(低 32 位)。有符号
-1 * 2 = -2(补码0xFFFFFFFE)。低 32 位结果一致。
5. 总结
| 场景 | 编译器选择指令 | 原因 |
|---|---|---|
| 无符号乘法,结果不溢出 | imul | 低 32 位结果正确,且 imul效率更高 |
| 需要高 32 位结果 | mul | mul可生成完整 64 位结果(edx:eax) |
| 调试模式 | imul | 调试模式下仍优先效率,除非显式需要高 32 位 |
- 核心结论:现代编译器对无符号乘法优先使用
imul,是一种基于性能的优化策略,仅在必要时(如 64 位结果)才会使用mul。
强制触发无符号乘法指令
代码
#include <stdio.h>
int main() {
// 场景1:普通无符号乘法(结果不溢出)
unsigned int a = 10, b = 3;
unsigned int unsigned_mul = a * b; // 10 * 3 = 30
// 场景2:显式需要高32位结果(触发mul指令)
unsigned int c = 0xFFFFFFFF, d = 2;
unsigned long long full_result = (unsigned long long)c * d; // 0xFFFFFFFF * 2 = 0x1FFFFFFFE
printf("unsigned_mul = %u\n", unsigned_mul);
printf("full_result = %llu (0x%llx)\n", full_result, full_result);
return 0;
}
汇编
int main() {
004144E0 push ebp
004144E1 mov ebp,esp
004144E3 sub esp,10Ch
004144E9 push ebx
004144EA push esi
004144EB push edi
004144EC lea edi,[ebp-4Ch]
004144EF mov ecx,13h
004144F4 mov eax,0CCCCCCCCh
004144F9 rep stos dword ptr es:[edi]
004144FB mov ecx,offset _C66D3399_simple_cpp@cpp (041C008h)
00414500 call @__CheckForDebuggerJustMyCode@4 (041131Bh)
00414505 nop
// 场景1:普通无符号乘法(结果不溢出)
unsigned int a = 10, b = 3;
00414506 mov dword ptr [a],0Ah
0041450D mov dword ptr [b],3
unsigned int unsigned_mul = a * b; // 10 * 3 = 30
00414514 mov eax,dword ptr [a]
00414517 imul eax,dword ptr [b]
0041451B mov dword ptr [unsigned_mul],eax
// 场景2:显式需要高32位结果(触发mul指令)
unsigned int c = 0xFFFFFFFF, d = 2;
0041451E mov dword ptr [c],0FFFFFFFFh
00414525 mov dword ptr [d],2
unsigned long long full_result = (unsigned long long)c * d; // 0xFFFFFFFF * 2 = 0x1FFFFFFFE
0041452C mov eax,dword ptr [c]
0041452F mul eax,dword ptr [d]
00414532 mov dword ptr [full_result],eax
00414535 mov dword ptr [ebp-44h],edx
printf("unsigned_mul = %u\n", unsigned_mul);
00414538 mov eax,dword ptr [unsigned_mul]
0041453B push eax
0041453C push offset string "unsigned_mul = %u\n" (0417BCCh)
00414541 call _printf (04113B1h)
00414546 add esp,8
printf("full_result = %llu (0x%llx)\n", full_result, full_result);
00414549 mov eax,dword ptr [ebp-44h]
0041454C push eax
0041454D mov ecx,dword ptr [full_result]
00414550 push ecx
00414551 mov edx,dword ptr [ebp-44h]
00414554 push edx
00414555 mov eax,dword ptr [full_result]
00414558 push eax
00414559 push offset string "full_result = %llu (0x%llx)\n" (0417CD0h)
0041455E call _printf (04113B1h)
00414563 add esp,14h
return 0;
00414566 xor eax,eax
}
00414568 pop edi
00414569 pop esi
0041456A pop ebx
0041456B add esp,10Ch
00414571 cmp ebp,esp
00414573 call __RTC_CheckEsp (041123Fh)
00414578 mov esp,ebp
0041457A pop ebp
0041457B ret
汇编代码分析(MSVC 编译器,32位模式)
这段代码演示了 无符号乘法 在汇编层面的实现方式,主要涉及两种场景:
普通无符号乘法(结果不溢出 32 位) → 使用
imul需要高 32 位结果的乘法(64 位运算) → 使用
mul
1. 函数入口和栈帧初始化
004144E0 push ebp ; 保存旧的基址指针
004144E1 mov ebp,esp ; 设置新的栈帧
004144E3 sub esp,10Ch ; 分配栈空间 (0x10C 字节)
004144E9 push ebx ; 保存寄存器
004144EA push esi
004144EB push edi
004144EC lea edi,[ebp-4Ch] ; 初始化栈空间起始地址
004144EF mov ecx,13h ; 循环次数 (19次)
004144F4 mov eax,0CCCCCCCCh ; 调试模式填充值 (0xCCCCCCCC)
004144F9 rep stos dword ptr es:[edi] ; 用 0xCCCCCCCC 填充栈
004144FB mov ecx,offset _C66D3399_simple_cpp@cpp (041C008h)
00414500 call @__CheckForDebuggerJustMyCode@4 (041131Bh) ; 调试检查
00414505 nop ; 空操作(对齐)
sub esp, 10Ch:为局部变量分配栈空间(a、b、c、d、unsigned_mul、full_result等)。
rep stos:在调试模式下用0xCCCCCCCC填充栈(检测未初始化内存)。
@__CheckForDebuggerJustMyCode@4:调试模式下的安全检查(MSVC 特有)。
2. 场景1:普通无符号乘法(imul)
00414506 mov dword ptr [a],0Ah ; a = 10 (0xA)
0041450D mov dword ptr [b],3 ; b = 3
00414514 mov eax,dword ptr [a] ; eax = a
00414517 imul eax,dword ptr [b] ; eax *= b(使用 imul)
0041451B mov dword ptr [unsigned_mul],eax ; 存储结果
imul指令:计算
eax * [b],结果存储在eax。即使是无符号乘法,编译器仍选择
imul,因为:当结果不溢出 32 位时,
imul和mul的低 32 位结果相同。imul在现代 CPU 上效率更高(单周期指令,mul可能需要更多周期)。
为什么不用
mul?mul会计算完整的 64 位结果(edx:eax),但这里只需要低 32 位,imul更高效。
3. 场景2:需要高 32 位的乘法(mul)
0041451E mov dword ptr [c],0FFFFFFFFh ; c = 0xFFFFFFFF (UINT_MAX)
00414525 mov dword ptr [d],2 ; d = 2
0041452C mov eax,dword ptr [c] ; eax = c
0041452F mul eax,dword ptr [d] ; edx:eax = eax * d(使用 mul)
00414532 mov dword ptr [full_result],eax ; 存储低 32 位
00414535 mov dword ptr [ebp-44h],edx ; 存储高 32 位
mul指令:计算
eax * [d],结果存储在edx:eax(64 位)。
0xFFFFFFFF * 2 = 0x1FFFFFFFE:低 32 位 (
eax) =0xFFFFFFFE高 32 位 (
edx) =1
由于
full_result是unsigned long long(64 位),编译器必须使用mul获取完整结果。
为什么不能用
imul?imul只计算低 32 位,无法获取高 32 位(edx),所以必须用mul。
4. printf输出
00414538 mov eax,dword ptr [unsigned_mul]
0041453B push eax
0041453C push offset string "unsigned_mul = %u\n" (0417BCCh)
00414541 call _printf (04113B1h)
00414546 add esp,8
00414549 mov eax,dword ptr [ebp-44h] ; 高 32 位
0041454C push eax
0041454D mov ecx,dword ptr [full_result] ; 低 32 位
00414550 push ecx
00414551 mov edx,dword ptr [ebp-44h] ; 高 32 位
00414554 push edx
00414555 mov eax,dword ptr [full_result] ; 低 32 位
00414558 push eax
00414559 push offset string "full_result = %llu (0x%llx)\n" (0417CD0h)
0041455E call _printf (04113B1h)
00414563 add esp,14h
unsigned_mul:- 直接传递
unsigned_mul(32 位)给printf。
- 直接传递
full_result:- 由于
unsigned long long是 64 位,需要分别传递 低 32 位 (eax) 和 高 32 位 (edx) 给printf。
- 由于
5. 函数返回
00414566 xor eax,eax ; eax = 0(返回值)
00414568 pop edi ; 恢复寄存器
00414569 pop esi
0041456A pop ebx
0041456B add esp,10Ch ; 释放栈空间
00414571 cmp ebp,esp ; 检查栈平衡
00414573 call __RTC_CheckEsp ; 调试模式栈检查
00414578 mov esp,ebp ; 恢复 esp
0041457A pop ebp ; 恢复 ebp
0041457B ret ; 返回
xor eax, eax:设置返回值0(C 语言的return 0)。
__RTC_CheckEsp:调试模式下检查栈是否平衡(防止栈溢出)。
关键结论
无符号乘法的指令选择:
imul:当结果不溢出 32 位时(默认选择,效率更高)。
mul:当需要高 32 位结果时(如 64 位运算)。
调试模式的影响:
- 额外的栈填充 (
0xCCCCCCCC) 和检查 (__RTC_CheckEsp) 会增加开销。
- 额外的栈填充 (
实际开发建议:
普通无符号乘法:信任编译器优化,使用
imul即可。64 位乘法:使用
unsigned long long强制mul指令。
输出验证
运行代码后,输出如下:
unsigned_mul = 30
full_result = 8589934590 (0x1fffffffe)
full_result正确计算了0xFFFFFFFF * 2 = 0x1FFFFFFFE,验证了mul的正确性。
浙公网安备 33010602011771号