算术操作符 逆向汇编 一 - 指南

文章目录

在 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`。

​重要注意事项:​

  • ​运算符优先级和结合性:​​ 算术运算符有不同的优先级(例如 *, /, %优先级高于 +, -)和结合性(大部分是左结合)。使用括号 ()可以明确指定运算顺序,提高代码可读性并避免错误。

  • ​类型转换:​​ 当操作数的类型不同时(例如 intfloat),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​:为局部变量分配栈空间(abadd_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​ 是十六进制的 103是直接值。


​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​:调试模式下检查栈平衡(防止栈溢出)。


​关键点总结​

  1. ​栈帧管理​​:

    • 通过 ebpesp维护栈帧。

    • 局部变量(abadd_result)存储在栈上。

  2. ​加法实现​​:

    • 使用 eax寄存器作为临时存储,直接通过 add指令完成加法。
  3. ​调试支持​​:

    • 0xCCCCCCCC填充未初始化内存(调试模式下)。

    • __RTC_CheckEsp检查栈平衡。

  4. ​返回值​​:

    • 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
  • ab的值存储到栈中(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的值(关键变化:addsub)。

  • 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                          ; 返回
  • 与加法示例完全一致,是标准的函数退出流程。

​关键差异​

  1. ​运算指令​​:

    • 加法示例使用 add eax, [b],而减法示例使用 sub eax, [b]

    • 其他指令(如数据移动、栈操作)完全相同。

  2. ​结果存储​​:

    • 加法结果存到 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为局部变量(abcd等)预留空间。

  • ​调试初始化​​:rep stos0xCCCCCCCC填充栈(帮助检测未初始化内存)。


​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                          ; 返回
  • 标准函数退出流程,与之前示例一致。

​关键点总结​

  1. imul指令​​:

    • 用于有符号整数乘法,可处理正数和负数。

    • 语法:imul dest, srcdest *= src)。

    • 如果结果溢出,高位部分会存入 edx(但本例未使用扩展结果)。

  2. ​负数存储​​:

    • -5的补码是 0xFFFFFFFB(32位),imul能正确解析并计算。
  3. ​调试模式开销​​:

    • 栈填充 (0xCCCCCCCC) 和 __RTC_CheckEsp是调试模式特有的安全措施。
  4. ​优化对比​​:

    • 若开启优化(如 -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. imulmul的指令特性对比​

指令操作数类型结果存储方式效率(现代 CPU)
mul无符号整数edx:eax = eax * src(64位结果)较低(延迟高)
imul有符号整数eax = eax * src(32位结果)更高(优化更好)
imul​也可用于无符号​低 32 位结果与 mul相同更高
  • ​关键点​​:当乘法结果的 ​​高 32 位不需要​​ 时(即结果未溢出 32 位),imulmul的低 32 位结果 ​​完全相同​​。

  • 编译器优先选择 imul的原因是:

    • ​更灵活​​:imul支持单操作数(eax * src)和双操作数(dest *= src)形式。

    • ​更高效​​:现代 CPU 对 imul的优化更好,延迟更低。


​2. 为什么本例使用 imul而不是 mul?​

(1)​​结果未溢出 32 位​
  • 示例中的乘法:

    • 10 * 3 = 300x1E

    • 5 * 4 = 200x14

  • 结果均远小于 32 位上限(0xFFFFFFFF),无需关心高 32 位。

  • imul的低 32 位结果与 mul完全一致​​,故编译器选择更高效的 imul

(2)​​编译器优化策略​
  • 当操作数均为无符号整数,但结果不溢出时,编译器会默认使用 imul

    • 避免检查是否需要高 32 位。

    • 减少指令复杂度(mul必须操作 edx:eax,而 imul可直接操作通用寄存器)。

(3)​​调试模式的影响​
  • 在调试模式下(如您的代码),编译器可能更保守,但依然优先选择 imul

    • 调试模式的额外检查(如 __RTC_CheckEsp)已占用资源,无需再为乘法选择低效指令。

​3. 何时会使用 mul指令?​

如果以下条件满足,编译器会生成 mul指令:

  1. ​显式需要高 32 位结果​​:

    unsigned long long result = (unsigned long long)a * b; // 需要 64 位结果

    对应汇编:

    mov eax, [a]
    mul dword ptr [b]    ; 结果在 edx:eax
  2. ​操作数可能溢出 32 位​​:

    unsigned int a = 0xFFFFFFFF, b = 2;
    unsigned int c = a * b; // 溢出,高 32 位丢失

    某些编译器可能选择 mul并忽略高 32 位(但主流编译器仍会用 imul)。


​4. 验证:无符号乘法的正确性​

即使使用 imul,无符号乘法的结果也是正确的:

  • ​数学本质​​:

    • 对于无符号数 ab,其二进制乘积的低 32 位与有符号数乘积的低 32 位相同。

    • 例如:

      • 无符号 0xFFFFFFFF * 2 = 0xFFFFFFFE(低 32 位)。

      • 有符号 -1 * 2 = -2(补码 0xFFFFFFFE)。

      • 低 32 位结果一致。


​5. 总结​

场景编译器选择指令原因
无符号乘法,结果不溢出imul低 32 位结果正确,且 imul效率更高
需要高 32 位结果mulmul可生成完整 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位模式)​

这段代码演示了 ​​无符号乘法​​ 在汇编层面的实现方式,主要涉及两种场景:

  1. ​普通无符号乘法(结果不溢出 32 位)​​ → 使用 imul

  2. ​需要高 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​:为局部变量分配栈空间(abcdunsigned_mulfull_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 位时,imulmul的低 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_resultunsigned 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​:调试模式下检查栈是否平衡(防止栈溢出)。


​关键结论​

  1. ​无符号乘法的指令选择​​:

    • imul​:当结果不溢出 32 位时(默认选择,效率更高)。

    • mul​:当需要高 32 位结果时(如 64 位运算)。

  2. ​调试模式的影响​​:

    • 额外的栈填充 (0xCCCCCCCC) 和检查 (__RTC_CheckEsp) 会增加开销。
  3. ​实际开发建议​​:

    • ​普通无符号乘法​​:信任编译器优化,使用 imul即可。

    • ​64 位乘法​​:使用 unsigned long long强制 mul指令。


​输出验证​

运行代码后,输出如下:

unsigned_mul = 30
full_result = 8589934590 (0x1fffffffe)
  • full_result正确计算了 0xFFFFFFFF * 2 = 0x1FFFFFFFE,验证了 mul的正确性。

posted on 2025-11-06 16:48  wgwyanfs  阅读(6)  评论(0)    收藏  举报

导航