SLP(Chapter 5):函数的调用返回
复习
-
PUSH & POP
SP 指向当前栈顶最后压入的一个字节
例子:- PUSH EAX (32bits)
假设压栈前SP = 0x0012ff40,则PUSH执行后,
0x0012ff3c~3f将用于存放EAX的值,
SP=SP-4,即SP修改为 0x0012ff3c - POP AX (16bits)
假设压栈前SP = 0x0012ff40,则POP执行后,
0x0012ff40~41的值拷贝入AX
SP=SP+2,即SP修改为 0x0012ff42
Tips: SP的修改意味着以前数据理论上不能访问了,但是该字节的内容并没有被擦除。
- PUSH EAX (32bits)
-
CALL & RET
例子:- CALL (导致ESP 修改-4/-8)
PUSH IP( a near or short call) / PUSH CS PUSH IP (far call)
JMP 跳转至被调用函数 - RET (导致ESP 修改+4,近调用)
POP IP - RETF(导致ESP 修改+8,对应far call)
POP IP POP CS
注:近转移 near/short call是同一代码段内转移,只需要变IP
远转移 far call是不同代码段间(调用另一个文件的代码段)转移,除了IP,还要改变CS
- CALL (导致ESP 修改-4/-8)
1 参数和局部变量
作用域 Scope:
变量具有作用域,即同一变量名称可能引用不同的值,具体**哪个取决于其使用位置
example:局部变量是定义它的块的私有变量;全局变量可以由其他块访问
mainlocal?
编译器如何实现作用域
编译器通过操作不同作用域中的不同内存地址来实现范围,具有相同名称的变量由编译器映射到不同的地址。
硬件不知道作用域
参数
- 形参 formal parameter:编译器按照被调用函数局部参数处理形参
- 实参 actual parameter:
编译器实现
每一次调用,距离0X58
- 问题 Problem:递归调用中,需要即使版本的变量,但无法在编译时决定如何分配
- 解决 Solution:
- 全局变量静态分配,即绝对地址
- 局部变量动态分配,因为不知道需要多少个,即相对地址
- where:栈帧 stack frame(在谈到内存分配,称之为栈帧)
- how:**记录activation record(与栈帧实际上是一个东西,在谈到如何运行时,习惯称之为**记录)
- ebp & esp
2 **记录activation record / 栈帧 stack frame
2.1 基础
局部变量和实际参数:
- 动态分配
- 减慢程序的执行速度
具体实现:为了最大程度地降低成本,编译器计算函数局部变量所需的总空间量,并将该空间分配到一个块中(**记录active record)
硬件支持
- 硬件编译器和硬件在内部维护两个重要值,这些值有助于以简单而优雅的方式分隔和操作**记录
堆栈指针 R --esp
帧指针 R --ebp - **记录以非常简单的方式组织,以适应硬件
规则:只能访问堆栈顶部的**记录
- 只有当该函数调用的所有子函数返回,该函数才可能返回,得到控制权。
- 变量的scope:我们只能访问当前正在执行的函数的局部变量(和全局变量—不在stack上)。
**记录 / 栈帧的定义:
为每个函数调用分配的内存块
生命周期:从函数调用到返回
2.2 CALL / RET 过程
Call(1. push IP; 2. Jmp)
调用函数时,编译器和硬件:
- 调用方 :保存上下文 context
- 被调用方:构造自己的栈帧 stack frame
Return
汇编代码例子:
3 函数的调用约定
why?
- C 级函数调用
参数
局部变量
返回值 - 汇编/机器级别的功能调用
call和 ret 指令
What?
调用约定 描述了 被调用代码的接口:
- 参数的分配顺序
- 放置参数的位置(推送在堆栈上或放置在寄存器中)
- 函数可以使用哪些寄存器
eax 的返回值 - 调用方还是被调用方负责在返回时展开堆栈
类型:
- _cdecl(上面的例子)
C Declaration 表示C语言默认的函数调用方法- 参数按"从右向左"的顺序推送到堆栈
- 调用方负责清除堆栈上的参数,手动清栈
- 被调用函数不要求参数,所有参数数量不对,不会有编译阶段错误
- _stdcall / WINAPI
- 参数按"从右向左"的顺序推送到堆栈
- 被调用方负责清除堆栈上的参数
- Pascal
- 参数按"从左向右"的顺序推送到堆栈
- 被调用方负责清除堆栈上的参数
完整的函数调用