C|從流程圖和彙編代碼清晰看透遞歸函數代碼的執行流程

先看下面的簡單實例:

<code>#include <stdio.h> 


int counts(int n)
{
\tprintf("call: %d\\n",n);
\tif(n==0)
\t\treturn n;
\telse
\t{
\t\tcounts(n-1);
\t\tprintf("back: %d\\n",n);
\t\treturn n;
\t}
}

void main()
{
\tprintf("%d",counts(3));
\tgetchar();
}
/<stdio.h>/<code>

運行結果:

<code>call: 3
call: 2
call: 1
call: 0
back: 1
back: 2
back: 3/<code>

關鍵是明白其代碼調用、迴歸的流程,以及其參數值是如何迭代的。

基本的流程如下:


C|從流程圖和彙編代碼清晰看透遞歸函數代碼的執行流程

下面從彙編的層面分析:

首先是主函數調用遞歸函數開始:

<code>18:       printf("%d",counts(3));
0040D7B8 push 3
0040D7BA call @ILT+0(counts) (00401005)
0040D7BF add esp,4/<code>

call指令會完成兩個動作,先是將call指令的下一條指令的地址0040D7BF壓棧(push),以便返回;然後是執行一個jmp指令:

<code>00401005   jmp         counts (00401020)/<code>

跳轉到函數開始處00401020,先整體看一下函數的彙編代碼:

<code>3:    int counts(int n)
4: {
00401020 push ebp
00401021 mov ebp,esp
00401023 sub esp,40h
00401026 push ebx
00401027 push esi
00401028 push edi
00401029 lea edi,[ebp-40h]
0040102C mov ecx,10h
00401031 mov eax,0CCCCCCCCh
00401036 rep stos dword ptr [edi]
5: printf("call: %d\\n",n);
00401038 mov eax,dword ptr [ebp+8]
0040103B push eax
0040103C push offset string "call: %d\\n" (00422028)
00401041 call printf (00401110)
00401046 add esp,8
6: if(n==0)
00401049 cmp dword ptr [ebp+8],0
0040104D jne counts+34h (00401054)
7: return n;
0040104F mov eax,dword ptr [ebp+8]
00401052 jmp counts+57h (00401077)

8: else
9: {
10: counts(n-1);
00401054 mov ecx,dword ptr [ebp+8]
00401057 sub ecx,1
0040105A push ecx
0040105B call @ILT+0(counts) (00401005)
00401060 add esp,4
11: printf("back: %d\\n",n);
00401063 mov edx,dword ptr [ebp+8]
00401066 push edx
00401067 push offset string "back: %d\\n" (0042201c)
0040106C call printf (00401110)
00401071 add esp,8
12: return n;
00401074 mov eax,dword ptr [ebp+8]
13: }
14: }
00401077 pop edi
00401078 pop esi
00401079 pop ebx
0040107A add esp,40h
0040107D cmp ebp,esp
0040107F call __chkesp (00401190)
00401084 mov esp,ebp
00401086 pop ebp
00401087 ret
/<code>

然後分開來分析:

<code>3:    int counts(int n)
4: {
00401020 push ebp
00401021 mov ebp,esp
00401023 sub esp,40h
00401026 push ebx
00401027 push esi
00401028 push edi
00401029 lea edi,[ebp-40h]
0040102C mov ecx,10h
00401031 mov eax,0CCCCCCCCh
00401036 rep stos dword ptr [edi]/<code>

進入函數後,先壓棧底指針,抬高棧底指針,提升棧頂指針,壓寄存器,初始化這一段棧幀空間(debug模式)。

通常是一段這樣的操作:


C|從流程圖和彙編代碼清晰看透遞歸函數代碼的執行流程

繼續下面的彙編代碼:

<code>5:        printf("call: %d\\n",n);
00401038 mov eax,dword ptr [ebp+8]
0040103B push eax
0040103C push offset string "call: %d\\n" (00422028)
00401041 call printf (00401110)
00401046 add esp,8
6: if(n==0)
00401049 cmp dword ptr [ebp+8],0
0040104D jne counts+34h (00401054)
7: return n;
0040104F mov eax,dword ptr [ebp+8]
00401052 jmp counts+57h (00401077)
8: else
9: {
10: counts(n-1);
00401054 mov ecx,dword ptr [ebp+8]
00401057 sub ecx,1
0040105A push ecx
0040105B call @ILT+0(counts) (00401005)
00401060 add esp,4/<code>

上面的cmp,在CPU內部是做一個減法,修改標誌寄存器的值,根據標誌寄存器的值形成跳轉。

參數n減1後壓棧。

遞歸調用函數,call指令會將地址00401060壓棧,跳轉到00401005。

循環上面的操作,繼續抬高堆棧,直到參數值n==0:

<code>00401049   cmp         dword ptr [ebp+8],0
0040104D jne counts+34h (00401054)
7: return n;
0040104F mov eax,dword ptr [ebp+8]
00401052 jmp counts+57h (00401077)/<code>

返回:

<code>00401077   pop         edi
00401078 pop esi
00401079 pop ebx
0040107A add esp,40h
0040107D cmp ebp,esp
0040107F call __chkesp (00401190)
00401084 mov esp,ebp
00401086 pop ebp
00401087 ret/<code>

C代碼的返回,在彙編層面還有一些代碼要生成並被執行,就是堆棧平衡,

然後是ret,與call相對應,ret指令也是執行兩個動作,pop出在call時壓進棧的返回地址,執行jmp命令返回:

<code>00401060   add         esp,4
11: printf("back: %d\\n",n);
00401063 mov edx,dword ptr [ebp+8]
00401066 push edx
00401067 push offset string "back: %d\\n" (0042201c)
0040106C call printf (00401110)
00401071 add esp,8
12: return n;
00401074 mov eax,dword ptr [ebp+8]
13: }
14: }
00401077 pop edi
00401078 pop esi
00401079 pop ebx
0040107A add esp,40h
0040107D cmp ebp,esp
0040107F call __chkesp (00401190)
00401084 mov esp,ebp
00401086 pop ebp
00401087 ret
/<code>

引用遞歸調用的函數參數,將返回值壓到寄存器eax,堆棧平衡,再ret返回。

循環上面的操作3次,ret到最首先調用的下一個語句的地址0040D7BF:

<code>0040D7BF   add         esp,4
0040D7C2 push eax
0040D7C3 push offset string "%d" (00422034)
0040D7C8 call printf (00401110)
0040D7CD add esp,8
19: getchar();/<code>

到此,遞歸調用完成。

存放堆棧指針的寄存器ebp、esp的值迴歸到原處。

-End-


分享到:


相關文章: