发布与调试模式下本地堆栈变量的代码生成

Posted

技术标签:

【中文标题】发布与调试模式下本地堆栈变量的代码生成【英文标题】:Code generation for local stack variables in release vs. debug mode 【发布时间】:2018-03-06 07:06:39 【问题描述】:

作为 Rust 操作系统的一部分,我有以下系统调用入口点:

#[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... 

这在调试模式和发布模式(启用调试信息)下工作正常,因为它生成的汇编代码在基指针 @987654324 的负偏移处存储局部堆栈变量,如 rdirsi 等@。 例如,这是生成的代码:

<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   $0x4,-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)

为什么会生成这段代码,使用堆栈指针的正偏移量的代码?这让我觉得很奇怪。

有什么方法可以避免这种情况或以某种方式更改代码生成?

【问题讨论】:

谢谢。我的目标 json 文件中已禁用红色区域,所以这不是问题。需要明确的是,来自 RSP 的负偏移量很好,正偏移量是问题,因为 RSP 上面的地址没有被映射。 只有当您确实有一个红色区域时,RSP 的负偏移才可以。但你不这样做,所以除非中断被禁用,否则它们是不安全的。 嗯,也许我误会了什么。假设我有一个内核堆栈占用地址 0x4000 到 0x8000,顶部 (rsp) 是 0x8000。如果我从堆栈顶部有一个负偏移量,比如 -8,足够的空间用于 u64,那么内存访问将在地址 0x7FF8 处有效。如果我有一个正偏移量,如我的代码示例中所示,访问将是无效的,不是吗? 是的,访问你没有用sub rsp, whatever 保留的堆栈内存也是不安全的,或者更一般地说,你不拥有由于某种原因(例如堆栈上的红色区域或函数参数) )。在普通函数中,您会在调用者的堆栈空间上乱涂乱画。在您的情况下,您将引用未分配或非堆栈内存,使所有堆栈内存低于rsp(直至0x4000)未使用。 它奇怪/令人困惑的原因是您要求一个naked 函数(因此没有函数序言来保留堆栈空间),但无论如何您在其中使用了局部变量。我认为在一些支持裸函数的 C 编译器中,这是不受支持的;只允许内联汇编作为整个函数体。但是,就内联汇编和编译器之间的奇怪探戈而言,Rust 所说的 IDK 得到了官方支持。 【参考方案1】:

堆栈向下增长。来自 RSP 的正偏移量是不受中断异步修改的安全部分,即“保留”。

来自 RSP 的负偏移量为 the red zone, which you can't have on the kernel stack。


使用sub rsp, 0x100 或其他任何方式为裸函数的局部变量保留足够的空间。 或者更好的是,将整个入口点写在 asm 中,而不是为此乱搞编译器生成的代码。

或者更好的是,自己使用push,它更紧凑(代码大小)并且同样高效push 非常适合在堆栈上保存寄存器; Linux 的系统调用入口点使用它。 (例如,the entry point into an x86-64 kernel from syscall in 64-bit user-space 使用 push 从 Linux 4.12 保存所有寄存器(在 Spectre / Meltdown 缓解/解决方法补丁使入口点更加复杂之前)。


这很奇怪/令人困惑的原因是您要求一个 naked 函数(因此没有函数序言来保留堆栈空间),但无论如何您在其中使用了局部变量。否则编译器会自行sub rsp, 0x... 为本地人保留足够的空间,然后再访问它们。

我认为在一些支持裸函数的 C/C++ 编译器中,这是不支持的;只允许内联汇编作为整个函数体。但是,就内联汇编和编译器之间的奇怪探戈而言,Rust 所说的 IDK 得到了官方支持。就像我说的,如果你用纯 asm 编写入口点,你就不会遇到这些问题。


您的调试模式版本出现故障;您正在相对于 RBP 存储,但尚未设置 RBP。您要求 naked 函数,因此您需要自己 mov rbp, rsp(在从 gs:0 加载 RSP 之后),然后 sub rsp, 0x20 或其他任何东西,以便在堆栈帧中为这些负偏移量保留足够的空间。

我认为您的调试模式版本是相对于用户空间的 RBP 进行存储的,如果用户空间使用 RBP 指向任何不应该被破坏的东西附近进行系统调用,这将非常糟糕,如果 RBP 持有则更可怕一个非指针值。

(如果您刚刚使用mov rbp, rsp 或其他东西,根据您的评论,您将这部分遗漏了,那么您使用的空间低于 RSP,如果没有红色区域,这是不安全的。)

【讨论】:

感谢您的回答。是的,我之前考虑过从 rsp 中减去一些空间,或者更好地将初始系统调用堆栈设置在堆栈本身的顶部下方,但在我看来这有点 hacky。抱歉,为简洁起见,我省略了一些其他代码,但是是的,我正在正确处理 rbp。我想我真正的困惑源于它删除了帧指针,然后使用来自rsp 的正偏移量,这是我没想到的(由于我自己的误解)。

以上是关于发布与调试模式下本地堆栈变量的代码生成的主要内容,如果未能解决你的问题,请参考以下文章

在 QtCreator 调试模式下看不到本地变量的值

Rust - 调试与发布模式的堆栈大小是不是不同?

在发布模式下调试符号

为啥在发布模式下调试会隐藏信息?

在调试模式下搜索变量的值,这可能吗?

发布模式下的调试(优化开启时)和调试模式下的调试有啥区别?