- 注册时间
 - 2011-1-27
 
- 最后登录
 - 2021-8-19
 
- 在线时间
 - 229 小时
 
 
 
 
 
终身VIP会员 
花钱是让你服务的,不是叫你大哥 ... 
    
	- 魔鬼币
 - 10656 
 
 
 
 | 
 
*第一次写代码分析的文章,所以第一次会详尽一点。 
问题原型: 
有5个人坐在一起,问第5个人多少岁?答, 比第4个人大2岁。 第4个人说他比第3个人大2岁, 第3个人说比第2个人大2岁, 第2个人比第1个人大2岁, 问第1个人时回答是10岁, 那第5个人到底多大? 
// 问题原型是5个人,我们简化为3个人,虽然性质是完全一样的。 
C源码: 
#include <stdio.h> 
int age(int n) 
{ 
        int c; 
        if(n == 1) c = 10; 
        else c = age(n-1) + 2; 
        return c; 
} 
int main(void) 
{ 
        printf("%d", age(3)); 
        return 0; 
} 
*这个子程序, 实参为3,age函数共调用了3次。(n=5~1之间调用了3次) 
 
 
VC++6.0 自带反汇编编译源码: 
10:   void main() 
11:   { 
00401090   push        ebp  //保存ebp的寄存器变量 
00401091   mov         ebp,esp  //将当前指向栈顶指针给ebp,是开辟内存空间的前奏 
00401093   sub         esp,40h //开辟 40h大小的内存空间 
00401096   push        ebx  //基地址保护(待确认) 
00401097   push        esi  //保存记录源地址的寄存器变量, 其中的s可以看做是source 
00401098   push        edi //同上, d 可以看做是destination 
00401099   lea         edi,[ebp-40h]  //edi指向所开辟内存空间的顶端, 目的是为了要对开辟的内存空间实行保护操作; 
0040109C   mov         ecx,10h    // 将循环次数保存到ecx中, 10h = 40h / 4 
004010A1   mov         eax,0CCCCCCCCh  // eax中写入0cccccccch 
004010A6   rep stos    dword ptr [edi]   //循环执行10次,向所开辟的空间写入int 3 
12:       printf("%d", age(3)); 
004010A8   push        3  //age函数实参 
004010AA   call        @ILT+0(age) (00401005)  //age函数,接收一个参数3 
004010AF   add         esp,4 
004010B2   push        eax  //printf参数1 
004010B3   push        offset string "%d" (0042201c)  //参数2,字符串偏移 
004010B8   call        printf (00401130)  //调用库函数printf 
004010BD   add         esp,8    //明显的__cdecl调用约定格式; 
13:   } 
// 恢复环境变量 
004010C0   pop         edi    
004010C1   pop         esi 
004010C2   pop         ebx 
004010C3   add         esp,40h 
004010C6   cmp         ebp,esp 
004010C8   call        __chkesp (004010f0) //堆栈检测,无需深究 
004010CD   mov         esp,ebp 
004010CF   pop         ebp 
004010D0   ret 
 
