学了逆向分析之后,发现,虽然我们写了程序,但是程序并不是按完全按照我们写的代码一句
一句执行的,很有可能一大部分代码都被编译器优化修改了。只有了解这些东西,我们才能写出
性能更加优益,可读性与精炼并存的代码。
在VC++6.0中,算术运算和其他传递计算结果的代码组合起来才能被视为一条有效的语句
,如赋值运算或函数的参数传递。单独的运算虽然可以编译通过,但是并不会生成代码。因为
只进行计算而没有传递结果的运算不会对程序结果又任何影响,此时编译器将其视为无效语句
,与空语句等价,不会有任何编译处理。
加法运算:加法运算对应的汇编指令为ADD。在执行加法运算时,针对不同的操作数,转
换的指令也会不同,编译器会根据优化方式选择最佳的匹配方案。VC++6.0中常见的优化方案
有两种:O1方案,生成文件占用空间最小,O2方案,执行效率最快。
我们来使用不同类型的操作数来查看加法运算在Debug编译选项组下编译后的汇编代码
//C++源码说明,加法运算
15+20; //无效语句
int nVarOne = 0; //变量定义
int nVarTwo = 0;
nVarOne = nVarOne + 1; //变量加常量的加法运算
nVarOne = 1 + 2; //常量相加的加法运算
nVarOne = nVarOne + nVarTwo;//两个变量相加的加法运算
printf("nVarOne = %d \r\n",nVarOne);
//C++源码与对应的汇编代码讲解
//C++
int nVarOne = 0;
//反汇编
mov dword ptr [ebp-4],0//将立即数0,传入地址ebp-4中,即变量nVarOne所在的地址
//C++
int nVarTwo = 0;
//反汇编
mov dword ptr [ebp-8],0//将立即数0,传入地址ebp-8中,即变量nVarTwo所在的地址
//C++
nVarOne = nVarOne + 1;
//反汇编
mov eax,dword ptr [ebp-4]//把nVarOne数据放入eax中
add eax,1//对eax执行加1运算
mov dword ptr [ebp-4],eax//把结果放回变量nVarOne中,完成加法运算
//C++
nVarOne = 1 + 2;
//反汇编
mov dword ptr [ebp-4],3//这里编译器直接算出两个常量相加后的结果,放入变量
nVarOne中
//C++
nVarOne = nVarOne + nVarTwo;
//反汇编
mov ecx,dword ptr [ebp-4]//将nVarOne中的值放入ecx
add ecx,dword ptr [ebp-8]//ecx与nVarTwo中的值做加法运算
mov dword ptr [ebp-4],ecx//把ecx中的值保存到nVarOne所在的地址中
在开启O2选项后,编译出的汇编代码将会有较大的变化。由于效率优先,编译器会将无用
的代码去除,并将可合并代码进行归并处理。例如,上面代码中“nVarOne = nVarOne + 1”
这样的语句会被删除,因为在其后又重新对变量nVarOne进行了赋值操作,并且在此之前没有
对变量nVarOne的任何访问,所以编译器判定此句代码是可删除的。
编译器常常采用“常量传播”和“常量折叠”这样的方案对代码中的变量与常量进行优
化。
常量传播:编译期间可计算出结果的变量转换成常量
如:
void main(){
int nVar = 1;
printf("nVarOne = %d \n",nVar);
}
等价于
void main(){
printf("nVarOne = %d\n",1);
}
常量折叠:当计算公式中出现多个常量进行计算的情况时,且编译器可以计算出结果时
,这样源码中所有的常量计算都被计算结果代替
如:
void main(){
int nVar = 1 + 5 -3 * 6;
printf("nVarOne = %d \n",nVar);
}
此时不会产生计算指令,1+5-3*6 = -12,编译器将用-12替换原表达式
void main(){
int nVar = -12;
printf("nVarOne = %d \n",nVar);
}
现在变量nVar是一个在编译期间可计算出的变量,那么接下来组合使用“常量传播”
对其进行常量转换是很合理的,程序中将不会出现变量
void main(){
printf("nVarOne = %d \n",-12);
}
下面再来看个例子:
//C源码
int main(int argc,char * argv[]){
int nVarOne = argc;
int nVarTwo = argc;
nVarOne = nVarOne+1;
nVarOne = 1+2;
nVarOne = nVarOne + nVarTwo;
printf("nVarOne = %d]n",nVarOne);
return 0;
}
//Release版反汇编
;int _cdecl mian(int argc,const char ** argv,const char **envp)
_main proc near
arg_0 = dword ptr 4
mov eax,[esp+arg_0]
add eax,3
push eax
push offset format ;"nVarOne = %d \n"
call _printf
add esp,8
xor eax,eax
retn
_main endp
这次代码中多了arg_0的定义使用。arg_0位IDA分析出的参数偏移,以后会说到,这
里只需知道[esp+arg_0]是在获取参数即可。我们来来看下优化过程。
int main(int argc,char * argv[]){
//int nVarOne = argc;在后面的代码中被常量代替
//int nVarTwo = argc;虽然不能被常量代替,但是由于之后没有对nVarTwo
进行修改,所以引用nVarTwo等价于引用argc,nVarTwo则被删除掉,这种方法称为“复写传播
”
//nVarOne = nVarOne +1;//随后重新对nVarOne赋值,这句被删除了
//nVarOne = 1+2;常量折叠,等价于nVarOne = 3;
//nVarOne = nVarOne + nVarTwo;常量传播和复写传播,等价于nVarOne =
3+argc;
//printf("nVarOne = %d\n",nVarOne);后面对nVarOne没有访问,可以用
3+argc代替
printf("nVarOne = %d\n",3+argc);
return 0;
}
编译器在编译期间通过对源码的分析,判定第二个变量nVarTwo可省略,因为它都被赋值为第一个参数argc。在变量nVarOne被赋值为3后,就做了两个变量的加法nVarOne = nVarOne + nVarTwo,这就等同于变量nVarOne = 3 +argc。其后printf引用nVarOne,也就等价于引用3+argc,因此nVarOne也可以被删除掉。