强烈建议在创建64位内核(针对x86_64平台)时,指示编译器不要使用用户空间ABI所使用的128字节的Red Zone。(对于GCC,编译器标志是-mno-red-zone
)。
如果内核是启用的,它将不是中断安全的。
为什么呢?
可以在内核类型上下文中使用红色区域。IDTentry可以指定一个0的堆栈索引(list)。7,这里0有点特殊。TSS包含这些堆栈的一个表。1..7被加载,并用于异常/中断保存的初始寄存器,并且不嵌套。如果按优先级(例如:NMI是最高的,可以在任何时候发生)并将这些堆栈视为蹦床,您可以安全地处理内核类型上下文中的红色区域。也就是说,在启用中断或可能导致异常的代码之前,可以从保存的堆栈指针中减去128,以获得可用的内核堆栈。
零索引堆栈以更传统的方式工作,当没有权限转换时,在现有堆栈上推入堆栈,标志,pc,错误。
在蹦床的代码必须小心(呸,它是一个内核),不要产生其他异常,而它的机器状态,但提供了一个很好的,安全的位置来检测病理内核嵌套,堆栈损坏,等等…[抱歉这么晚才回复,在搜索其他东西时注意到这一点]。
引用AMD64 ABI:
超出%rsp所指向位置的128字节区域被认为是保留的,信号处理程序或中断处理程序不得修改。因此,函数可以将此区域用于在函数调用中不需要的临时数据。特别地,叶函数可以在整个堆栈框架中使用这个区域,而不是在序言和尾声中调整堆栈指针。这个区域被称为红色区域。
本质上,这是一种优化——userland编译器确切地知道在任何给定时间有多少Red Zone被使用(在最简单的实现中,是局部变量的整个大小),并且可以在调用子函数之前相应地调整%rsp
。
特别是在叶函数中,这可以产生一些不必调整%rsp
的性能优势,因为我们可以确定在函数中不会运行不熟悉的代码。(POSIX信号处理程序可能被视为协例程的一种形式,但您可以指示编译器在信号处理程序中使用堆栈变量之前调整寄存器)。
在内核空间中,一旦您开始考虑中断,如果这些中断对%rsp
做出任何假设,那么它们很可能是不正确的—关于Red Zone的利用率没有确定性。因此,您要么假设所有这些都是脏的,并且不必要地浪费堆栈空间(在每个函数中有效地运行一个128字节的保证局部变量),要么保证中断对%rsp
不做任何假设——这很棘手。
在用户空间,上下文切换+ 128字节的堆栈超额分配为您处理它。
在内核空间中,您使用的是中断使用的相同堆栈。当中断发生时,CPU会推送一个返回地址和RFLAGS。这比rsp
低16字节。即使您想编写一个中断处理程序,假设红色区域的全部128字节是有价值的,这也是不可能的。
你可能有一个内核内部的ABI,从rsp-16
到rsp-48
或其他地方有一个小的红色区域。(小是因为内核栈很有价值,而且大多数函数不需要太多的红区)
中断处理程序必须在压入任何寄存器之前执行sub rsp, 32
。(并恢复到iret
之前)
如果中断处理程序本身可以在运行sub rsp, 32
之前中断,或者在iret
之前恢复rsp
之后中断,则此想法将不起作用。有价值的数据在rsp .. rsp-16
上会有一个漏洞窗口。
这个方案的另一个实际问题是AFAIK gcc没有可配置的红区参数。不是开就是关。因此,如果您想利用它,就必须在gcc/clang中添加对内核风格的red-zone的支持。
即使它对嵌套中断是安全的,好处也很小。证明它在内核中是安全的困难可能使它不值得。(正如我所说的,我不确定是否可以安全地实现,因为我认为嵌套中断是可能的。)
(顺便说一句,请参阅x86标记wiki以获取记录红色区域的ABI链接,以及其他内容)
我给你一个引用维基百科的例子:
众所周知,对于x86-64内核开发人员来说,红色区域会导致问题,因为CPU本身在调用中断处理程序时不尊重红色区域。这将导致一个微妙的内核破坏,因为ABI与CPU行为相矛盾。
在我的内核中,我使用Linux memcpy() c函数:
void *memcpy(void *dest, const void *src,
size_t count)
{
char *tmp = dest;
const char *s = src;
while (count--)
*tmp++ = *s++;
return dest;
}
,拆卸为:
0000000000000000 <memcpy>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 8d 05 f9 ff ff ff lea -0x7(%rip),%rax # 8 <memcpy+0x8>
f: 49 bb 00 00 00 00 00 movabs $0x0,%r11
16: 00 00 00
19: 4c 01 d8 add %r11,%rax
1c: 48 89 7d e8 mov %rdi,-0x18(%rbp)
20: 48 89 75 e0 mov %rsi,-0x20(%rbp)
24: 48 89 55 d8 mov %rdx,-0x28(%rbp)
28: 48 8b 45 e8 mov -0x18(%rbp),%rax
2c: 48 89 45 f8 mov %rax,-0x8(%rbp)
30: 48 8b 45 e0 mov -0x20(%rbp),%rax
34: 48 89 45 f0 mov %rax,-0x10(%rbp)
38: eb 1d jmp 57 <memcpy+0x57>
3a: 48 8b 55 f0 mov -0x10(%rbp),%rdx
3e: 48 8d 42 01 lea 0x1(%rdx),%rax
42: 48 89 45 f0 mov %rax,-0x10(%rbp)
46: 48 8b 45 f8 mov -0x8(%rbp),%rax
4a: 48 8d 48 01 lea 0x1(%rax),%rcx
4e: 48 89 4d f8 mov %rcx,-0x8(%rbp)
52: 0f b6 12 movzbl (%rdx),%edx
55: 88 10 mov %dl,(%rax)
57: 48 8b 45 d8 mov -0x28(%rbp),%rax
5b: 48 8d 50 ff lea -0x1(%rax),%rdx
5f: 48 89 55 d8 mov %rdx,-0x28(%rbp)
63: 48 85 c0 test %rax,%rax
66: 75 d2 jne 3a <memcpy+0x3a>
68: 48 8b 45 e8 mov -0x18(%rbp),%rax
6c: 5d pop %rbp
6d: c3 retq
注意1c到24中的指令,三个参数通过" move "存储在堆栈中;但不是"push",和2c和34一样,是两个局部变量。
现在问题来了。我在ubuntu上编译了我的x86_64内核,使用gcc默认的x64 abi(sysv amd64 abi,隐式红色区域)。当运行到这个由exec调用的函数时,肯定会触发写时复制(意味着会首先导致页面错误异常),变量address和%RSP看起来像:调试会话1的屏幕截图
你可以看到%RSP在存储的args和localvars的上方,所以你猜当在x86_64机器上引发异常时会发生什么---- cpu自动保存至少5个寄存器在堆栈----它们覆盖了args和localvars。
然后用选项-mno-red-zone编译它,反汇编的开始部分:
0000000000000000 <memchr>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 83 ec 28 sub $0x28,%rsp
c: 48 8d 05 f9 ff ff ff lea -0x7(%rip),%rax # c <memchr+0xc>
注意到与前者的区别了吗?它使用
保留args和localvars的堆栈空间。8: 48 83 ec 28 sub $0x28,%rsp
和运行结果:调试会话2的屏幕截图现在%RSP位于参数和本地变量的下方。
所以核心原因是:在叶子函数中,正常情况下,不需要将%RSP调整到栈顶,所以在红区机制下,%RSP不会被调整。但是在内核中,内核代码和异常/中断代码共享内核堆栈(除非您为异常/中断准备隔离堆栈,对于X86_64 cpu它是IST),当叶子函数中断时,args和localvars将被覆盖