TLDR 在底部。注意:这里的一切都是假设x86_64。
这里的问题是编译器实际上永远不会在函数体中使用push 或pop(序言/结语除外)。
考虑this example。
当函数开始时,在序言中的堆栈上腾出空间:
push rbp
mov rbp, rsp
sub rsp, 32
这会为main 创建 32 个字节的空间。然后注意在整个函数中,不是将项目推入堆栈,而是通过rbp 的偏移量将它们mov'd 到堆栈:
mov DWORD PTR [rbp-20], edi
mov QWORD PTR [rbp-32], rsi
mov DWORD PTR [rbp-4], 2
mov DWORD PTR [rbp-8], 5
这样做的原因是它允许变量随时随地存储,并随时随地从任何地方加载,而不需要大量的push/pops。
考虑使用push 和pop 存储变量的情况。假设一个变量在函数的早期存储,我们称之为foo。后面8个变量入栈,需要foo,应该怎么访问?
好吧,您可以弹出所有内容直到foo,然后将所有内容推回,但这很昂贵。
当您有条件语句时,它也不起作用。假设仅当 foo 是某个特定值时才存储变量。现在你有一个条件,堆栈指针可能位于它之后的两个位置之一!
出于这个原因,编译器总是更喜欢使用rbp - N 来存储变量,因为在函数中的任何点,变量仍将存在于rbp - N。
注意:在不同的 ABI(例如 i386 system V)上,参数的参数可能会在堆栈上传递,但这不是什么大问题,因为 ABI 通常会指定应该如何处理。同样,以 i386 system V 为例,函数的调用约定将类似于:
push edi ; 2nd argument to the function.
push eax ; 1st argument to the function.
call my_func
; here, it can be assumed that the stack has been corrected
那么,为什么push 实际上会引起问题?
嗯,我给the code加个小asmsn-p
在函数的最后,我们现在有以下内容:
push 64
mov eax, 0
leave
ret
由于压入堆栈,现在有 2 件事失败了。
第一个是leave指令(见this thread)
离开指令将尝试pop 存储在函数开头的rbp 的值(注意编译器生成的唯一push 在开头:push rbp)。
这是为了使调用者的堆栈帧保留在main 之后。通过压栈,在我们的例子中,rbp 现在将被设置为64,因为最后压入的值是64。当main 的被调用者恢复执行并尝试访问rbp - 8 处的值时,将发生崩溃,因为rbp - 8 在十六进制中是0x38,这是一个无效地址。
但这假设被调用者甚至可以得到执行!
rbp 用无效值恢复它的值后,堆栈上的下一个东西将是rbp 的原始值。
ret 指令将从堆栈中pop 一个值,并返回到该地址...
请注意这可能会有一些问题?
CPU 将尝试跳转到存储在函数开头的rbp 的值!
在几乎所有现代程序中,堆栈都是“禁止执行”区域(请参阅here),尝试从那里执行代码会立即导致崩溃。
所以,TLDR:推入堆栈违反了编译器所做的假设,最重要的是关于函数的返回地址。这种违规导致程序执行(通常)最终在堆栈上,这将导致崩溃