//age函数反汇编 
2:    int age(int n) 
3:    { 
00401020   push        ebp  //执行3次递归调用 
00401021   mov         ebp,esp 
00401023   sub         esp,44h 
00401026   push        ebx 
00401027   push        esi 
00401028   push        edi 
00401029   lea         edi,[ebp-44h] 
0040102C   mov         ecx,11h 
00401031   mov         eax,0CCCCCCCCh 
00401036   rep stos    dword ptr [edi] 
4:        int c; 
5:        if(n == 1) c = 10; 
00401038   cmp         dword ptr [ebp+8],1  //ebp+8显然就是形参c了,这是一个明显的if指令 
0040103C   jne         age+27h (00401047)  //n!=1则跳转 
// n = 1 则执行下述指令; 
0040103E   mov         dword ptr [ebp-4],0Ah 
6:        else c = age(n-1) + 2; 
00401045   jmp         age+3Ch (0040105c) 
// n != 1 则执行下述指令; 
00401047   mov         eax,dword ptr [ebp+8] 
0040104A   sub         eax,1  //sub eax, 1 就是 n -1 
0040104D   push        eax  
0040104E   call        @ILT+0(age) (00401005) //在age函数内部再次调用age函数 
00401053   add         esp,4 
00401056   add         eax,2 
00401059   mov         dword ptr [ebp-4],eax  //执行完3次调用age函数后,eax=10,ebp-4为变量c 
7:        return c; 
0040105C   mov         eax,dword ptr [ebp-4] 
8:    } 
0040105F   pop         edi 
00401060   pop         esi 
00401061   pop         ebx 
00401062   add         esp,44h 
00401065   cmp         ebp,esp 
00401067   call        __chkesp (004010f0) 
0040106C   mov         esp,ebp 
0040106E   pop         ebp 
0040106F   ret 
        假设我们传递到age函数里面的实参为3; 
        1. 首先走到004010AA   call        @ILT+0(age) (00401005) 处, f11进入age函数内部 ,观察并记录进入内部后的esp栈顶指向的值; 此时esp=0x0012ff2c , 0x0012ff2c地址指向的数据为0x004010af ,即执行完当前call后的eip的值; 这也验证了一个执行call的经典规则: push eip & jmp call;  
        进入call内部后,第一条指令 00401005   jmp         age (00401020) ,继续F10正式进入call内部,接着往下走,或者ctrl+F10走到 一个递归自调用age函数前, F11进入后,记录下当前的esp中的数据为: ss:[esp] = 0x00401053; 
        递归调用3次age函数后,执行到ret指令(第三次调用age函数中)处,此时esp=0012fe74,[esp] = 00401053 (显然是 第三次调用age函数完成后的eip值) ,恢复栈平衡后, add eax, 2 即 age(n-1)+2 。此时变量c = 10 + 2 ,反汇编代码即为: mov         dword ptr [ebp-4],eax  //ebp-4 为变量c , 0040105C   mov    eax,dword ptr [ebp-4]在这里, 说明age(2) = 12 ;我们ret处返回; 
        继续返回到 00401053 处 ,继续上一步的操作后,变量c = 10 + 2 + 2 ,age(3) = 14 
        然后我们返回到 0x004010af 处,自此走出了age函数; 
        不难发现, ret指令的运行机制就是:pop eip 
        重要:再补充一点,递归函数写起来简单,理解起来有难度,其中问题的关键在于,多次调用call之后,没有ret。 这个例子中,是连续3次 push eip ,但是都没有ret,调用后第三次后 连续ret3次, 这也就是递归的本质, 先多次递推调用函数,然后多次回推! 
*ret的本质(属于段内转移): 
Pop eip //将esp指向的栈顶元素弹出堆栈 存放到 eip寄存器中去; 
执行完pop eip后,以一条指令执行的位置即eip执行的内存地址处开始执行; 
 
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 
od反汇编源码: 
*我们跳过启动函数。 
*这里我用得是吾爱破解版的原版OD,出于不同的名称修饰约定,所以事先说明下。 
 
