在 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
。
这里的问题是编译器实际上永远不会在函数体中使用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
/pop
s。
考虑使用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加个小asm
sn-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 推送到堆栈的主要内容,如果未能解决你的问题,请参考以下文章