汇编递归
这次通过简单的递归例子来分析递归的对应汇编
递归求前n项和
描述
求序列的前n项和。输入一个正整数n,输出1+2+3+4+5+6+...+n的前n项和
输入输出示例
输入
5
输出
15(1+2+3+4+5)
代码
int sum(int n){
if(n==1)return n;
else{
return n+sum(n-1);
}
}
int main(int argc, char* argv[])
{
int result=sum(5);
printf("%d\n",result);
return 0;
}
运行结果
反汇编代码
函数外部
19: int result=sum(5);
0040D768 push 5
0040D76A call @ILT+10(fib) (0040100f)
0040D76F add esp,4
0040D772 mov dword ptr [ebp-4],eax
20: printf("%d\n",result);
函数内部
8: int sum(int n){
0040D6F0 push ebp
0040D6F1 mov ebp,esp
0040D6F3 sub esp,40h
0040D6F6 push ebx
0040D6F7 push esi
0040D6F8 push edi
0040D6F9 lea edi,[ebp-40h]
0040D6FC mov ecx,10h
0040D701 mov eax,0CCCCCCCCh
0040D706 rep stos dword ptr [edi]
9: if(n==1)return n;
0040D708 cmp dword ptr [ebp+8],1
0040D70C jne sum+23h (0040d713)
0040D70E mov eax,dword ptr [ebp+8]
0040D711 jmp sum+37h (0040d727)
10: else{
11: return n+sum(n-1);
0040D713 mov eax,dword ptr [ebp+8]
0040D716 sub eax,1
0040D719 push eax
0040D71A call @ILT+10(fib) (0040100f)
0040D71F add esp,4
0040D722 mov ecx,dword ptr [ebp+8]
0040D725 add eax,ecx
12: }
13: }
0040D727 pop edi
0040D728 pop esi
0040D729 pop ebx
0040D72A add esp,40h
0040D72D cmp ebp,esp
0040D72F call __chkesp (004010e0)
0040D734 mov esp,ebp
0040D736 pop ebp
0040D737 ret
反汇编分析
函数外部
19: int result=sum(5);
0040D768 push 5
0040D76A call @ILT+10(fib) (0040100f)
0040D76F add esp,4
0040D772 mov dword ptr [ebp-4],eax
1.压入参数5
0040D768 push 5
2.调用函数
0040D76A call @ILT+10(fib) (0040100f)
3.堆栈外平衡(默认采用cdecl调用协定)
0040D76F add esp,4
4.将返回值eax赋值给result
0040D772 mov dword ptr [ebp-4],eax
函数内部
截取出关键代码
9: if(n==1)return n;
0040D708 cmp dword ptr [ebp+8],1
0040D70C jne sum+23h (0040d713)
0040D70E mov eax,dword ptr [ebp+8]
0040D711 jmp sum+37h (0040d727)
10: else{
11: return n+sum(n-1);
0040D713 mov eax,dword ptr [ebp+8]
0040D716 sub eax,1
0040D719 push eax
0040D71A call @ILT+10(fib) (0040100f)
0040D71F add esp,4
0040D722 mov ecx,dword ptr [ebp+8]
0040D725 add eax,ecx
12: }
13: }
0040D727 pop edi
1. cmp比较语句
比较参数和1,这里的[ebp+8]=参数n
0040D708 cmp dword ptr [ebp+8],1
2.jcc判断跳转语句
jne:jump not equal,不相等则跳转
0040D70C jne sum+23h (0040d713)
跳转地址:0040d713
10: else{
11: return n+sum(n-1);
0040D713 mov eax,dword ptr [ebp+8]
即跳转到else
3.将返回值赋值给eax
注意这里是前面的跳转语句没执行才能运行的,也就是前面[ebp+8]=1
这里其实就是mov eax,1
0040D70E mov eax,dword ptr [ebp+8]
4.绝对跳转语句
0040D711 jmp sum+37h (0040d727)
跳转地址:0040d727
0040D727 pop edi
跳转到要返回前的恢复现场的语句
5.将参数赋值给eax
这里属于else的代码,这里相当于eax=参数n
0040D713 mov eax,dword ptr [ebp+8]
6.将eax减1
相当于eax=eax-1,结合前面的eax=参数n,此时eax=参数n - 1
0040D716 sub eax,1
7.将eax作为参数压入堆栈
0040D719 push eax
8.再次调用函数本身,实现递归
0040D71A call @ILT+10(fib) (0040100f)
9.call执行后进行堆栈外平衡
0040D71F add esp,4
10.将参数n赋值给ecx
相当于ecx=n
0040D722 mov ecx,dword ptr [ebp+8]
11.将前面的ecx加给eax
函数返回值存储在eax中,相当于eax=eax+ecx,结合前面的赋值语句,相当于eax=n+sum(n-1)
0040D725 add eax,ecx
自写汇编实现
仅仅是分析汇编显然不够interesting(>人<;),自己用汇编安排它(ง •_•)ง
代码
#include "stdafx.h"
int sum(int n){
int address=(int)sum;
int result;
__asm{
_if:
cmp n,1 //比较参数n和1
jne _else //如果参数n不等于1则跳转到_else段
_match:
mov eax,1 //将1赋值给eax
mov result,eax //将eax赋值给result,结合上面相当于mov result,1
jmp _ret //绝对跳转到_ret段
_else:
mov eax,n //将参数n赋值给eax
dec eax //让eax自减1,相当于eax=eax-1,结合上面相当于eax=参数n-1
push eax //将eax作为参数压入堆栈
call address //调用函数自身
add esp,4 //堆栈外平衡
add eax,n //相当于eax=eax+n
mov result,eax //将eax赋值给result
_ret:
}
return result;
}
int main(int argc, char* argv[])
{
int result=sum(5);
printf("%d\n",result);
return 0;
}
代码分析
这次的自写的函数没有使用裸函数,而是直接在代码中插入汇编代码
裸函数相关可前往:逆向基础笔记九 C语言内联汇编和调用协定
代码流程如下:
1.获取函数的地址
int address=(int)sum;
因为在汇编中需要调用函数本身,所以需要函数的地址
2.声明一个变量用于存储返回值
int result;
3.声明四个代码段
- _if段:对应if(n==1)的代码段
- _match段:对应n==1后执行的代码段
- _else段:对应else的代码段
- _ret段:对应返回后的代码段,这里为空,采用了c和asm混编的写法,后面直接就是return result
代码段的详细注释已在上面给出,这里不再赘述
总结
递归函数和普通函数的反汇编其实并没用什么特别大的出入,只不过在函数内部调用的函数是自身
函数调用的优先级较其它运算更高(调用函数也可以看作是一种运算),在汇编语句中可以看到是先调用了函数然后再进行的加法
递归函数如果没有正确返回就会不断向堆栈中压入参数,就会导致所谓的递归栈溢出