为啥内核代码不能使用红区

Posted

技术标签:

【中文标题】为啥内核代码不能使用红区【英文标题】:Why can't kernel code use a Red Zone为什么内核代码不能使用红区 【发布时间】:2014-11-05 09:36:29 【问题描述】:

强烈建议在创建 64 位内核(对于 x86_64 平台)时,指示编译器不要使用用户空间 ABI 所做的 128 字节红色区域。 (对于 GCC,编译器标志是 -mno-red-zone)。

如果启用,内核将不是中断安全的。

但这是为什么呢?

【问题讨论】:

相关:***.com/questions/38042188/… 和 ***.com/questions/37941779/… 有答案解释了红色区域对于可以使用它的代码的意义。 【参考方案1】:

可以在内核类型的上下文中使用红色区域。 IDTentry 可以指定堆栈索引 (ist) 为 0..7,其中 0 有点特殊。 TSS 包含这些堆栈的表。 1..7被加载,用于异常/中断保存的初始寄存器,不嵌套。如果您按优先级划分各种异常条目(例如,NMI 是最高的并且可能随时发生)并将这些堆栈视为蹦床,您可以安全地处理内核类型上下文中的红色区域。也就是说,在启用可能导致异常的中断或代码之前,您可以从保存的堆栈指针中减去 128 以获得可用的内核堆栈。

零索引堆栈以更传统的方式运行,当没有特权转换时,将堆栈、标志、pc、错误推入现有堆栈。

trampoline 中的代码必须小心(呃,它是一个内核),在清理机器状态时不要产生其他异常,但提供了一个很好、安全的位置来检测病态内核嵌套、堆栈损坏等。 .. [抱歉这么晚才回复,在搜索其他内容时注意到了这一点]。

【讨论】:

请多点赞这个人。这就是为什么在 ABI 中引入红色区域的原因 - 如果您确实使用 64 位 TSS 和 IST 机制,它是普遍可用的,专门为使其工作而创建的。【参考方案2】:

在内核空间中,您使用的堆栈与中断使用的堆栈相同。当中断发生时,the CPU pushes a return address and RFLAGS。这会破坏rsp 以下的 16 个字节。即使您想编写一个假设红色区域的全部 128 字节都是有价值的中断处理程序,也是不可能的。


你可能有一个内核内部的 ABI,它有一个从 rsp-16rsp-48 或其他东西的小红区。 (小是因为内核堆栈很有价值,而且大多数函数反正不需要太多的红区。)

在推送任何寄存器之前,中断处理程序必须sub rsp, 32。 (并在iret之前恢复)。

这个想法是行不通的,如果一个中断处理程序本身可以在它运行sub rsp, 32 之前被中断,或者在它恢复rsp 之后在iret 之前被中断。将有一个漏洞窗口,其中有价值的数据位于rsp .. rsp-16


此方案的另一个实际问题是 AFAIK gcc 没有可配置的红区参数。它是打开还是关闭。因此,如果您想利用它,就必须在 gcc / clang 中添加对 red-zone 内核风格的支持。

即使它不受嵌套中断的影响,好处也很小。证明它在内核中安全的困难可能使它不值得。 (正如我所说,我完全不确定它可以安全地实现,因为我认为嵌套中断是可能的。)


(顺便说一句,请参阅x86 标记 wiki 以获取指向记录红区和其他内容的 ABI 的链接。)

【讨论】:

有点不确定为什么它不起作用参见。句子“如果中断处理程序本身可以在它运行 sub rsp、32 之前或在它在 iret 之前恢复 rsp 之后被中断,那么这个想法将不起作用。将有一个漏洞窗口,其中有价值的数据位于 rsp .. rsp -16。”。 “第二个中断”处理程序不会也执行 sub rsp,32'ing,从而保护原始中断代码的假定红色区域吗?是因为会有多个嵌套返回地址+RFLAGS(由 CPU 本身推送)最终可能覆盖红色区域吗? @Morty:如果第二个或第三个嵌套中断由硬件软件可以运行sub rsp,32 之前处理,则不会。一个异常/中断帧超过 16 个字节:至少 RIP、CS、RFLAGS,对于同步异常,还有一个异常类型代码 IIRC。而如果嵌套可以发生,那么理论上双嵌套也可以发生,所以即使sub rsp, 2*max_single_frame理论上也是不够的,任意大小也不是。【参考方案3】:

引用 AMD64 ABI:

%rsp 指向的位置以外的 128 字节区域被认为是保留的,不应被信号或中断处理程序修改。因此,函数可以将此区域用于函数调用之间不需要的临时数据。特别是,叶函数可以将这个区域用于它们的整个堆栈帧,而不是在序言和尾声中调整堆栈指针。这个区域被称为红色区域。

本质上,这是一种优化 - 用户级编译器确切地知道在任何给定时间使用了多少红色区域(在最简单的实现中,局部变量的整个大小)并且可以在调用之前相应地调整 %rsp子功能。

特别是在叶函数中,这可以产生一些不必调整%rsp 的性能优势,因为我们可以确定在函数中不会运行不熟悉的代码。 (POSIX 信号处理程序可能被视为协程的一种形式,但您可以指示编译器在信号处理程序中使用堆栈变量之前调整寄存器)。

在内核空间中,一旦您开始考虑中断,如果这些中断对%rsp 做出任何假设,那么它们很可能是不正确的——关于红区的利用率并不确定。因此,您要么假设所有这些都是脏的,并且不必要地浪费堆栈空间(有效地在每个函数中使用 128 字节保证的局部变量运行),或者,您保证中断不对%rsp 做出任何假设——这很棘手.

在用户空间中,上下文切换 + 128 字节的堆栈过度分配会为您处理它。

【讨论】:

这不仅仅是节省空间。实际上不可能安全地实现正常的 128 字节红色区域,因为在中断处理程序的任何代码运行之前,中断总是会破坏 %rsp 以下的 16 字节。 @qdot,你能解释一下你所说的 128 字节过度分配是什么意思吗?这意味着如果 amd64 ABI 没有“红区”概念,可以增长的最低地址堆栈将高出 128 字节? POSIX 信号由内核而不是硬件传递给处理程序。当传递不使用sigaltstack 的信号时,内核只是尊重ABI 的红色区域。内核中的相关代码不是编译器生成的。这就是为什么可以将普通函数注册为信号处理程序的原因;他们不需要任何特殊的__attribute__ 来专门编译。 至少在 Linux 上,libc 不必默默地用包装函数替换 sigaction(2) 中的真实地址。它只告诉内核将什么返回地址传递给该用户空间函数调用,使其返回到使用sigreturn(2) 的特殊libc 函数。 (该手册页描述了内核将线程的寄存器状态放入用户空间堆栈的 Linux 机制。) 此外,在 x86-64 上,局部变量低于返回地址,因此为虚拟局部变量保留 128 字节的空间也无济于事。有一个上面的返回地址会破坏一个红色区域。 (与带有链接寄存器的 ISA 不同,在这种情况下,普通函数调用在寄存器中获取其返回地址,而不是在堆栈内存中。尽管大多数 ISA 上的中断仍然隐式使用堆栈。)

以上是关于为啥内核代码不能使用红区的主要内容,如果未能解决你的问题,请参考以下文章

System V ABI的红区是如何实现的

为啥这段 Java 代码没有利用所有 CPU 内核?

为啥 printf() 可以在内核中工作,但使用 std::cout 不能?

为啥 printf() 可以在内核中工作,但使用 std::cout 不能?

为啥 C++ 中的 main() 不能内联?

为啥编译内核时不能得到相同的符号?