文章目录
- 1.Call和ret指令
- 2. 如何访问栈帧里面的数据
- 为什么栈底放在上面,栈顶放在下面
- X86中的寄存器
- EBP、ESP寄存器
- push 、pop 指令
- mov 指令
- 总结如何访问栈帧
- 3. 如何切换栈帧
- 函数调用时
- 函数返回时
- 4. 完整的函数调用过程
- 1. 一个函数的栈帧内包含哪些内容
- 2. 汇编代码实战
- 3. 内容拓展
- 5. 大总结
1.Call和ret指令
除了main函数外,在栈底还会保存一些和硬件以及系统相关的其他的一些信息
调用函数的时候就压栈,
函数执行完成,return的时候就出栈
当前执行的函数永远都是在栈顶的位置!
如何保证函数调用的时候能够过得去,还能回得来呢?
当CPU正在执行Call指令的时候,
IP寄存器应该是指向下一条mov指令
那么执行call指令的第一个作用就是把当前IP寄存器的值【mov指令的地址】给他压栈保存,把他压到栈顶的位置。
接下来会让IP寄存器指向新的位置【add函数的第一条指令】,同时add函数压栈
执行完add函数之后【return之前的指令】,add函数就会出栈,
接下来CPU执行add函数的return指令。
return指令的作用就是从函数函数栈中的顶部找到IP旧值,并且把他写到IP寄存器当中,这样的话,IP寄存器的值就会回到call指令的后一条指令
2. 如何访问栈帧里面的数据
为什么栈底放在上面,栈顶放在下面
对应一个32的操作系统来说,系统会为每一个进程分配4GB的虚拟地址空间,也就是虚拟内存,地址的范围从0x0000 0000~0xFFFF FFFF,一共4GB,
高地址的1GB是操作系统的内核区
低地址的3GB可以由用户进程使用
那我们之前提到的函数调用栈的栈底在0xC000 0000这个位置,而栈顶是在地址变小的这个方向,
因此大多数情况下,会把栈底画在上面(高地址),栈顶画在下面(低地址)
在这3GB的用户区当中,除了用户栈(又名函数调用栈)之外,还会被划分成其他的区域,用于存储不同的数据
X86中的寄存器
在X86的CPU里面,有这样一些常见的寄存器
其中EBP和ESP和我们的函数调用栈息息相关
注意在一个CPU内部,只有一个EBP和一个ESP
EBP、ESP寄存器
push 、pop 指令
mov 指令
总结如何访问栈帧
3. 如何切换栈帧
CPU内有有EBP和ESP两个寄存器
这两个寄存器标记了当前正在执行的函数他的栈帧的范围
当发生函数调用的时候,需要修改EBP和ESP的值
让他们指向新的被调用函数的底部和顶部
而且当被调用的函数执行完毕之后,
我们还需要把EBP和ESP重新让他指回上一层函数的栈帧
函数调用时
当CPU执行call这条指令的时候
IP寄存器应该指向下一条指令【mov】,
这时候需要push IP,把IP的值压入栈顶,也就是保存外层函数的下一条应该执行指令的地址。
同时,IP指向add函数的第一条指令
发现,add函数的第一条指令是push ebp,此时的EBP指向的还是上一层函数的栈底,这条指令会把上一层函数的栈底压栈。方便之后返回的时候重新指向上层函数的栈底
add函数的第二条指令是mov ebp,esp。意思是把ESP栈顶的值复制到EBP当中,换句话说就是让EBP指向当前这个函数【add函数】的基地址【0XA00F 0010】
我们发现每一个函数的栈帧,他的底部一定是存储了它上一层函数的基地址【add函数他的栈帧底部存储了caller函数的基地址0xA00F 0030】
这样做的好处就是,当一层函数执行完毕,要返回到上一层函数的之前,我们总能在当前函数的栈帧底部找到上一层函数的基地址,这样我们就可以恢复EBP寄存器的值。
接下来我们可以给这个新的函数栈帧进行一系列逻辑操作
函数返回时
- 把EBP的值复制一份给ESP。换句话说就是让ESP和EBP都指向当前栈帧的底部
- pop ebp,把当前ESP所指的栈顶元素出栈,写入EBP中,也就是让EBP的值重新指回了栈帧底部0xA00F 0030这个位置。同时ESP+4,指向0xA00F 0014这个位置
return指令,让程序的执行流回到Call指令的后面这条指令,继续往后执行
4. 完整的函数调用过程
1. 一个函数的栈帧内包含哪些内容
2. 汇编代码实战
在调用含参的方法之前,要把外层函数的两个参数放入外层函数栈帧的顶部位置
注意:mov指令不支持两个操作数都来自主存,所以要把temp2的值传到距离栈顶的倒数第二个位置,必须先把temp2放入当一个寄存器,再从寄存器中将数据写入到esp+4
执行call add函数,IP指向下一条指令【mov】,这时先将IP的值压入栈中,然后IP指向add函数的第一条指令。
add函数中如何访问到上层函数的两个参数?
caller函数中,已经把两个参数值存入到靠近栈顶的两个位置,访问这两个参数只需要用【ebp+8】访问第一个参数,【ebp+12】访问第二个参数
caller函数给上一层函数返回值的时候,把这个返回值写入到eax寄存器中,这样,上一层函数只需要从eax寄存器中取这个值就可以获得函数的返回结果了。
假设在发生函数调用的时候,这个调用者他有一些运算结果被存在了一些寄存器当中,比如edx、ecx、eax。那么当函数调用发生的时候,被调用者也有可能使用到这几个寄存器,那就有可能吧调用者的这几个中间结果给覆盖掉,就有可能导致数据的丢失。那如何解决这个问题呢?
办法:如果有必要的话,在发起函数调用之前,我把我需要的这些寄存器值压栈保存,在函数调用返回之后,再把这些寄存器值从栈恢复到寄存器,这样就可以保证寄存器中的数据不会丢失
3. 内容拓展