SLP(Chapter 5):函数的调用返回

复习

  1. PUSH & POP
    SP 指向当前栈顶最后压入的一个字节
    例子:

    1. PUSH EAX (32bits)
      假设压栈前SP = 0x0012ff40,则PUSH执行后,
      0x0012ff3c~3f将用于存放EAX的值,
      SP=SP-4,即SP修改为 0x0012ff3c
    2. POP AX (16bits)
      假设压栈前SP = 0x0012ff40,则POP执行后,
      0x0012ff40~41的值拷贝入AX
      SP=SP+2,即SP修改为 0x0012ff42
      Tips: SP的修改意味着以前数据理论上不能访问了,但是该字节的内容并没有被擦除。
  2. CALL & RET
    例子:

    1. CALL (导致ESP 修改-4/-8)
      PUSH IP( a near or short call) / PUSH CS PUSH IP (far call)
      JMP 跳转至被调用函数
    2. RET (导致ESP 修改+4,近调用)
      POP IP
    3. RETF(导致ESP 修改+8,对应far call)
      POP IP POP CS
      注:近转移 near/short call是同一代码段内转移,只需要变IP
      远转移 far call是不同代码段间(调用另一个文件的代码段)转移,除了IP,还要改变CS
      SLP(Chapter 5):函数的调用返回

1 参数和局部变量

作用域 Scope:

变量具有作用域,即同一变量名称可能引用不同的值,具体**哪个取决于其使用位置
example:局部变量是定义它的块的私有变量;全局变量可以由其他块访问
mainlocal?

编译器如何实现作用域

编译器通过操作不同作用域中的不同内存地址来实现范围,具有相同名称的变量由编译器映射到不同的地址。
硬件不知道作用域

参数

  1. 形参 formal parameter:编译器按照被调用函数局部参数处理形参
  2. 实参 actual parameter:

编译器实现

SLP(Chapter 5):函数的调用返回
每一次调用,距离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 过程

SLP(Chapter 5):函数的调用返回

Call(1. push IP; 2. Jmp)

调用函数时,编译器和硬件:

  • 调用方 :保存上下文 context
    SLP(Chapter 5):函数的调用返回
  • 被调用方:构造自己的栈帧 stack frame
    SLP(Chapter 5):函数的调用返回

Return

SLP(Chapter 5):函数的调用返回
SLP(Chapter 5):函数的调用返回

汇编代码例子:

SLP(Chapter 5):函数的调用返回
SLP(Chapter 5):函数的调用返回
SLP(Chapter 5):函数的调用返回

3 函数的调用约定

why?

  • C 级函数调用
    参数
    局部变量
    返回值
  • 汇编/机器级别的功能调用
    call和 ret 指令

What?
调用约定 描述了 被调用代码的接口:

  • 参数的分配顺序
  • 放置参数的位置(推送在堆栈上或放置在寄存器中)
  • 函数可以使用哪些寄存器
    eax 的返回值
  • 调用方还是被调用方负责在返回时展开堆栈

类型:

  1. _cdecl(上面的例子)
    C Declaration 表示C语言默认的函数调用方法
    • 参数按"从右向左"的顺序推送到堆栈
    • 调用方负责清除堆栈上的参数,手动清栈
    • 被调用函数不要求参数,所有参数数量不对,不会有编译阶段错误
  2. _stdcall / WINAPI
    • 参数按"从右向左"的顺序推送到堆栈
    • 被调用方负责清除堆栈上的参数
  3. Pascal
    • 参数按"从左向右"的顺序推送到堆栈
    • 被调用方负责清除堆栈上的参数

完整的函数调用