6. 内核栈¶
6.1. x86-64 位上的内核栈¶
大部分文本来自 Keith Owens,由 AK 修改
x86_64 页大小 (PAGE_SIZE) 为 4K。
和所有其他架构一样,x86_64 为每个活动线程都有一个内核栈。这些线程栈的大小为 THREAD_SIZE (4*PAGE_SIZE)。只要线程处于活动状态或为僵尸线程,这些栈就会包含有用的数据。当线程在用户空间时,内核栈是空的,除了底部的 thread_info 结构体。
除了每个线程的栈之外,还有与每个 CPU 关联的专用栈。这些栈仅在内核在该 CPU 上处于控制状态时使用;当 CPU 返回用户空间时,专用栈不包含有用数据。主要的 CPU 栈有
中断栈。IRQ_STACK_SIZE
用于外部硬件中断。如果这是第一次外部硬件中断(即不是嵌套的硬件中断),则内核从当前任务切换到中断栈。就像 i386 上分离的线程栈和中断栈一样,这为内核中断处理提供了更多空间,而无需增加每个线程栈的大小。
处理软中断(softirq)时也会使用中断栈。
切换到内核中断栈是由软件根据每个 CPU 的中断嵌套计数器完成的。这是必需的,因为 x86-64 的 “IST” 硬件栈在不发生竞争的情况下无法嵌套。
x86_64 还具有 i386 上所没有的一项功能,即能够针对双重异常(double fault)或 NMI 等指定事件自动切换到新栈,这使得在 x86_64 上处理这些异常事件更加容易。此功能称为中断栈表 (IST)。每个 CPU 最多可有 7 个 IST 条目。IST 代码是任务状态段 (TSS) 的索引。TSS 中的 IST 条目指向专用栈;每个栈的大小可以不同。
IST 是通过中断门描述符中 IST 字段的非零值来选择的。当发生中断且硬件加载了此类描述符时,硬件会自动根据 IST 值设置新的栈指针,然后调用中断处理程序。如果中断来自用户模式,则中断处理程序的序言(prologue)将切换回每个线程的栈。如果软件希望允许嵌套的 IST 中断,则处理程序必须在进入和退出中断处理程序时调整 IST 值。(这偶尔会用到,例如用于调试异常。)
具有不同 IST 代码(即具有不同栈)的事件可以嵌套。例如,调试中断可以安全地被 NMI 打断。arch/x86_64/kernel/entry.S::paranoidentry 会在所有 IST 事件的入口和出口处调整栈指针,理论上允许相同代码的 IST 事件嵌套。但是在大多数情况下,分配给 IST 的栈大小假定相同代码不会嵌套。如果打破了该假设,则栈将会损坏。
当前分配的 IST 栈有
ESTACK_DF. EXCEPTION_STKSZ (PAGE_SIZE)。
用于中断 8 - 双重异常 (#DF)。
在处理一个异常时引发另一个异常时调用。当内核非常混乱(例如内核栈指针损坏)时发生。使用单独的栈可以让内核在许多情况下恢复得足够好,从而仍然能够输出 oops。
ESTACK_NMI. EXCEPTION_STKSZ (PAGE_SIZE)。
用于不可屏蔽中断 (NMI)。
NMI 可以在任何时候传递,包括内核正处于切换栈的过程中。将 IST 用于 NMI 事件可以避免对内核栈的前一个状态做出假设。
ESTACK_DB. EXCEPTION_STKSZ (PAGE_SIZE)。
用于硬件调试中断(中断 1)和软件调试中断 (INT3)。
在调试内核时,调试中断(包括硬件和软件)可能随时发生。对这些中断使用 IST 可以避免对内核栈的前一个状态做出假设。
为了正确处理嵌套的 #DB,存在两个 DB 栈实例。在 #DB 入口处,#DB 的 IST 栈指针会切换到第二个实例,因此嵌套的 #DB 从一个干净的栈开始。嵌套的 #DB 会将 IST 栈指针切换到一个保护洞(guard hole)以捕获三重嵌套。
ESTACK_MCE. EXCEPTION_STKSZ (PAGE_SIZE)。
用于中断 18 - 机器检查异常 (#MC)。
MCE 可以在任何时候传递,包括内核正处于切换栈的过程中。将 IST 用于 MCE 事件可以避免对内核栈的前一个状态做出假设。
更多详情请参见 Intel IA32 或 AMD AMD64 架构手册。
6.2. 在 x86 上打印回溯¶
关于 x86 栈回溯中函数名前面带有“?”的问题一直不断出现,这里有一个深入的解释。如果读者仔细研究 print_context_stack() 以及 arch/x86/kernel/dumpstack.c 内部及周围的整个机制,将会大有裨益。
改编自 Ingo 的邮件,Message-ID: <20150521101614.GA10889@gmail.com>
我们总是从栈顶到栈底扫描整个内核栈,查找存储在内核栈上的返回地址 [1],并打印出任何“看起来像”内核代码 (text) 地址的内容。
如果它符合帧指针 (frame pointer) 链,我们就直接打印它而不带问号,因为我们知道它是真实回溯的一部分。
如果该地址不符合我们期望的帧指针链,我们仍然会打印它,但会带上一个“?”。这可能意味着两件事
要么该地址不属于调用链:它只是来自早期函数调用的内核栈上的陈旧值。这是常见的情况。
或者它是调用链的一部分,但帧指针在函数内部没有正确设置,因此我们无法识别它。
通过这种方式,无论帧指针是否正确设置,我们总是能打印出真正的调用链(外加一些条目)——但在大多数情况下,我们也能正确获取调用链。打印的条目严格按栈顺序排列,因此您也可以从中推断出更多信息。
这种方法最重要的特性是我们_永不_丢失信息:我们总是努力打印出栈上所有“看起来像”内核代码地址的_所有_地址,因此即使调试信息是错误的,我们依然能够打印出真正的调用链——只是问号比理想情况要多。