使用的工具
- VC6.0
- Excel
编写一个简单的程序
#include <stdio.h> //头文件
int plus1(int x,int y) //定义函数plus1,参数x,y
{
return x+y; //返回x+y的值
}
int plus(int x,int y,int z) //定义函数plus,参数x,y,z
{
int m = plus1(x,y); //定义局部变量m的值为调用函数plush1的值
return m+z; //返回m+z的值
}
void main() //程序入口
{
int r; //定义局部变量r
r = plus(1,2,3); //r的值为传参后函数plus的值
printf("%d\n",r); //以十进制输出计算结果
return; //程序结束
}
下断点开始分析
第一次函数调用
首先压入三个参数,然后CALL执行时把下一行地址压入栈顶,也就是所谓的返回地址。
push 3
push 2
push 1
call 0040100a
它在堆栈中是这样表示的:
接着push了个EBP,这里也就是把原EBP的值存到了堆栈中,随后把ESP的值赋给EBP,这么做可以理解为提升栈底,我们在堆栈中记录一下:
push ebp
mov ebp,esp
接着给esp的值减44,这一步是为了提升堆栈,给程序运行制造出缓冲区,接着保存了ebx,esi,edi这三个寄存器的值,这样可以理解为拍了个快照。
sub esp,44
push ebx
push esi
push edi
画一下堆栈图:
接下来的这里行指令是为了填充缓冲区,全部填充为CC指令,
lea edi,[ebp-44]
mov ecx,11
mov eax,CCCCCCCC
rep stos dword ptr [edi]
到这里整个函数的堆栈就成型了:
第二次函数调用
这里我们分析的是这行代码:
int m = plus1(x,y);
接着这里把函数plus()的前两个参数(x,y)压入了栈顶,这里x,y也就是我们一开始压入的参数1,2,这里新的一轮堆栈操作开始了。
mov eax,dword ptr [ebp+C]
push eax
mov ecx,dword ptr [ebp+8]
push ecx
call 00401005
画一下堆栈图:
到这里我们就会发现,其实跟之前是一样的,传完参数之后准备为函数的执行腾出空间,从而提升堆栈创造缓冲区,填充CC指令,然后储存ebx,esi,edi这三个寄存器的原值。
push ebp
mov ebp,esp
sub esp,40
push ebx
push esi
push edi
lea edi,[ebp-40]
mov ecx,10
mov eax,CCCCCCCC
rep stos dword ptr[edi]
我们画一下堆栈图:
接下来的两行指令是计算x+y的值,并把结果返回到EAX中,这时EAX的值为3
mov eax,dword ptr [ebp+8]
add eax,dword ptr [ebp+0Ch]
这里堆栈没有变化,我们看一下程序的运行结果:
还原堆栈
接下来的三行指令是还原堆栈,把当前栈顶储存的值还原给指定的寄存器
pop ebx
pop esi
pop edi
画一下堆栈图:
接着把EBP的值赋给ESP,又是一个pop操作,然后ret返回到栈顶储存的地址,也就是第二个call的下一行地址,此时ESP储存的值应给是1。
mov esp,ebp
pop ebp
ret
画一下堆栈图:
看一下程序的运行结果:
接着跟了一条堆栈平衡的指令,这里用的方法是外平栈。
add esp+8
此时的堆栈变化:
接着把EAX的值存到了EBP-4,也就是缓冲区中。
mov dword ptr [ebp-4],eax
画一下堆栈图:
返回结果
接下来分析的是这行代码
return m+z;
到这里指令开始计算结果,并把结果返回到EAX中
mov eax,dword ptr [ebp-4]
add eax,dword ptr [ebp+10h]
此时EAX中的值应为3,我们运行一下程序看一下:
继续还原堆栈
接下来的这几条指令都是还原堆栈:
pop edi //还原EDI的值
pop esi //还原ESI的值
pop ebx //还原EBX的值
add esp,44h //减少堆栈,还原缓冲区
cmp ebp,esp //EBP-ESP并进行比较
call __chkesp (00401120) //调用__chkesp()函数检测缓冲区是否溢出
mov esp,ebp //把EBP的值赋给ESP,也是还原堆栈
pop ebp //还原EBP的值
ret //返回到第一个call的下一行地址
画一下堆栈:
运行一下程序看看结果是否一致:
然后是堆栈平衡,这里同样采用的外平栈方法,接着把EAX的值赋到EBP-4里:
add esp+8
mov ss:dword ptr[ebp-4],eax
堆栈变化:
输出结果
终于分析到printf了,画堆栈图真的好痛苦啊!接下来我们分析的代码:
printf("%d\n",r);
这里我直接在汇编指令里分析了:
mov eax,dword ptr [ebp-4] //把之前储存的值存回EAX
push eax //把EAX的值压入栈顶,ESP的值-4
push offset string "%d\n" (0042201c) //把转义字符压入栈顶
call printf (00401160) //调用printf()函数,把结果输出到控制台
add esp,8 //使用外平栈平衡堆栈
这里虽然调用了个printf()函数,但是跟我们之前画的堆栈图大同小异,这里就不分析printf这个函数了(其实我现在的水平也分析不明白)最后的堆栈样子是这样的:
总结
函数运行期间调用另外一个函数,在运行被调用函数之前,系统需要先完成3件事情:
- 将所有的实参、返回地址等信息传递给被调用函数保存
- 为被调用函数局部变量在栈上分配内存
- 将控制转移到被调用函数入口
被调用函数返回调用函数之前,系统也需要相应的完成3件事情:
- 在栈中保存被调用函数的返回值
- 释放在栈中为被调用函数分配的缓冲区等
- 依照被调用函数保存的返回地址将控制转移到调用函数