在 C 内联汇编中将 Segfault 推送到堆栈

Posted

技术标签:

【中文标题】在 C 内联汇编中将 Segfault 推送到堆栈【英文标题】:Segfault pushing to stack in C inline assembly 【发布时间】:2022-01-16 14:33:28 【问题描述】:

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

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


【问题讨论】:

1.对于汇编语言问题,我们需要了解您正在使用的 CPU 架构。 2.内联汇编不得[rfc2119] 修改堆栈指针。这适用于所有 CPU 架构,以及所有使用您正在使用的内联汇编语法的 C 编译器。 最简单的内联汇编是nop(或空的)。 虽然'main' 很容易被认为是c 程序的顶部,但它上面通常有一些代码(例如设置argc 和argv)。这意味着它需要能够返回给调用者。但是您的代码正在调整堆栈,并将 $1 放在调用者的地址所在的位置。所以当它试图返回时,它会去一个非常糟糕的地方。 @DavidWohlferd 如果您想将此作为答案发布,那就太好了。我真的没有意识到这一点。谢谢你:) 【参考方案1】:

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

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

考虑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。

考虑使用pushpop 存储变量的情况。假设一个变量在函数的早期存储,我们称之为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:推入堆栈违反了编译器所做的假设,最重要的是关于函数的返回地址。这种违规导致程序执行(通常)最终在堆栈上,这将导致崩溃

【讨论】:

好的,谢谢。关于我的编译器的事情是它是一个名为 corth 的“副本”。它将具有更多功能。这就是它使用堆栈的原因。只是为了方便。 @ANTHONYSTERLING-PALMARI:将基于堆栈的语言编译成 x86 代码以同样的方式使用堆栈对于性能来说是非常垃圾的,但可以作为玩具编译器的小步骤来完成。无论如何,我看不出在编译器中执行 推送/弹出指令 会有什么帮助。它真的是翻译吗? (我假设您显示的代码应该是您的编译器的一部分,而不是您的编译器编译的程序。) 如果你想使用 asm 堆栈作为堆栈数据结构,你也不能混合调用/返回,因为返回地址和本地变量会与你的数据混合。在 C 中做到这一点并不容易,甚至不可能。如果您的编译器(或解释器?)是用 asm 编写的,那么您可以这样做,并且也会使问题变得明显,因为那时 push / ret 就在您自己的代码中。 @msimonelli:如果 GCC 在函数入口(而不是推送)上移动 RSP,它将只使用leave。在为 x86-64 SysV 编译时,它可以使用 RSP 下面的红色区域作为本地变量(包括在 -O0 调试版本中溢出寄存器参数)。这就是为什么这个内联汇编实际上会破坏事情:如果它使用了leave,那将撤消推送。 godbolt.org/z/61vsoqf5M 表明,如果您使用 -O0 -mno-red-zone 构建它,它不会崩溃(尽管仍然超级损坏),因为它使用 main(int, char**) 而不是 main(void)。后者仍然会崩溃。 说到红区,在一个 asm() 语句中执行 balanced push/pop 是不安全的,因为没有办法告诉编译器你是将覆盖该空间。您必须在输入时将 RSP 向下移动 128 个字节,然后将其移回,除非您使用 -mno-red-zone 编译此函数/文件。 Inline assembly that clobbers the red zone@安东尼斯特林-帕尔马里。 (尽管我认为在 inline asm 中执行 push/pop 来解释 Forth 的整个想法注定要失败,即使使用 -mno-red-zone。)

以上是关于在 C 内联汇编中将 Segfault 推送到堆栈的主要内容,如果未能解决你的问题,请参考以下文章

用完堆内存尝试在 Java 中将单个字符推送到堆栈

pthread中将处理程序送到堆栈上

推送到包含C中唯一唯一值的堆栈

汇编程序从堆栈中写入字符

如何在 PHP 中将数组值推送到另一个数组中? [复制]

了解 C/C++ 中函数调用的堆栈框架? [关闭]