CSAPP (一): 栈帧, 计算机的世界观

概述

任一时刻, 计算机都在执行一个程序 (进程), 这个正在执行的状态可以用以下内容唯一确定:

  • 程序的代码
  • 当前寄存器组的状态
  • 堆和栈的数据

知道了以上三者, 可以唯一确定现在计算机正在执行哪个程序, 执行的是该程序的哪一个函数的哪一.

由于程序的代码一般是不变的, 所以一个程序的多个实例如果同时执行(同一程序的多个进程), 那么它们共享同一个程序代码, 但是每一个进程都有自己各自的寄存器组状态和堆栈数据. 同理, 同一进程的多个线程拥有各自的寄存器组状态和栈, 共享同一个进程的堆栈数据.

本专题以 IA-32 架构为语境.

寄存器

名称用法
%eax随便用
%ebx随便用, 使用完要恢复初值
%ecx随便用
%edx随便用
%esi随便用, 使用完要恢复初值
%edi随便用, 使用完要恢复初值
%esp指向栈顶, 表示当前栈帧的上界
%ebp表示当前栈帧的下界, 即函数的作用域
%eip表示下一个要执行代码的地址

解释

  • %ebx, %esi 和 %edi 要在使用前 push, 使用后 pop, 这是惯例.
  • %esp 表示栈顶, 是当前调用函数的最深处, 所谓的"越界"实际上就是访问了比栈顶更往上的内存 (即地址比 %esp 更小的内存).
  • %ebp 表示当前栈帧的起点, 这个值很重要, 所有局部变量和函数的参数的位置都可以通过 %ebp 的值找到.下文会讲.

栈帧

栈帧即为栈 (内存) 的某一部分, 这一部分唯一对应一个函数的作用域, 它里面的数据是该函数的所有局部变量和要调用下一个函数时传递的参数以及一些需要临时保存的数据. 每调用一个函数, 栈上便会创建一个该函数对应的栈帧.

在学C语言的时候, 看到过这句话:

函数局部变量的生命周期从执行函数开始, 到函数返回结束

用底层的语言来解释就是: 执行函数的时候创建栈帧 (里面包含函数的局部变量), 函数返回时释放栈帧.
由于 (%esp, %ebp) 可以唯一确定一个栈帧, 而相邻栈帧的上下界可以互相推导出来, 所以每调用一个函数, 都会先保存之前栈帧的下界 %ebp, 然后再给当前栈帧的上界 %esp 赋新的值.

下面来详细解释上述的这几段话.

%esp 和 %ebp 定义了一个栈帧

将以下C语言代码

void foo(int p1, int p2) {
	int v1 = 7;
	int v2 = p1;
	int v3 = 1 + p2;
}

用gcc和objdump转化为汇编语言

push %ebp
mov %esp, %ebp
sub $0x10, %esp
movl $0x7, -0xc(%ebp)
mov 0x8(%ebp), %eax
mov %eax, -0x8(%ebp)
mov 0xc(%ebp), %eax
add $0x1, %eax
mov %eax, -0x4(%ebp)

一行一行来解释:

  1. 将%ebp压栈, 即保存调用者的栈帧的下界 ( 栈帧的上界是调用该语句前的%esp, 因为压栈导致 %esp 的值 -4 )
  2. 将 %esp 的值赋给 %ebp, 即当前栈帧的下界是上一个栈帧的上界 + 4. ( 因为栈帧是连续存储的, 所以只需保存上一个栈帧的起点即可, 上一个栈帧的终点可以由当前栈帧的起点计算出来. )
  3. 将 %esp - 0x10, 即为当前栈帧分配空间, 我们发现当前函数实际上只有3个临时变量, 需要的空间是 12 byte 小于分配的 16 byte, 这是因为要进行对齐.
  4. 将立即数7赋值给 %ebp - 12 的位置
  5. 将 %ebp + 8 位置的值赋给中间变量
  6. 将中间变量赋值给 %ebp - 8的位置
  7. 将 %ebp + 12 位置的值赋值给中间变量
  8. 中间变量 + 1
  9. 将中间变量的值赋值给 %ebp - 4 的位置

前三行留给下文具体解释, 我们只关注 4 ~ 9 行, 很明显可以看到 函数参数和局部变量的位置可以由 %ebp 来计算得到, 如下图所示.

%ebp的作用
由此可见, 通过 %ebp 可以得到当前函数的所有变量以及参数的位置, 所以 %ebp 就相当于一个函数的基地址. 而下一个栈帧的 %ebp 是由 %esp 得到的, 所以 %esp 的值即为当前栈帧和下一个栈帧的分界线. 因此%esp 和 %ebp 定义了一个栈帧.

总结

过程式语言如 C 语言, 从 main 函数开始, 到 main 函数返回结束, 期间调用了其他函数. 因此在实际运行中, 内存中唯一在变化的是栈(先不考虑堆), 栈的变化就像是音乐播放器的声音波形一样上上下下.

  • 当调用一个函数的时候, 栈增长了一部分作为该函数的栈帧, 里面包含了该函数的所有局部变量;
  • 当一个函数又调用另一个有参数的函数前, 按照参数的逆序将参数 push 进栈中.
  • 当一个函数返回时, 栈帧释放, 栈减小. 恢复原来的 %ebp 和 %esp, 并将 %eip 设置为函数返回后下一条应执行指令的地址.
  • ( 除了全局作用域 ) 程序执行的每一步都是以一个函数为当前的context, 他所需要的所有变量都可以通过 %ebp 或者 %esp 直接或者间接地找到.

就是这样.

©️2020 CSDN 皮肤主题: 像素格子 设计师:CSDN官方博客 返回首页