事实上堆(heap)和栈(stack)是两种不同的数据结构,我不太明白为什么国内的教材要称呼其为堆栈,因此以下的内容都称其为栈。我们可以尝试通过一个简单的函数调用来看看局部变量是如何在汇编代码中存储的。以下的结果是我在M1 Macbook Pro上用x86_64-elf-gcc编译得到的结果。C代码如下int add(int x, int y){ int z = 1; return x + y + z;}int main(){ add(1, 2); return 0;}
编译语句为 x86_64-elf-gcc -O0 -o main.s -S main.c
生成的汇编代码我选取了add函数并去掉了无关的注释,如下 add: pushq %rbp movq %rsp, %rbp movl %edi, -4(%rbp) movl %esi, -8(%rbp) movl -4(%rbp), %edx movl -8(%rbp), %eax addl %edx, %eax popq %rbp ret
创建局部变量的过程在于movl %edi, -4(%rbp) 和movl %esi, -8(%rbp)。在前两步中函数保存了并更新rbp值 ,而传递的参数初始保存在edi和esi寄存器中。因此前两部执行完后栈分布情况大致如下----------------------------------------the value of previous rbp <- rbp----------------------------------------
接下来通过两条movl指令,函数向相对rbp的指定偏移地址写入参数。注意栈是自顶向下增长的。情况如下 ----------------------------------------the value of previous rbp <- rbp----------------------------------------argument0----------------------------------------argument1----------------------------------------
在这之后传入的变量就被存储在栈上了,也就是说我们可以通过相对rbp的偏移量去访问到这两个参数。例如,在接下来的加法运算中,函数首先就通过两条加法指令将数据从对应的偏移量地址读取到寄存器中,再执行加法操作。最后,当函数执行完成时,函数会进行退栈。注意到最后一步 popq %rbp ,事实上是恢复了原始的rbp值,这意味着在函数返回之后我们无法再通过原先相对rbp的偏移量再去访问到原先局部变量的值,函数调用栈事实上被销毁了。当然,在更复杂的例子中rsp指针也应当被移动,并在返回时被重新对齐。可能是我们的C函数过于简单,尽管关闭了优化,编译器依然没有移动它。 如果是在函数中自己创建局部变量,道理也同传入参数是一样的,始终是将数据存储在栈上再通过相对rbp或者rsp的偏移量去找到相应的数据。
|