00401286  |.  52            push edx 
00401287  |.  A1 847C4200   mov eax,dword ptr ds:[__argv] 
0040128C  |.  50            push eax 
0040128D  |.  8B0D 807C4200 mov ecx,dword ptr ds:[__argc] 
00401293  |.  51            push ecx 
//这里就是我们写的main函数的入口地址了,上面都是一些启动函数,暂时无须深究,进入; 
00401294  |.  E8 71FDFFFF   call 1.0040100A 
00401299  |.  83C4 0C       add esp,0C 
0040129C  |.  8945 E4       mov [local.7],eax 
 
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 
(OD)main函数反汇编代码: 
//显然是比vc++6.0显示的要简洁明了多了,这里我们不进行深入分析了; 
00401090 >/> \55            push ebp 
00401091  |.  8BEC          mov ebp,esp 
00401093  |.  83EC 40       sub esp,40 
00401096  |.  53            push ebx 
00401097  |.  56            push esi 
00401098  |.  57            push edi 
00401099  |.  8D7D C0       lea edi,[local.16] 
0040109C  |.  B9 10000000   mov ecx,10 
004010A1  |.  B8 CCCCCCCC   mov eax,CCCCCCCC 
004010A6  |.  F3:AB         rep stos dword ptr es:[edi] 
004010A8  |.  6A 03         push 3 
//走到004010aa处,我们看堆栈,显示0012FF30   00000003  \Arg1 = 00000003 (arg1就是形参1,这里只有1个形参),进入: 
004010AA  |.  E8 56FFFFFF   call 1.00401005 
004010AF  |.  83C4 04       add esp,4 
004010B2  |.  50            push eax                                 ; /<%d> 
004010B3  |.  68 1C204200   push 1.0042201C                          ; |format = "%d" 
004010B8  |.  E8 73000000   call 1.printf                            ; \printf 
004010BD  |.  83C4 08       add esp,8 
004010C0  |.  5F            pop edi 
004010C1  |.  5E            pop esi 
004010C2  |.  5B            pop ebx 
004010C3  |.  83C4 40       add esp,40 
004010C6  |.  3BEC          cmp ebp,esp 
004010C8  |.  E8 23000000   call 1._chkesp 
004010CD  |.  8BE5          mov esp,ebp 
004010CF  |.  5D            pop ebp 
004010D0  \.  C3            retn 
 
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 
(OD)age函数反汇编代码: 
//反汇编代码略长,但除却必要的环境保护和恢复之外,代码量已经不大了,VC++6.0的ebp看的头痛,这里有种眼前一亮的感觉! 
00401020 >/> \55            push ebp 
00401021  |.  8BEC          mov ebp,esp 
00401023  |.  83EC 44       sub esp,44 
00401026  |.  53            push ebx 
00401027  |.  56            push esi 
00401028  |.  57            push edi 
00401029  |.  8D7D BC       lea edi,[local.17] 
0040102C  |.  B9 11000000   mov ecx,11 
00401031  |.  B8 CCCCCCCC   mov eax,CCCCCCCC 
00401036  |.  F3:AB         rep stos dword ptr es:[edi] 
//arg.1 是 形参1 
00401038  |.  837D 08 01    cmp [arg.1],1  
0040103C  |.  75 09         jnz short 1.00401047 
//local.1 是 局部变量1 
0040103E  |.  C745 FC 0A000>mov [local.1],0A 
00401045  |.  EB 15         jmp short 1.0040105C 
00401047  |>  8B45 08       mov eax,[arg.1] 
0040104A  |.  83E8 01       sub eax,1 
0040104D  |.  50            push eax 
0040104E  |.  E8 B2FFFFFF   call 1.00401005 
00401053  |.  83C4 04       add esp,4 
00401056  |.  83C0 02       add eax,2 
00401059  |.  8945 FC       mov [local.1],eax 
0040105C  |>  8B45 FC       mov eax,[local.1] 
0040105F  |.  5F            pop edi 
00401060  |.  5E            pop esi 
00401061  |.  5B            pop ebx 
00401062  |.  83C4 44       add esp,44 
00401065  |.  3BEC          cmp ebp,esp 
00401067  |.  E8 84000000   call 1._chkesp 
0040106C  |.  8BE5          mov esp,ebp 
0040106E  |.  5D            pop ebp 
0040106F  \.  C3            retn 
 
*用od分析,显然难度急骤降低. 以后我们会逐渐从简单的开始, 然后会用其他工具,比如ida, 难度梯进式的反汇编分析一些更有挑战性的例子。比如在游戏中会常用到的链表、二叉树等等。 
*递归的本质 就是  
I.        回推阶段 
II.        递推阶段 |   
 
评分
- 
查看全部评分
 
 
 
 
 
 |