函数调用栈的过程
本文分析函数的调用过程中,栈的变化过程。
X86寄存器简介
寄存器名称 | 注释 |
---|---|
eax | 累加器,是很多加法、乘法的默认寄存器 |
ebx | 基地址,内存寻址时,存放基地址 |
ecx | 计数器,是重复前缀指令和LOOP指令的内定计数器 |
edx | 存放整数除法产生的余数 |
esi | 源索引寄存器,因为在很多字符串操作指令中,DS:ESI指向源串,而ES:EDI指向目标串 |
ebp | 基址指针,常被用作高级语言函数调用时的“帧指针”(frame pointer) |
esp | 专门用作堆栈指针,被形象地称为栈顶指针,堆栈的顶部是地址小的区域,压入堆栈的数据越多,ESP也就越来越小。 |
文章常用汇编指令的理解
0x4(%esp)
把寄存器esp中的值取出,然后加上4,得到的值作为地址,间接寻址得到需要的数据
例如,
pushl -0x4(%ecx)
该指令的含义是取出寄存器ecx的值,减去4,将得到的值作为地址,在内存找到该地址对应的值,将其压入栈中。
lea 0x4(%esp), %ecx
该指令的作用是,取出esp寄存器里的值,加上4,不再继续寻址,而是将得到值直接传递给ecx;如果是其他指令,则还需进行间接寻址,再传值。
call
首先将eip设置为本函数中call后的那条指令的地址(所有指令都有这步)。然后push %eip。(esp自减4,将eip的值存入esp指向的地址,最后子函数ebp+4出即存储了这一步保存的eip值)。然后将eip的值设置为被调用函数的第一调指令的地址。(最后cpu继续执行eip所指向地址的指令,虽然这不是call干的事)
leave
首先将esp指向ebp。然后pop %ebp.(将esp指向内存的值拷到ebp,esp自减4)。
ret
pop%eip(即先将esp指向的内存块的值拷贝到eip中,然后esp自减4)
函数栈帧结构
举例演示
简单的源代码:
#include<stdio.h>
int sum(int x, int y)
{
int tmp = 0;
tmp = x+y;
return tmp;
}
int main()
{
int a=10;
int b=20;
int ret=0;
ret=sum(a,b);
printf("ret=%d\n",ret);
return 0;
}
利用反汇编工具 objdump 将执行文件生成汇编文件,其内容如下:
......
0804840b <sum>:
804840b: 55 push %ebp // 把ebp寄存器的内容压入栈中,也就是保存栈帧寄存器
804840c: 89 e5 mov %esp,%ebp // 将esp中的内容拷贝到ebp中,也就是将帧指针指向栈指针
804840e: 83 ec 10 sub $0x10,%esp // 调整栈指针,存放局部变量(栈顶增加了4*4字节的空间)
8048411: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%ebp) // 局部变量tmp(0)的值存入栈底
8048418: 8b 55 08 mov 0x8(%ebp),%edx // 取出x的值
804841b: 8b 45 0c mov 0xc(%ebp),%eax // 取出y的值
804841e: 01 d0 add %edx,%eax // x+y的结果存入%eax寄存器中
8048420: 89 45 fc mov %eax,-0x4(%ebp) // x+y的结果存入局部变量tmp的地址处
8048423: 8b 45 fc mov -0x4(%ebp),%eax // x+y的结果存入%eax寄存器中
8048426: c9 leave
8048427: c3 ret
08048428 <main>:
8048428: 8d 4c 24 04 lea 0x4(%esp),%ecx
804842c: 83 e4 f0 and $0xfffffff0,%esp
804842f: ff 71 fc pushl -0x4(%ecx)
8048432: 55 push %ebp // 把ebp寄存器的内容压入栈中,也就是保存栈帧寄存器
8048433: 89 e5 mov %esp,%ebp // 将esp中的内容拷贝到ebp中,也就是将帧指针指向栈指针
8048435: 51 push %ecx // 保护ecx寄存器的内容
8048436: 83 ec 14 sub $0x14,%esp // 调整栈指针,存放局部变量(栈顶增加了5*4字节的空间)
8048439: c7 45 ec 0a 00 00 00 movl $0xa,-0x14(%ebp) // 局部变量a(10)的值存入栈底-20
8048440: c7 45 f0 14 00 00 00 movl $0x14,-0x10(%ebp) // 局部变量b(20)的值存入栈底-16地址处
8048447: c7 45 f4 00 00 00 00 movl $0x0,-0xc(%ebp) // 局部变量ret(0)的值存入栈底-12地址处
804844e: ff 75 f0 pushl -0x10(%ebp) // 向函数sum()的参数x传实参a(10),并压栈
8048451: ff 75 ec pushl -0x14(%ebp) // 向函数sum()的参数y传实参b(20),并压栈
8048454: e8 b2 ff ff ff call 804840b <sum> // 调用sum()函数
8048459: 83 c4 08 add $0x8,%esp // 调整栈顶位置(+8),
804845c: 89 45 f4 mov %eax,-0xc(%ebp) // 相加结果(30)存入局部变量ret(0)所在(栈底-12)地址中
804845f: 83 ec 08 sub $0x8,%esp // 调整栈顶位置(-8)
8048462: ff 75 f4 pushl -0xc(%ebp) //
8048465: 68 00 85 04 08 push $0x8048500
804846a: e8 71 fe ff ff call 80482e0 <[email protected]>
804846f: 83 c4 10 add $0x10,%esp
8048472: b8 00 00 00 00 mov $0x0,%eax
8048477: 8b 4d fc mov -0x4(%ebp),%ecx
804847a: c9 leave
804847b: 8d 61 fc lea -0x4(%ecx),%esp
804847e: c3 ret
804847f: 90 nop
下面我们通过图示的方法,展示函数调用的过程:
这里我们有几个假设条件:
- 我们假设这儿的栈实现方式是 满栈,所谓的 满栈就是在压栈的时候,地址先自加4,然后再存储数值。另外一种方式就是 空栈,而空栈就是先存储值,然后再把地址加4。
- 本文只研究 main和 sum之间的函数栈帧,对于 printf以及 main函数之前和之后的启动和结束代码不作研究;
相关文章:栈缓存溢出