汇编之递归
这是一个经典的递归代码
用来累加3+2+1
#include<stdio.h>
int accumulation(int n){
n&&(n+=accumulation(n-1));
return n;
}
int main(){
printf("%d\n",accumulation(3));
return 0;
}
vc++反汇编查看后,看主要的几句:
printf("%d\n",accumulation(3)):
push 3
call @ILT+0(_accumulation) (00401005)
add esp,4
push eax
push offset string "%d\n" (0042201c)
call printf (00401120)
add esp,8
首先将3压入了堆栈,接下来调用accumulation函数,但并不是直接调用到了函数入口处。
@ILT+0(_accumulation):
00401005 E9 16 00 00 00 jmp accumulation (00401020)
@ilt相当于一个跳板,记录了一些函数的入口位置,当一个程序中多次调用一个函数,都可以通过ilt进行跳转,这样的好处是当函数入口位置更改时,只要修改@ilt中的位置,有利于提高效率。
n&&(n+=accumulation(n-1)):
cmp dword ptr [ebp+8],0
je accumulation+35h (00401055)
mov eax,dword ptr [ebp+8]
sub eax,1
push eax
call @ILT+0(_accumulation) (00401005)
add esp,4
mov ecx,dword ptr [ebp+8]
add ecx,eax
mov dword ptr [ebp+8],ecx
到了accumulation函数中首先当然是开辟栈空间等一系列常规操作,然后就到了最主要的部分。
第一步
- 将【ebp+8】的值与0进行比较,这个【ebp+8】实际上就是之前被压入栈中的3.比较发现不相等,继续往下走。
- eax=3
- eax=eax-1=2
- eax的值压入栈
- 调用自身
调用自己后又是一系列的开辟栈空间的常规操作,然后。
第二步
- 将【ebp+8】的值与0进行比较,这时【ebp+8】的值是之前被压入栈的eax的值2
- 把2赋给eax,eax=2
- eax=eax-1=1
- 将1压入栈
- 调用自身
第三步
- 将之前被压入栈的值1与0进行比较,不相等,继续往下进行
- eax=1
- eax=eax-1=0
- 将0压入栈
- 调用自身
第四步
- 0与0比较,相等,跳转到accumulation+35h (00401055)的位置,这个位置查看后发现就是return n的位置`。
return n;
00401055 mov eax,dword ptr [ebp+8]
以上这一系列操作完成后大概就是这个样子:
此时的eax是0,这个记住,之后要进行计算。
先看一下栈空间是什么样子:
push ebp
mov ebp,esp
sub esp,40h
push ebx
push esi
push edi
lea edi,[ebp-40h]
mov ecx,10h
mov eax,0CCCCCCCCh
rep stos dword ptr [edi]
以上是每次函数开始都会进行的常规操作,一波操作完后的栈空间大概长这个样子:
地址和上面的图一样,底下是高地址,上边是低地址。
}
pop edi
pop esi
pop ebx
add esp,40h
cmp ebp,esp
call __chkesp (004010e0)
mov esp,ebp
pop ebp
ret
以上操作是逐步退出堆栈,栈空间清空的过程。一通pop操作后,edi,esi等寄存器一次退出,esp也退到了ebp指向的位置,pop ebp后,ebp的位置退到了旧的ebp的位置。
值得注意的是ret操作,这个很有灵性,ret操作主要分为两步:1.将当前esp指向的位置pop给eip。2.add esp,4。
那么当前esp指向什么位置呢?
从最后几步操作来看,mov esp,ebp,pop ebp后esp应该指向图中旧ebp下面的位置,那旧ebp下面是什么呢,在第一幅图中没画出来,实际在每一次call调用时,call函数都会首先把下一步操作的地址压入栈,即 add esp,4这一步的地址.
所以,ret后会执行add esp,4.
接下来就是真正的累加操作了
add esp,4
mov ecx,dword ptr [ebp+8]
add ecx,eax
mov dword ptr [ebp+8],ecx
return n;
mov eax,dword ptr [ebp+8]
……
……
ret
此时的ebp+8是指向之前被压入栈中的数字的,
第一次:
- dword ptr [ebp+8]指向的数字是1,ecx=1
- ecx=ecx+eax=1+0=1
- [ebp+8]=ecx=1
- eax=1
第二次:
- dword ptr [ebp+8]指向的数字是2,ecx=2
- ecx=ecx+eax=2+1=3
- [ebp+8]=ecx=3
- eax=3
第三次:
- dword ptr [ebp+8]指向的数字是3,ecx=3
- ecx=ecx+eax=3+3=6
- [ebp+8]=ecx=6
- eax=6
这是最后一回调用自身函数了,接下去的ret操作返回值就不是call accumulation的下一步了。而是返回到了main函数中call printf 函数的下一步。
而在printf函数运行机制中,eax寄存器中的值一般就是会被打印出来的值,所以最后得出结果6。
感觉递归就是 call在一层层向下嵌套,ret在一层层向上逃离的过程。