为了提供一个具体的示例,说明编译器如何管理堆栈以及如何访问堆栈上的值,我们可以看一下视觉描述,以及GCC
在i386作为目标体系结构的Linux环境中由Linux 生成的代码。
1.堆叠框架
如您所知,栈是函数或过程使用的正在运行的进程的地址空间中的位置,在某种意义上来说,栈是在栈上为本地声明的变量以及传递给函数的参数分配的空间(在函数之外声明的变量(即全局变量)的空间分配在虚拟内存的不同区域中。为函数的所有数据分配的空间称为堆栈帧。这是多个堆栈框架的可视化描述(来自“ 计算机系统:程序员的视角”):
2.堆栈框架管理和可变位置
为了使在特定堆栈帧中写入堆栈的值由编译器管理并由程序读取,必须有某种方法来计算这些值的位置并检索其内存地址。CPU中称为堆栈指针和基本指针的寄存器可以帮助实现这一点。
按照ebp
惯例,基本指针包含堆栈底部或底部的内存地址。可以使用基本指针中的地址作为参考来计算堆栈帧中所有值的位置。如上图所示:例如,它%ebp + 4
是存储在基本指针加4中的内存地址。
3.编译器生成的代码
但是我没有得到的是应用程序如何读取堆栈上的变量-如果我声明x并将其分配为整数,例如x = 3,并在堆栈上保留存储,然后将其值3存储在那里,然后在同一函数中我声明并分配y,例如4,然后在另一个表达式中使用x(例如z = 5 + x),程序如何读取x以便在以下情况下计算z它在堆栈上的y以下吗?
让我们使用一个用C编写的简单示例程序来查看其工作原理:
int main(void)
{
int x = 3;
int y = 4;
int z = 5 + x;
return 0;
}
让我们检查一下GCC为该C源代码生成的汇编文本(为清晰起见,我对其进行了一些清理):
main:
pushl %ebp # save previous frame's base address on stack
movl %esp, %ebp # use current address of stack pointer as new frame base address
subl $16, %esp # allocate 16 bytes of space on stack for function data
movl $3, -12(%ebp) # variable x at address %ebp - 12
movl $4, -8(%ebp) # variable y at address %ebp - 8
movl -12(%ebp), %eax # write x to register %eax
addl $5, %eax # x + 5 = 9
movl %eax, -4(%ebp) # write 9 to address %ebp - 4 - this is z
movl $0, %eax
leave
我们观察到的是,变量x,y和z位于地址%ebp - 12
,%ebp -8
并%ebp - 4
分别。换句话说,使用保存在CPU寄存器中的存储器地址来计算变量在堆栈帧中的位置。main()
%ebp
4.超出堆栈指针的内存中的数据超出范围
我显然缺少了一些东西。是不是堆栈上的位置仅与变量的生存期/范围有关,并且整个堆栈实际上始终可以被程序访问?如果是这样,是否意味着还有其他索引仅保留堆栈上变量的地址以允许检索值?但是后来我认为堆栈的全部目的是将值与变量地址存储在同一位置?
堆栈是虚拟内存中的一个区域,其使用由编译器管理。编译器以这样的方式生成代码:绝不会引用超出堆栈指针的值(超出堆栈顶部的值)。可以说,当调用一个函数时,堆栈指针的位置会发生变化,从而在堆栈上创建被认为不是“超出范围”的空间。
随着函数的调用和返回,堆栈指针递减并递增。超出范围后,写入堆栈的数据不会消失,但是编译器不会生成引用该数据的指令,因为编译器无法使用%ebp
或来计算这些数据的地址%esp
。
5.总结
可以由CPU直接执行的代码由编译器生成。编译器管理堆栈,功能和CPU寄存器的堆栈帧。GCC用来跟踪打算在i386架构上执行的代码中堆栈帧中变量位置的一种策略是使用堆栈帧基址指针中的内存地址%ebp
作为参考,并将变量值写入堆栈帧中的位置相对于中地址的偏移量%ebp
。