先看下面的简单实例:
#include int counts(int n){printf("call: %d",n);if(n==0)return n;else{counts(n-1);printf("back: %d",n);return n;}}void main(){printf("%d",counts(3));getchar();}
运行结果:
call: 3call: 2call: 1call: 0back: 1back: 2back: 3
关键是明白其代码调用、回归的流程,以及其参数值是如何迭代的。
基本的流程如下:
下面从汇编的层面分析:
首先是主函数调用递归函数开始:
18: printf("%d",counts(3));0040D7B8 push 30040D7BA call @ILT+0(counts) (00401005)0040D7BF add esp,4
call指令会完成两个动作,先是将call指令的下一条指令的地址0040D7BF压栈(push),以便返回;然后是执行一个jmp指令:
00401005 jmp counts (00401020)
跳转到函数开始处00401020,先整体看一下函数的汇编代码:
3: int counts(int n)4: {00401020 push ebp00401021 mov ebp,esp00401023 sub esp,40h00401026 push ebx00401027 push esi00401028 push edi00401029 lea edi,[ebp-40h]0040102C mov ecx,10h00401031 mov eax,0CCCCCCCCh00401036 rep stos dword ptr [edi]5: printf("call: %d",n);00401038 mov eax,dword ptr [ebp+8]0040103B push eax0040103C push offset string "call: %d" (00422028)00401041 call printf (00401110)00401046 add esp,86: if(n==0)00401049 cmp dword ptr [ebp+8],00040104D jne counts+34h (00401054)7: return n;0040104F mov eax,dword ptr [ebp+8]00401052 jmp counts+57h (00401077)8: else9: {10: counts(n-1);00401054 mov ecx,dword ptr [ebp+8]00401057 sub ecx,10040105A push ecx0040105B call @ILT+0(counts) (00401005)00401060 add esp,411: printf("back: %d",n);00401063 mov edx,dword ptr [ebp+8]00401066 push edx00401067 push offset string "back: %d" (0042201c)0040106C call printf (00401110)00401071 add esp,812: return n;00401074 mov eax,dword ptr [ebp+8]13: }14: }00401077 pop edi00401078 pop esi00401079 pop ebx0040107A add esp,40h0040107D cmp ebp,esp0040107F call __chkesp (00401190)00401084 mov esp,ebp00401086 pop ebp00401087 ret
然后分开来分析:
3: int counts(int n)4: {00401020 push ebp00401021 mov ebp,esp00401023 sub esp,40h00401026 push ebx00401027 push esi00401028 push edi00401029 lea edi,[ebp-40h]0040102C mov ecx,10h00401031 mov eax,0CCCCCCCCh00401036 rep stos dword ptr [edi]
进入函数后,先压栈底指针,抬高栈底指针,提升栈顶指针,压寄存器,初始化这一段栈帧空间(debug模式)。
通常是一段这样的操作:
(地址值仅供参考)
继续下面的汇编代码:
5: printf("call: %d",n);00401038 mov eax,dword ptr [ebp+8]0040103B push eax0040103C push offset string "call: %d" (00422028)00401041 call printf (00401110)00401046 add esp,86: if(n==0)00401049 cmp dword ptr [ebp+8],00040104D jne counts+34h (00401054)7: return n;0040104F mov eax,dword ptr [ebp+8]00401052 jmp counts+57h (00401077)8: else9: {10: counts(n-1);00401054 mov ecx,dword ptr [ebp+8]00401057 sub ecx,10040105A push ecx0040105B call @ILT+0(counts) (00401005)00401060 add esp,4
上面的cmp,在CPU内部是做一个减法,修改标志寄存器的值,根据标志寄存器的值形成跳转。
参数n减1后压栈。
递归调用函数,call指令会将地址00401060压栈,跳转到00401005。
循环上面的操作,继续抬高堆栈,直到参数值n==0:
00401049 cmp dword ptr [ebp+8],00040104D jne counts+34h (00401054)7: return n;0040104F mov eax,dword ptr [ebp+8]00401052 jmp counts+57h (00401077)
返回:
00401077 pop edi00401078 pop esi00401079 pop ebx0040107A add esp,40h0040107D cmp ebp,esp0040107F call __chkesp (00401190)00401084 mov esp,ebp00401086 pop ebp00401087 ret
C代码的返回,在汇编层面还有一些代码要生成并被执行,就是堆栈平衡,
然后是ret,与call相对应,ret指令也是执行两个动作,pop出在call时压进栈的返回地址,执行jmp命令返回:
00401060 add esp,411: printf("back: %d",n);00401063 mov edx,dword ptr [ebp+8]00401066 push edx00401067 push offset string "back: %d" (0042201c)0040106C call printf (00401110)00401071 add esp,812: return n;00401074 mov eax,dword ptr [ebp+8]13: }14: }00401077 pop edi00401078 pop esi00401079 pop ebx0040107A add esp,40h0040107D cmp ebp,esp0040107F call __chkesp (00401190)00401084 mov esp,ebp00401086 pop ebp00401087 ret
引用递归调用的函数参数,将返回值压到寄存器eax,堆栈平衡,再ret返回。
循环上面的操作3次,ret到最首先调用的下一个语句的地址0040D7BF:
0040D7BF add esp,40040D7C2 push eax0040D7C3 push offset string "%d" (00422034)0040D7C8 call printf (00401110)0040D7CD add esp,819: getchar();
到此,递归调用完成。
存放堆栈指针的寄存器ebp、esp的值回归到原处。
-End-