函数调用栈
不同语言之间的转换
高级程序语言经过编译,变成汇编代码,汇编代码经过汇编器和连接器变成可执行的机器代码。汇编代码是人类可读的代码。机器代码是01序列。
在程序执行前,数据和指令事先存放在存储器中,每条指令和每个数据都有地址。程序启动后,计算机取指令执行。执行过程中,指令和数据从存储器取到CPU,存放到CPU寄存器中,CPU内的寄存器与控制器与ALU进行数据传送与运算,将运算结果写入内存中(存储器)。
寄存器
IA-32体系中,有8个通用寄存器GPR,2个专用寄存器EIP和EFLAGs和六个段寄存器,间接给出段基址。
指令
计算机的指令有微指令、机器指令和伪指令之分。汇编指令是机器指令的符号表示,以帮助记忆,可以称之为助记符。机器指令和汇编指令是一一对应的,都是机器级指令。
机器指令对源操作数进行操作,操作结果储存到目的操作数中。操作码:操作性质;立即数:操作数在指令中给出,这个数叫做立即数。找到操作数或操作数的地址这个过程叫寻址,操作数在不同的位置,有不同的寻址方式。
指令可以分为传送指令,如mov,push,pop,leal,in,out;定点算术运算指令,如add,sub,int,dec,neg,cmp;浮点运算指令等等。
函数调用过程
一段c语言代码
1 | int add(int x,int y){ |
过程描述
存放参数t1,t2;调出add执行,取出参数t1,t2,执行运算,保存返回结果,返回到main中。过程调用执行步骤(p为调用者,Q为被调用者)
关于Q过程中的保存P的现场和恢复P的现场
为什么要压入返回地址啊?返回地址是指被调用函数的下一条语句。ret指令会将返回地址送到eip寄存器,即指令指针。
保存现场即保存寄存器,以便被调用者返回前恢复寄存器。
根据IA-32的寄存器使用约定:
调用者保存寄存器:eax,edx,ecx
如果调用函数P调用完Q返回后,还需用到eax寄存器,则需要P在调用Q前保存。*一般而言P调用Q之后不需要用到eax寄存器,所以一般会将操作数优先存在eax,edx,ecx这类寄存器,以减少准备阶段和结束阶段的开销。被调用者保存寄存器:ebx,esi,edi
如果被调用者Q需要用到ebx,则Q需要保存到栈中再使用。ebp:帧指针寄存器,指向当前栈帧底部,高地址 esp:栈指针寄存器,指向当前栈帧顶部,低地址。如果以向上为高地址的话,栈就是个倒置的桶,栈帧底部abp描述的就是杯底,底部为高地址,没毛病。
栈和栈帧的变化
相应的汇编指令
1 | int add(int x,int y){ |
汇编代码 | 操作 |
---|---|
pushl %ebp | 将旧ebp地址压栈(以便回到原栈底),esp地址-4,指向下一个存储单元 |
movl %esp,%ebp | 将esp的值赋给ebp,ebp指向了当前栈帧的底部 |
subl $24,%esp | 把esp减去了24,开辟内存空间 |
movl $125,-12(%ebp) | 125送到ebp-12的位置 |
movl $80,-8(%ebp) | 80这个立即数送到ebp-8的位置 |
movl -8(%ebp),%eax | |
movl %eax,4(%esp) | 把ebp-8的数送到esp+4的位置,在对t2参数进行赋值 |
movl -12(%ebp),%eax | |
movl %eax,(%esp) | 同理,在进行t1参数的赋值 |
call add | 转入add函数体执行,同理第一二条指令是push ebp;mov esp,ebp形成add栈帧的底。return返回参数总是在eax中。call指令总会把下一条指令(movl %eax,-4(%ebp))压入到栈中。add最后一条指令是ret指令,ret指令会将返回地址取出送到eip寄存器(指令指针)中。 |
movl %eax,-4(%ebp) | 将eax的值(即return的和)传送至ebp-4的位置 |
movl -4(%ebp),%eax | 把返回值sum再放入eax寄存器中 |
leave | 退栈,将ebp的内容传送到esp,将esp又指向了ebp;再执行pop指令,把esp指向的内容弹出到ebp内,恢复ebp的内容(ebp回到了p过程的栈底)。esp指向上一个单元,该单元存放的是返回地址。 |
ret | return指令又可以返回到p(调用函数)中执行 |
准备参数入口,将值再复制了一遍是为什么?
假设回调函数为swap(&t1,&t2)
,要改变局部变量的之前是不是得先存起来,不然令t1=t2
,t1原来的值消失了怎么办,简单比喻一下。