发布与调试模式下局部堆栈变量的代码生成
Code generation for local stack variables in release vs. debug mode
作为 Rust OS 的一部分,我有以下系统调用入口点:
#[no_mangle]
#[naked]
#[inline(never)]
unsafe extern "C" fn syscall_handler() {
// switch to the kernel stack dedicated for syscall handling, and save the user task's details
asm!("swapgs; \
mov gs:[0x8], rsp; \
mov gs:[0x10], rcx; \
mov gs:[0x18], r11; \
mov rsp, gs:[0x0];"
: : : "memory" : "intel", "volatile");
let (rax, rdi, rsi, rdx, r10, r8, r9): (u64, u64, u64, u64, u64, u64, u64);
asm!("" : "={rax}"(rax), "={rdi}"(rdi), "={rsi}"(rsi), "={rdx}"(rdx), "={r10}"(r10), "={r8}"(r8), "={r9}"(r9) : : "memory" : "intel", "volatile");
// do stuff with rax, rdi, rsi...
这在调试模式和发布模式(启用调试信息)下工作正常,因为它生成汇编代码,以负偏移量存储本地堆栈变量,如 rdi
、rsi
等来自基指针 rbp
。
例如,这是生成的代码:
<syscall_handler>:
swapgs
mov %rsp,%gs:0x8
mov %rcx,%gs:0x10
mov %r11,%gs:0x18
mov %gs:0x0,%rsp
mov %rax,-0x1f0(%rbp)
mov %rdi,-0x1e8(%rbp)
mov %rsi,-0x1e0(%rbp)
mov %rdx,-0x1d8(%rbp)
mov %r10,-0x1d0(%rbp)
mov %r8,-0x1c8(%rbp)
mov %r9,-0x1c0(%rbp)
movb [=11=]x4,-0x1b1(%rbp)
该代码运行良好,因为我的系统调用处理程序使用指向当前内核堆栈顶部的堆栈指针运行(像往常一样),这意味着可以使用堆栈指针/基指针的负偏移量(基指针 rbp
在此之前根据堆栈指针值设置)。
当我在没有调试信息的发布模式下构建时,它生成的代码使用堆栈指针本身的正偏移量(rsp
,而不是基指针)作为本地堆栈变量的位置。这真的很奇怪并且会导致问题,因为当前堆栈指针 rsp
上方的内存超出范围。
这是在没有调试信息的纯发布模式下生成的代码:
<syscall_handler>:
swapgs
mov %rsp,%gs:0x8
mov %rcx,%gs:0x10
mov %r11,%gs:0x18
mov %gs:0x0,%rsp
mov %rax,0x1c0(%rsp)
mov %rdi,0x1c8(%rsp)
mov %rsi,0x1d0(%rsp)
mov %rdx,0x1d8(%rsp)
mov %r10,0x1e0(%rsp)
mov %r8,0x1e8(%rsp)
mov %r9,0x1f0(%rsp)
为什么会生成这段代码,使用堆栈指针的正偏移量的代码?这让我觉得很奇怪。
有什么方法可以避免这种情况或以某种方式更改代码生成吗?
堆栈向下增长。 RSP 的正偏移量是可以避免中断异步修改的部分,即 "reserved".
RSP 的负偏移量为 the red zone, which you can't have on the kernel stack。
使用 sub rsp, 0x100
或其他方式为裸函数的局部变量保留足够的 space。 或者更好的是,将整个入口点写在 asm 中而不是为此乱搞 compiler-generated 代码。
或者更好的是,您自己使用 push
,它更紧凑 (code-size) 并且同样高效 。 push
非常适合在堆栈上保存寄存器; Linux 的系统调用入口点使用它。 (例如 the entry point into an x86-64 kernel from syscall
in 64-bit user-space 使用 push
保存所有寄存器,从 Linux 4.12 开始(在 Spectre / Meltdown mitigation/workaround 补丁使入口点更加复杂之前)。
奇怪/令人困惑的原因是您要求一个 naked
函数(因此没有函数序言来保留堆栈 space),但是您无论如何都在其中使用了局部变量。否则,编译器将 sub rsp, 0x...
自行为本地人保留足够的 space,然后再访问它们。
我认为在某些支持裸函数的 C/C++ 编译器中,这是不支持的;只允许内联 asm 作为整个函数体。但是就内联 asm 和编译器之间奇怪的探戈而言,IDK 官方支持 Rust 所说的。就像我说的,如果你用纯 asm 编写入口点,你就不会有这些问题。
您的 debug-mode 版本似乎已损坏;您正在相对于 RBP 进行存储,但 RBP 尚未设置。你要求一个 naked
函数,所以你需要 mov rbp, rsp
自己(从 gs:0
加载 RSP 之后),然后 sub rsp, 0x20
或任何保留足够的 space 在那些负偏移量的堆栈帧中。
我认为你的 debug-mode 版本是相对于 user-space 的 RBP 存储的,如果 user-space 进行系统调用时 RBP 指向任何不应该指向的地方,它会严重崩溃被破坏,如果 RBP 持有 non-pointer 值,则更可怕。
(如果您刚刚使用 mov rbp, rsp
或其他东西,根据您的评论,您将此部分排除在外,那么您使用的是低于 RSP 的 space,如果没有red-zone.)
作为 Rust OS 的一部分,我有以下系统调用入口点:
#[no_mangle]
#[naked]
#[inline(never)]
unsafe extern "C" fn syscall_handler() {
// switch to the kernel stack dedicated for syscall handling, and save the user task's details
asm!("swapgs; \
mov gs:[0x8], rsp; \
mov gs:[0x10], rcx; \
mov gs:[0x18], r11; \
mov rsp, gs:[0x0];"
: : : "memory" : "intel", "volatile");
let (rax, rdi, rsi, rdx, r10, r8, r9): (u64, u64, u64, u64, u64, u64, u64);
asm!("" : "={rax}"(rax), "={rdi}"(rdi), "={rsi}"(rsi), "={rdx}"(rdx), "={r10}"(r10), "={r8}"(r8), "={r9}"(r9) : : "memory" : "intel", "volatile");
// do stuff with rax, rdi, rsi...
这在调试模式和发布模式(启用调试信息)下工作正常,因为它生成汇编代码,以负偏移量存储本地堆栈变量,如 rdi
、rsi
等来自基指针 rbp
。
例如,这是生成的代码:
<syscall_handler>:
swapgs
mov %rsp,%gs:0x8
mov %rcx,%gs:0x10
mov %r11,%gs:0x18
mov %gs:0x0,%rsp
mov %rax,-0x1f0(%rbp)
mov %rdi,-0x1e8(%rbp)
mov %rsi,-0x1e0(%rbp)
mov %rdx,-0x1d8(%rbp)
mov %r10,-0x1d0(%rbp)
mov %r8,-0x1c8(%rbp)
mov %r9,-0x1c0(%rbp)
movb [=11=]x4,-0x1b1(%rbp)
该代码运行良好,因为我的系统调用处理程序使用指向当前内核堆栈顶部的堆栈指针运行(像往常一样),这意味着可以使用堆栈指针/基指针的负偏移量(基指针 rbp
在此之前根据堆栈指针值设置)。
当我在没有调试信息的发布模式下构建时,它生成的代码使用堆栈指针本身的正偏移量(rsp
,而不是基指针)作为本地堆栈变量的位置。这真的很奇怪并且会导致问题,因为当前堆栈指针 rsp
上方的内存超出范围。
这是在没有调试信息的纯发布模式下生成的代码:
<syscall_handler>:
swapgs
mov %rsp,%gs:0x8
mov %rcx,%gs:0x10
mov %r11,%gs:0x18
mov %gs:0x0,%rsp
mov %rax,0x1c0(%rsp)
mov %rdi,0x1c8(%rsp)
mov %rsi,0x1d0(%rsp)
mov %rdx,0x1d8(%rsp)
mov %r10,0x1e0(%rsp)
mov %r8,0x1e8(%rsp)
mov %r9,0x1f0(%rsp)
为什么会生成这段代码,使用堆栈指针的正偏移量的代码?这让我觉得很奇怪。
有什么方法可以避免这种情况或以某种方式更改代码生成吗?
堆栈向下增长。 RSP 的正偏移量是可以避免中断异步修改的部分,即 "reserved".
RSP 的负偏移量为 the red zone, which you can't have on the kernel stack。
使用 sub rsp, 0x100
或其他方式为裸函数的局部变量保留足够的 space。 或者更好的是,将整个入口点写在 asm 中而不是为此乱搞 compiler-generated 代码。
或者更好的是,您自己使用 push
,它更紧凑 (code-size) 并且同样高效 。 push
非常适合在堆栈上保存寄存器; Linux 的系统调用入口点使用它。 (例如 the entry point into an x86-64 kernel from syscall
in 64-bit user-space 使用 push
保存所有寄存器,从 Linux 4.12 开始(在 Spectre / Meltdown mitigation/workaround 补丁使入口点更加复杂之前)。
奇怪/令人困惑的原因是您要求一个 naked
函数(因此没有函数序言来保留堆栈 space),但是您无论如何都在其中使用了局部变量。否则,编译器将 sub rsp, 0x...
自行为本地人保留足够的 space,然后再访问它们。
我认为在某些支持裸函数的 C/C++ 编译器中,这是不支持的;只允许内联 asm 作为整个函数体。但是就内联 asm 和编译器之间奇怪的探戈而言,IDK 官方支持 Rust 所说的。就像我说的,如果你用纯 asm 编写入口点,你就不会有这些问题。
您的 debug-mode 版本似乎已损坏;您正在相对于 RBP 进行存储,但 RBP 尚未设置。你要求一个 naked
函数,所以你需要 mov rbp, rsp
自己(从 gs:0
加载 RSP 之后),然后 sub rsp, 0x20
或任何保留足够的 space 在那些负偏移量的堆栈帧中。
我认为你的 debug-mode 版本是相对于 user-space 的 RBP 存储的,如果 user-space 进行系统调用时 RBP 指向任何不应该指向的地方,它会严重崩溃被破坏,如果 RBP 持有 non-pointer 值,则更可怕。
(如果您刚刚使用 mov rbp, rsp
或其他东西,根据您的评论,您将此部分排除在外,那么您使用的是低于 RSP 的 space,如果没有red-zone.)