C/C++反汇编-各种表达式
基于vs2019的反汇编
优化
对于vs来说一般的优化有两种方案:
O1:生成可执行文件空间小
O2:执行效率高
在vs2019中的release默认是采用的O2方案来处理也就是执行效率优先,而在debug版本中为了调试比较方便可能就会优化比较少。接下来的反汇编会在release和debug中两个分开呈现
常量传播
编译期间会将可计算结果的变量转化为常量来处理
比如int c = 1;
printf("%d\n",c);
这里会直接用1来替代c而不是在内存中进行读取
常量折叠
当几个常量进行计算的时候,编译器会直接将计算的结果来处理
int c=1+2-3;
会直接把c赋值为-1
加法
直接上代码
DEBUG下
//加法反汇编
//变量赋值
int Number1 = 0;
00C11DE8 mov dword ptr [Number1],0
int Number2 = 0;
00C11DEF mov dword ptr [Number2],0
//常量加常量
Number1 = 1 + 2;
00C11DF6 mov dword ptr [Number1],3
//常量加变量
Number1 = Number2 + 1;
00C11DFD mov eax,dword ptr [Number2]
00C11E00 add eax,1
00C11E03 mov dword ptr [Number1],eax
//变量加变量
Number1 = Number1 + Number2;
00C11E06 mov eax,dword ptr [Number1]
00C11E09 add eax,dword ptr [Number2]
00C11E0C mov dword ptr [Number1],eax
这里可以看到当两个常量相加的时候,编译器会直接计算值来处理而不是一个一个寄存器来读取,这样会直接减少运行量
Release下
//加法反汇编
//变量赋值
int Number1 = 0;
int Number2 = 0;
//常量加常量
Number1 = 1 + 2;
//常量加变量
Number1 = Number2 + 1;
//变量加变量
Number1 = Number1 + Number2;
这里直接没有了那么为什么呢?
因为这里采用了各种优化,在编译过程中,编译器通常会采用两种优化方式:常量传播和常量折叠来处理
这里可以采用对变量进行不是常量的赋值比如:
int Number = argc
argc是main函数中的第一个参数,需要输入才能识别,所以编译器不会将其折叠
减法
减法和加法类似,只是对于减法的处理是利用对补码的加法来处理。
乘法
由于乘法的周期比较长,所以编译器会优先考虑使用加法或者使用位移的方法来处理乘法
debug下:
//两常量相乘
printf("2 * 2 = %d\n", 2 * 2);
00BE43CC push offset string "2 * 2 = %d\n" (0BE7CD0h)
00BE43D1 call _main (0BE13C5h)
00BE43D6 add esp,8
//混和运算
printf("Number1*4+5=%d\n", Number1 * 4 + 5);
00BE43D9 mov eax,dword ptr [Number1]
00BE43DC lea ecx,[eax*4+5]
00BE43E3 push ecx
00BE43E4 push offset string "Number1*4+5=%d\n" (0BE7CDCh)
00BE43E9 call _main (0BE13C5h)
00BE43EE add esp,8
printf("Number1*9+5=%d\n", Number1 * 9 + 5);
00BE43F1 imul eax,dword ptr [Number1],9
00BE43F5 add eax,5
00BE43F8 push eax
00BE43F9 push offset string "Number1*9+5=%d\n" (0BE7CECh)
00BE43FE call _main (0BE13C5h)
00BE4403 add esp,8
//两个变量相乘
printf("Number1*Number2=%d\n",Number1*Number2);
00BE4406 mov eax,dword ptr [Number1]
00BE4409 imul eax,dword ptr [Number2]
00BE440D push eax
00BE440E push offset string "Number1*Number2=%d\n" (0BE7E20h)
00BE4413 call _main (0BE13C5h)
00BE4418 add esp,8
在release下的情况:
//处理不是2的倍数的乘数作为乘法时
printf("Number1*15=%d\n", Number1 * 15);
00A31044 mov esi,dword ptr [argc]
00A31047 mov eax,esi
00A31049 shl eax,4
00A3104C sub eax,esi
00A3104E push eax
00A3104F push offset string "Number1*15=%d\n" (0A32100h)
00A31054 call printf (0A31010h)
//处理以2为倍数的乘法作为乘法时
printf("Number1*16=%d\n", Number1 * 16);
00A31059 mov eax,esi
00A3105B shl eax,4
00A3105E push eax
00A3105F push offset string "Number1*16=%d\n" (0A32110h)
00A31064 call printf (0A31010h)
//两常量相乘
printf("2 * 2 = %d\n", 2 * 2);
00A31069 push 4
00A3106B push offset string "2 * 2 = %d\n" (0A32120h)
00A31070 call printf (0A31010h)
//混和运算
printf("Number1*4+5=%d\n", Number1 * 4 + 5);
00A31075 lea eax,[esi*4+5]
00A3107C push eax
00A3107D push offset string "Number1*4+5=%d\n" (0A3212Ch)
00A31082 call printf (0A31010h)
printf("Number1*9+5=%d\n", Number1 * 9 + 5);
00A31087 lea eax,[esi*8+5]
00A3108E add eax,esi
00A31090 push eax
00A31091 push offset string "Number1*9+5=%d\n" (0A3213Ch)
00A31096 call printf (0A31010h)
//两个变量相乘
printf("Number1*Number2=%d\n",Number1*Number2);
00A3109B imul esi,esi
00A3109E push esi
00A3109F push offset string "Number1*Number2=%d\n" (0A3214Ch)
00A310A4 call printf (0A31010h)
00A310A9 add esp,30h
算数结果溢出
for(int i=0;i>0;i++)
{
printf("i=%d\n",i);
}
这个结果看似是一个死循环,但是由于int是一个4个整形的有符号数变量,当他的符号位由0变为1的时候,i就变为了一个负数。
进位
进位是指无符号数超出了表达范围,因为无符号数没有符号位,所以不会影响数据。只是进位后的1位数据1不在存储空间,而是在CF标记寄存位。可通过检查CF来判断是否产生进位
溢出
针对有符号位数,因为由于数据进位有符号数的符号位有可能被破坏,因为只有有符号位数才有符号位,所以溢出只针对有符号数,要分别是否溢出,只需要查看OF寄存器位,但是要OF寄存器位来判断是否溢出的时候只需要判断加法指令的结果是不是数值符号一致就可以了
自增和自减
C/C++使用 --和++来实现自增和自减,根据变量和自增运算符的位置又可以分为先使用后加减和先加减后使用。
int a = argc;
00B21828 mov eax,dword ptr [argc]
00B2182B mov dword ptr [a],eax
int b = argc;
00B2182E mov eax,dword ptr [argc]
00B21831 mov dword ptr [b],eax
a = 5 + (++b);
00B21834 mov eax,dword ptr [b]
00B21837 add eax,1
00B2183A mov dword ptr [b],eax
00B2183D mov ecx,dword ptr [b]
00B21840 add ecx,5
00B21843 mov dword ptr [a],ecx
a = 5 + (b++);
00B21846 mov eax,dword ptr [b]
00B21849 add eax,5
00B2184C mov dword ptr [a],eax
00B2184F mov ecx,dword ptr [b]
00B21852 add ecx,1
00B21855 mov dword ptr [b],ecx
可以看到 ++b和b++的区别就是在传送变量和自增的位置不同
关系运算与逻辑运算
关系运算用来判断两者之间的关系,如:小于,等于,大于,不小于,小于等于等等。
关系运算的作用是获得一个关系运算的判断结果:是真还是假
逻辑运算:用来判断依赖关系,如:与或非。对应的符号为 && || !
关系运算和条件跳转的对应
各种跳转指令,和标志寄存器配合使用,来跳转。
表达式短路
通常在执行 与或等逻辑运算的时候,编译器为了更快,会对条件进行优化。
对于与运算:当有一个不为真就返回0
对于或运算:当有一个为真就返回1
来优化
int a = 1;
000C1828 mov dword ptr [a],1
int b = 2;
000C182F mov dword ptr [b],2
if ((a!=1) && b)
000C1836 cmp dword ptr [a],1 //如果1不等于1就直接跳转了
000C183A je main+49h (0C1849h)
000C183C cmp dword ptr [b],0
000C1840 je main+49h (0C1849h)
{
int c = 1;
000C1842 mov dword ptr [ebp-20h],1
}
if (a || b)
000C1849 cmp dword ptr [a],0
000C184D jne main+55h (0C1855h) //a不为0就直接跳转了
000C184F cmp dword ptr [b],0
000C1853 je main+5Ch (0C185Ch)
{
int d = 1;
000C1855 mov dword ptr [ebp-2Ch],1
}
return 0;
条件表达式-三目运算符
表达式1?表达式2,表达式3
如果表达式1为真就执行表达式2,
如果表达式1为假就执行表达式3
三目运算符的构成应该是先判断在选择,但是根据表达式的不同执行的流程和效果也有不同
当表达式2和表达式3都为常量,条件表达式可以被优化
当表达式2或表达式3其中的一个为变量的时候,条件表达式不可被优化,会转化为分支结构
当表达式1为常量的时候,编译器会直接优化掉
位运算
指令 | 效果 |
---|---|
<< | 左移指令 |
>> | 右移指令 |
| | 位或运算 |
& | 为与运算 |
^ | 异或运算 |
~ | 取反运算 |
int a = 100;
00D51828 mov dword ptr [a],64h
int b;
b = a << 2;
00D5182F mov eax,dword ptr [a]
00D51832 shl eax,2 //逻辑左移
00D51835 mov dword ptr [b],eax
b = a >> 2;
00D51838 mov eax,dword ptr [a]
00D5183B sar eax,2 //算数右移
00D5183E mov dword ptr [b],eax
b = a | a;
00D51841 mov eax,dword ptr [a]
00D51844 or eax,dword ptr [a] //或运算
00D51847 mov dword ptr [b],eax
b = a& a;
00D5184A mov eax,dword ptr [a]
00D5184D and eax,dword ptr [a] //且运算
00D51850 mov dword ptr [b],eax
b = a^ a;
00D51853 mov eax,dword ptr [a]
00D51856 xor eax,dword ptr [a] //异或运算
00D51859 mov dword ptr [b],eax
b = ~a;
00D5185C mov eax,dword ptr [a]
00D5185F not eax //取反,非运算
00D51861 mov dword ptr [b],eax
return 0;
编译器优化
常见的有以下几种
常量叠加
int x = 1+2;
1和2都是常量,结果可以预见,所以没必要还要产生一个加法指令,直接生成x=3
常量传播
x = 3;
y = x+3;
结果也是可以遇见的直接生成y=6
减少变量
int x = i*2;
int j = i*2;
if(x>y)
{
}
这里对于x和y的比较等价于i和j的比较所以直接生成if(i>j)
复写传播
x = a;
y = x+c;
与常量传播类似,可直接变为y=a+c
减去不可达分支
if(1>2)
{
}
这种根本不可能执行的分支,没有必要存在
数学变化
代数恒等式
x = a+0;
x = a-0;
x = a*1
x = a/1;
x = a*b+a*c
替换 x= (b+c)*a
代码外提
把代码从一个范围中提出来
while(x>y/2)
{
里面的代码没有和y有关系
}
就可以变换成
t = y/2;
while(x>t)
减少了除法