1) Debug版本算法反汇编,现有如下3×3矩阵相乘的程序:
1 #define SIZE 3
2 int MyFunction(int a[SIZE][SIZE],int b[SIZE][SIZE],int c[SIZE][SIZE])
3 {
4 int i,j;
5 for ( i = 0 ; i < 3 ; i++ )
6 {
7 for ( j = 0 ; j < 3 ; j++ )
8 {
9 c[i][j] = a[i][0]*b[0][j] + a[i][1]*b[1][j] + a[i][2]*b[2][j];
10 }
11 }
12 return 0;
13 }
Debug版本汇编后为:
1 #define SIZE 3
2 int MyFunction(int a[SIZE][SIZE],int b[SIZE][SIZE],int c[SIZE][SIZE])
3 {
4 00401020 push ebp
5 00401021 mov ebp,esp
6 00401023 sub esp,48h ;48H字节局部变量存储区
7 00401026 push ebx
8 00401027 push esi
9 00401028 push edi
10 00401029 lea edi,[ebp-48h]
11 0040102C mov ecx,12h
12 00401031 mov eax,0CCCCCCCCh
13 00401036 rep stos dword ptr [edi]
14 int i,j;
15 for ( i = 0 ; i < 3 ; i++ )
16 00401038 mov dword ptr [ebp-4],0 ;ebp – 4 为局部变量i
17 0040103F jmp MyFunction+2Ah (0040104a)
18 00401041 mov eax,dword ptr [ebp-4]
19 00401044 add eax,1
20 00401047 mov dword ptr [ebp-4],eax
21 0040104A cmp dword ptr [ebp-4],3
22 0040104E jge MyFunction+0AAh (004010ca) ;标准for循环语句
23 {
24 for ( j = 0 ; j < 3 ; j++ )
25 00401050 mov dword ptr [ebp-8],0 ;ebp – 8 为局部变量j
26 00401057 jmp MyFunction+42h (00401062)
27 00401059 mov ecx,dword ptr [ebp-8]
28 0040105C add ecx,1
29 0040105F mov dword ptr [ebp-8],ecx
30 00401062 cmp dword ptr [ebp-8],3
31 00401066 jge MyFunction+0A5h (004010c5) ;标准for循环语句
32 {
33 c[i][j] = a[i][0]*b[0][j] + a[i][1]*b[1][j] + a[i][2]*b[2][j];
34 00401068 mov edx,dword ptr [ebp-4] ;取i值
35 0040106B imul edx,edx,0Ch ;一级偏移:第一下标[i]
36 0040106E mov eax,dword ptr [ebp+8] ;ebp+8为第1参数:数组a
37 00401071 mov ecx,dword ptr [ebp-8] ;取j值
38 00401074 mov esi,dword ptr [ebp+0Ch] ;ebp+C为第2参数:数组b
39 00401077 mov edx,dword ptr [eax+edx] ;a[i][0]
40 0040107A imul edx,dword ptr [esi+ecx*4] ;a[i][0] * b[0][j] -> edx
41 0040107E mov eax,dword ptr [ebp-4]
42 00401081 imul eax,eax,0Ch
43 00401084 mov ecx,dword ptr [ebp+8]
44 00401087 mov esi,dword ptr [ebp-8]
45 0040108A mov edi,dword ptr [ebp+0Ch]
46 0040108D mov eax,dword ptr [ecx+eax+4] ;a[i][1]
47 00401091 imul eax,dword ptr [edi+esi*4+0Ch]; a[i][1] * b[1][j] ->eax
48 ;这里注意:0CH = b[1]–b[0] = ( 1- 0 ) * SIZE * sizeof( int )
49 00401096 add edx,eax ;edx + eax -> edx
50 ;即edx = a[i][0] * b[0][j] + a[i][1] * b[1][j]
51 00401098 mov ecx,dword ptr [ebp-4]
52 0040109B imul ecx,ecx,0Ch
53 0040109E mov eax,dword ptr [ebp+8]
54 004010A1 mov esi,dword ptr [ebp-8]
55 004010A4 mov edi,dword ptr [ebp+0Ch]
56 004010A7 mov ecx,dword ptr [eax+ecx+8] ; a[i][2]
57 004010AB imul ecx,dword ptr [edi+esi*4+18h]; a[i][2] * b[2][j] ->ecx
58 ;同上:018H = b[2]–b[0] = ( 2- 0 ) * SIZE * sizeof( int )
59 004010B0 add edx,ecx ;edx + eax -> edx
60 ;即edx = a[i][0] * b[0][j] + a[i][1] * b[1][j] + a[i][2] * b[2][j]
61 004010B2 mov eax,dword ptr [ebp-4]
62 004010B5 imul eax,eax,0Ch
63 004010B8 mov ecx,dword ptr [ebp+10h] ;ebp+10第3参数:数组c
64 004010BB add ecx,eax ;c[i] -> ecx
65 004010BD mov eax,dword ptr [ebp-8]
66 004010C0 mov dword ptr [ecx+eax*4],edx ;edx -> c[i][j]
67 }
68 004010C3 jmp MyFunction+39h (00401059) ;内层循环
69 }
70 004010C5 jmp MyFunction+21h (00401041) ;外层循环
71 return 0;
72 004010CA xor eax,eax ;返回0
73 }
74 004010CC …
75 //省略现场恢复代码
再看看这个函数的调用,使用如下代码:
1 int main(void)
2 {
3 int a[SIZE][SIZE] = {1,2,3,4,5,6,7,8,9};
4 int b[SIZE][SIZE] = {9,8,7,6,5,4,3,2,1};
5 int c[SIZE][SIZE];
6 MyFunction(a,b,c);
7 return 0;
8 }
对应的汇编代码为:
1 int main(void)
2 {
3 00401100 push ebp ;将看到标准现场保护
4 00401101 mov ebp,esp ;ebp为栈基址指针
5 00401103 sub esp,0Ach ;临时取堆栈大小0ACH
6 00401109 push ebx
7 0040110A push esi
8 0040110B push edi
9 0040110C lea edi,[ebp-0ACh] ;起点
10 00401112 mov ecx,2Bh ;循环次数
11 00401117 mov eax,0CCCCCCCCh ;所赋的值
12 0040111C rep stos dword ptr [edi] ;初始化临时区
13 int a[SIZE][SIZE] = {1,2,3,4,5,6,7,8,9}; ;以下为局部变量初始化
14 0040111E mov dword ptr [ebp-24h],1 ;a数组起始地址epb – 24H
15 00401125 mov dword ptr [ebp-20h],2 ;ebp为栈基址指针
16 0040112C mov dword ptr [ebp-1Ch],3 ;故这里的地址是逐渐增大
17 00401133 mov dword ptr [ebp-18h],4
18 0040113A mov dword ptr [ebp-14h],5
19 00401141 mov dword ptr [ebp-10h],6
20 00401148 mov dword ptr [ebp-0Ch],7
21 0040114F mov dword ptr [ebp-8],8
22 00401156 mov dword ptr [ebp-4],9 ;赋值
23 int b[SIZE][SIZE] = {9,8,7,6,5,4,3,2,1};
24 0040115D mov dword ptr [ebp-48h],9 ;b数组起始地址ebp – 48H
25 00401164 mov dword ptr [ebp-44h],8
26 0040116B mov dword ptr [ebp-40h],7
27 00401172 mov dword ptr [ebp-3Ch],6
28 00401179 mov dword ptr [ebp-38h],5
29 00401180 mov dword ptr [ebp-34h],4
30 00401187 mov dword ptr [ebp-30h],3
31 0040118E mov dword ptr [ebp-2Ch],2
32 00401195 mov dword ptr [ebp-28h],1
33 int c[SIZE][SIZE];
34 MyFunction(a,b,c);
35 0040119C lea eax,[ebp-6Ch] ;c数组起始地址ebp – 6CH
36 0040119F push eax ;传递第3个参数(c)
37 004011A0 lea ecx,[ebp-48h]
38 004011A3 push ecx ;传递第2个参数(b)
39 004011A4 lea edx,[ebp-24h]
40 004011A7 push edx ;传递第1个参数(a)
41 004011A8 call @ILT+0(MyFunction) (00401005)
42 004011AD add esp,0Ch ;恢复堆栈
43 return 0;
44 004011B0 xor eax,eax
45 }
46 004011B2 …
47 //省略现场恢复
第一:C语言在函数入口时在栈区开辟临时存储区来存储局部变量,这些局部变量的生存周期一直到函数返回;
第二:局部变量的声明并不会对其进行赋值,只有在赋值语句出现时,才对内存进行读写;
第三:C语言中不管以什么方式传递数组,传递的永远是指针值(它在MyFunction函数中并没有对传递过来的数组实行一次拷贝);
第四:所有的下标运算符同点操作符一样被转为偏移量的形式;
第五:读Debug版汇编很简单,写Debug版汇编是一个挑战
2) 阅读汇编代码技巧:先分清楚控制流程代码与数值计算代码。对数值计算的代码判断输入与输出(被读的内部变量为输入,被写的内部变量为输出),还原成C语言。任何一段中间不带跳转的连续MOV和加减乘除指令均可还原为一个C表达式。
3) 对于二(2)中的for循环函数:
1 int MyFunction(int a,int b)
2 {
3 int c = a + b;
4 int i;
5 for ( i = 0 ; i < 50 ; i ++ )
6 {
7 c = c + i;
8 }
9 return c;
10 }
在Release版本中反汇编为(我采用的是OllyICE反汇编,其中RETN及为RET):
可以看到与Debug版本的汇编代码大相径庭:第一:没有现场保护与现场恢复代码;第二:并没有为参数以及自定义的局部变量分配临时存储区域;第三:for循环的结构显然被改写。
先看看这个函数是怎么被调用的:
可以看到,调用代码与Debug版本并没有区别,先把参数1,2反序压入堆栈,随后执行调用函数,最后恢复堆栈指针(这里的返回0不用理会,Release汇编以后,在函数入口(主函数)中的函数调用会另外生成一个函数来执行,即上面所看到的代码段)。
综上:
第一:Release版本会对代码执行优化,甚至不保存EBP,完全不做现场保护与恢复工作;
第二:对于数量较少的局部变量,Release版本并不会为它们在堆栈上分配临时变量存储区域,这意味着ESP也不需要进行操作(RET指令除外),取参数直接用ESP加上偏移来取(第一个参数偏移4,是因为执行CALL调用的时候实现了一次压栈);
第三:既然不分配临时变量存储区域,那么临时变量将使用寄存器来存放,这与C语言中指定的寄存器变量等同?目前我还不清楚。不过对于频繁操作的变量(如本例的计数器局部变量i)会自动优化为使用寄存器;
第四:内部的结构可能会被部分篡改或者完全改变,无法和原C语言一一对应(如本例优化后的for循环使用了相对简单的do循环方式,把判断和跳转放到了最后)
下面看看三(1)中矩阵相乘的例子,先简单看下调用代码是否有区别:
简要分析如下:
第1行:分配局部变量栈区06CH( = 108 )字节(32位下相当于27个int型数值)
第2~4行:将常数7、8、9移动到寄存器
第5~11行(排除第9行):对数组a、b赋值7,8,9
第12~16行(外加第9行):取数组c、b、a地址并压入堆栈实现参数传递
第17~22行:对数组b其余单元进行赋值
第23~28行:对数组a其余单元进行赋值
第29行:执行函数调用
第30行:清EAX,即设置返回值为0
第31行:恢复堆栈,078H = 06CH(局部变量堆栈区) + 08H(3次PUSH操作)
对与赋值的与参数取址传递的过程很不和谐,直到函数调用前,堆栈区情况如下:
堆栈地址 |
值(HEX) |
描述 |
改变代码行号 |
… |
… |
非局部变量堆栈区 |
|
0012FF0C |
0012FF3C |
第1个参数,a数组起始地址 |
16 |
0012FF10 |
0012FF18 |
第2个参数,b数组起始地址 |
15 |
0012FF14 |
0012FF60 |
第3个参数,c数组起始地址 |
13 |
0012FF18 |
00000009 |
b[0][0]:b数组起始地址 |
6 |
0012FF1C |
00000008 |
b[0][1] |
8 |
0012FF20 |
00000007 |
b[0][2] |
11 |
0012FF24 |
00000006 |
b[1][0] |
23 |
0012FF28 |
00000005 |
b[1][1] |
24 |
0012FF2C |
00000004 |
b[1][2] |
25 |
0012FF30 |
00000003 |
b[2][0] |
26 |
0012FF34 |
00000002 |
b[2][1] |
27 |
0012FF38 |
00000001 |
b[2][2] |
28 |
0012FF3C |
00000001 |
a[0][0]:a数组起始地址 |
17 |
0012FF40 |
00000002 |
a[0][1] |
18 |
0012FF44 |
00000003 |
a[0][2] |
19 |
0012FF48 |
00000004 |
a[1][0] |
20 |
0012FF4C |
00000005 |
a[1][1] |
21 |
0012FF50 |
00000006 |
a[1][2] |
22 |
0012FF54 |
00000007 |
a[2][0] |
10 |
0012FF58 |
00000008 |
a[2][1] |
7 |
0012FF5C |
00000009 |
a[2][2] |
5 |
0012FF60 |
随机值 |
c[0][0]:c数组起始地址 |
|
0012FF64 |
随机值 |
c[0][1] |
|
0012FF68 |
随机值 |
c[0][2] |
|
0012FF6C |
随机值 |
c[1][0] |
|
0012FF70 |
随机值 |
c[1][1] |
|
0012FF74 |
随机值 |
c[1][2] |
|
0012FF78 |
随机值 |
c[2][0] |
|
0012FF7C |
随机值 |
c[2][1] |
|
0012FF80 |
随机值 |
c[2][2] |
|
0012FF84 |
… |
非局部变量堆栈区 |
|
综上:
第一:Release总是最大限度的使用寄存器(如本例的赋值拆成了两部分,把7、8、9放入寄存器后赋值比使用立即数赋值更高效?暂时不得而知);
第二:Release汇编总是使用尽量少的临时堆栈区(如这里使用3个大小为9的二维int型数组,刚好108个字节,编译器并没有进行多余的分配,这与Debug版本不同,Debug下一般都会保证一定余量);
第三:Release汇编代码与C语句并没有一一对应的关系,顺序结构完全可能被打乱,必须从操作的结果来分析操作的目的(如这里是对数组的初始化与函数调用,但初始化被参数压栈“打断”)
可见,Release版本虽然汇编代码与Debug版本很多差别,但是完成的功能类似,下面看看MyFunction的反汇编代码
详细分析如下(这里数据段DS与堆栈段基址SS相等):
第1行:取第1个参数(数组a起始地址)到EAX
第2行:取第3个参数(数组c起始地址)到EDX
第3~6行:现场保护
第7行:ECX = a[0][1]
第8行:寄存器EDI作为外层循环计数器,赋初值EDI = 3
第9行:函数被调用以后,ESP被移动了5次(一次CALL+4次PUSH),则这里的ESP+18H实际上指向的是调用前的ESP + 18H – 20 = ESP + 4,即指向第2个函数参数。故此处为取第2个参数(数组b起始地址)到EAX
第10行:ESI作为内层循环计数器,赋初值3
第11~13行:EAX = b[1][0],EBX = b[2][0],EBP = b[0][0],即取b数组的一列(第一次循环是第1列,下内层循环一次,移动到下一列)
第14~15行:EBX = a[0][2] * b[2][0],EBP = a[0][0] * b[0][0],a的第1行,与上类似
第16行:EBX = a[0][2] * b[2][0] + a[0][0] * b[0][0]
第17~19行:EBX = a[0][2] * b[2][0] + a[0][1] * b[1][0] + a[0][0] * b[0][0]
第20行:EXA = EXA + 4,即指向下一列
第21~22行:将结果EBX写入数组c,c向后移动一个单位
第23~24行:内层循环计数器减1,循环判断
第25行:a数组移动到下一行
第26~27行:外层循环计数器减1,循环判断
第28~32行:置返回值为0,恢复现场
综上:
第一:两个for循环均采用了do-while循环的形式;
第二:打乱了c[i][j] = a[i][0]*b[0][j] + a[i][1]*b[1][j] + a[i][2]*b[2][j]从左到右的相加次序;
第三:与Debug下汇编相比,大量使用寄存器来存储变量,数组下标的偏移变的隐晦;
第四:并非所有Release版反汇编都可以还原为编译之前的C语言代码,但是还原成功能等价的C语言代码还是可能的
总述:读Release版汇编很蛋疼,写Release版汇编是不可能!
说明:这里看到的汇编源代码与原书并不相同,原书仅仅作为引导,我以实际为准自行分析与学习。所有如有错误,与原书无关。
4) 阅读Release汇编代码时,如2)所言先分清楚控制流程代码与数值计算代码。对数值计算代码的阅读可以使用逆向的方法:先判断输出,如在3*3矩阵乘法Release版本汇编代码时,先标记出所有控制流程的代码,判断为双层嵌套循环,形式如:
1 int i,j;
2 for( int i = 0 ; i < 3 ; i ++ )
3 {
4 //dosomething
5 for( j = 0 ; j < 3 ; j ++ )
6 {
7 //dosomething
8 }
9 }
然后进行数值计算代码分析时,先找到输出变量,这里由语句找到语句MOV PTR DWORD DS:[EDX],EBX可以判断,DS:[EDX]是输出变量,而且整段代码只有这一处对它进行了修改,然后逆向追踪EBX,从而最终得到由已知输入变量计算得到输出变量的计算表达式。