段错误推送到 C 内联汇编中的堆栈
Segfault pushing to stack in C inline assembly
我遇到一些内联汇编的问题。我正在编写一个编译器,它正在编译为汇编,为了可移植性,我让它在 C 中添加了主要功能,并且只使用内联汇编。尽管即使是最简单的内联汇编也会给我一个段错误。感谢您的帮助
int main(int argc, char** argv) {
__asm__(
"push \n"
);
return 0;
}
TLDR 在底部。注意:这里的一切都是假设 x86_64
.
这里的问题是编译器实际上永远不会在函数体中使用 push
或 pop
(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
/pop
s。
考虑使用 push
和 pop
存储变量的情况。假设一个变量存储在函数的早期,我们称之为 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 地址。这种违规导致程序执行结束在堆栈上(通常),这将导致崩溃
我遇到一些内联汇编的问题。我正在编写一个编译器,它正在编译为汇编,为了可移植性,我让它在 C 中添加了主要功能,并且只使用内联汇编。尽管即使是最简单的内联汇编也会给我一个段错误。感谢您的帮助
int main(int argc, char** argv) {
__asm__(
"push \n"
);
return 0;
}
TLDR 在底部。注意:这里的一切都是假设 x86_64
.
这里的问题是编译器实际上永远不会在函数体中使用 push
或 pop
(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
/pop
s。
考虑使用 push
和 pop
存储变量的情况。假设一个变量存储在函数的早期,我们称之为 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 地址。这种违规导致程序执行结束在堆栈上(通常),这将导致崩溃