本文共包含一下四个部分。
- C源代码
- 注释
- 对应汇编代码:此汇编使用”gcc -S hello.c”命令编译生成,部分删减
- 栈空间的使用过程:包括5个部分,五张图
- C源码
int sayhello(int a,int b,int c){
int aa=100;
int bb=200;
bb=a;
bb=b;
bb=c;
return aa;
}
main()
{
int a=100;
int b=10;
int c=102;
c=sayhello(a,b,c);
c=200;
}
- 汇编代码
080483ec <sayhello>:
80483ec: push %ebp //把main函数的ebp放入栈顶
80483ed: mov %esp,%ebp //ebp指向新的栈帧
80483ef: sub $0x10,%esp //为esp分配了16个字节空间
80483f2: movl $0x64,-0x8(%ebp) //栈内变量aa=200
80483f9: movl $0xc8,-0x4(%ebp) //栈内变量bb=200
8048400: mov 0x8(%ebp),%eax //ebp向上取出a=100
8048403: mov %eax,-0x4(%ebp) //赋值给bb
8048406: mov 0xc(%ebp),%eax //ebp向上取出b=10
8048409: mov %eax,-0x4(%ebp) //赋值给bb
804840c: mov 0x10(%ebp),%eax //ebp向上取值c=102
804840f: mov %eax,-0x4(%ebp) //赋值给bb
8048412: mov -0x8(%ebp),%eax //取出aa的值,存入eax
8048415: leave
8048416: ret //返回现场
08048417 <main>:
804841a: sub $0x1c,%esp //为main函数的栈分配28字节空间
804841d: movl $0x64,-0xc(%ebp) //变量a=100
8048424: movl $0xa,-0x8(%ebp) //变量b=10
804842b: movl $0x66,-0x4(%ebp) //变量c=102
8048432: mov -0x4(%ebp),%eax //把参数c放入栈顶
8048435: mov %eax,0x8(%esp)
8048439: mov -0x8(%ebp),%eax //把参数b放入栈顶
804843c: mov %eax,0x4(%esp)
8048440: mov -0xc(%ebp),%eax //把参数a放入栈顶
8048443: mov %eax,(%esp)
8048446: call 80483ec <sayhello>//调用sayhello,隐含把EIP放入栈顶
804844b: mov %eax,-0x4(%ebp) //返回值赋给c
804844e: movl $0xc8,-0x4(%ebp) //c重新赋值
8048455: leave
8048456: ret
- 栈调用过程
1.
main函数汇编代码头部分我删去了几行,先不要理会,我们需要的是一个确定的干净的简单的起点,那就从三个变量的定义和初始化开始吧,从代码中可以看出,三个变量的定义和创建使用了ebp的相对位置并且是逆序存放;
此时esp指向的位置比较有意思,从ebp到esp这段空间就是gcc为main函数分配的栈帧,也就是说main函数的运行以后只会使用这一段空间了;这一段空间的大小由两部分组成,如图解释。main函数中定义了三个局部变量12个字节,gcc为局部变量分配了16个字节;main函数调用的函数最多入参个数是3,gcc再分配12个字节,所以main函数的栈帧总共有28个字节,7个字。
2.
main函数准备调用sayhello了!
1、先参考esp的位置把入参mov栈中,没有使用PUSH,没有使用PUSH,没有使用PUSH
2、调用call指令,call指令其实隐含了一个PUSH指令,一个MOV指令。他先把当前main的EIP压入栈中,然后把sayhello的代码位置MOV给EIP。程序下一步执行就到了sayhello位置了。注意,保护现场工作尚未完成!
3.
进入sayhello函数后继续进行现场保护工作,保存main函数的ebp。之后ebp可以指向为sayhello分配的栈帧了,再为sayhello分配局部变量空间16个字节,创建两个变量。接下来就要使用入参了,入参的访问使用了当前ebp的相对寻址,不过不是向sayhello的栈帧,而是向上取main函数栈帧底部的三个参数!ok,更新完毕,准备返回。
4.
准备返回:1、返回的参数放入eax寄存器。2、执行ret。ret负责恢复现场,和call一样隐含了好几个POP和MOV。先让esp指向当前栈帧的底部ebp,然后pop出ebp回复main的栈帧,pop出EIP回复main的代码指针,此时虽然esp上面还存着sayhello的入参,但已经没用了。
5.
函数继续运行,EIP已经指向main的代码了,main开始对c进行赋值,先把eax里的数据拿出来,然后赋值给c。结束。
注释
- esp:当前栈的栈顶指针
- ebp:当前函数的栈帧基地址
- ess:栈空间的基地址
- eax:临时变量寄存器
- EIP:当前函数的代码指针
栈有很多栈帧组成,随着程序的运行,每进入新的函数都需要创建一个栈帧,函数只使用自己的栈帧,函数运行结束够要弹出栈帧。
刚进入函数时,esp会先跳一段空间,用来存储局部变量。
可以看出,栈只有在保护现场时使用push,pop,其他都是采用寻址的方式使用的。