段错误推送到 C 内联汇编中的堆栈

Segfault pushing to stack in C inline assembly

我遇到一些内联汇编的问题。我正在编写一个编译器,它正在编译为汇编,为了可移植性,我让它在 C 中添加了主要功能,并且只使用内联汇编。尽管即使是最简单的内联汇编也会给我一个段错误。感谢您的帮助

int main(int argc, char** argv) {
  __asm__(
"push \n"
  );
  return 0;
}

TLDR 在底部。注意:这里的一切都是假设 x86_64.

这里的问题是编译器实际上永远不会在函数体中使用 pushpop(prologues/epilogues 除外)。

考虑 this example

函数开始时,在序言中的堆栈上腾出空间:

push rbp
mov rbp, rsp
sub rsp, 32

这为 main 创建了 32 字节的空间。然后注意在整个函数中,不是将项目推入堆栈,而是通过 rbp:

的偏移量将它们 mov' 到堆栈
        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。

考虑使用 pushpop 存储变量的情况。假设一个变量存储在函数的早期,我们称之为 foo。 8个变量入栈之后,需要foo,应该怎么访问呢?

好吧,您可以在 foo 之前弹出所有内容,然后将所有内容推回,但这很昂贵。

当你有条件语句时它也不起作用。假设只有 foo 是某个特定值时才会存储变量。现在你有一个条件,堆栈指针可以在它后面的两个位置之一!

出于这个原因,编译器总是喜欢使用 rbp - N 来存储变量,因为在函数的 任何 点,变量仍然存在于 rbp - N.

注意:在不同的 ABI 上(例如 i386 系统 V),参数的参数可能会在堆栈上传递,但这不是什么大问题,因为 ABI 通常会指定应如何处理。同样,以 i386 系统 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

中添加一小段 asm

在函数的最后,我们现在有以下内容:

        push 64

        mov     eax, 0
        leave
        ret

由于压入堆栈,现在有 2 件事失败了。

首先是leave指令(参见this thread

leave 指令将尝试 pop 存储在函数开头的 rbp 的值(注意编译器生成的唯一 push 是在开头: push rbp).

这是为了在 main 之后保留调用者的堆栈帧。通过压入堆栈,在我们的例子中 rbp 现在将被设置为 64,因为最后压入的值是 64。当 main 的被调用者恢复它的执行,并试图访问一个值,比如 rbp - 8,就会发生崩溃,因为 rbp - 8 是十六进制的 0x38,这是无效地址。

但这假设被调用者甚至得到执行!

rbp 用无效值恢复其值后,堆栈中的下一个内容将是 rbp 的原始值。

ret指令将从堆栈中pop一个值,return到那个地址...

注意这可能有点问题吗?

CPU 将尝试跳转到函数开头存储的 rbp 的值!

几乎在每个现代程序中,堆栈都是一个“禁止执行”区域(参见 here),尝试从那里执行代码会立即导致崩溃。

所以,TLDR:推入堆栈违反了编译器所做的假设,最重要的是关于函数的 return 地址。这种违规导致程序执行结束在堆栈上(通常),这将导致崩溃