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 上的分离线程和中断栈一样,这为内核中断处理提供了更多空间,而无需增加每个线程栈的大小。
中断栈也用于处理软中断。
切换到内核中断栈是由软件基于每个 CPU 中断嵌套计数器完成的。 这是必需的,因为 x86-64 “IST” 硬件栈无法在没有竞争的情况下嵌套。
x86_64 还具有 i386 上不可用的功能,即能够自动切换到新栈以处理指定的事件,例如双重错误或 NMI,这使得在 x86_64 上处理这些不寻常的事件变得更加容易。 此功能称为中断栈表 (IST)。 每个 CPU 最多可以有 7 个 IST 条目。 IST 代码是任务状态段 (TSS) 的索引。 TSS 中的 IST 条目指向专用栈;每个栈的大小可能不同。
IST 由中断门描述符的 IST 字段中的非零值选择。 发生中断并且硬件加载此类描述符时,硬件会自动根据 IST 值设置新的栈指针,然后调用中断处理程序。 如果中断来自用户模式,则中断处理程序序言将切换回每个线程的栈。 如果软件希望允许嵌套的 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 栈指针切换到保护孔以捕获三重嵌套。
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 的邮件,消息 ID:<20150521101614.GA10889@gmail.com>
我们总是扫描整个内核栈,以查找存储在内核栈上的返回地址 [1],从栈顶到栈底,并打印出任何“看起来像”内核文本地址的内容。
如果它适合帧指针链,我们会打印它而没有问号,因为我们知道它是真实回溯的一部分。
如果地址不适合我们期望的帧指针链,我们仍然会打印它,但我们会打印一个“?”。 这可能意味着两件事
或者该地址不是调用链的一部分:它只是内核栈上的陈旧值,来自之前的函数调用。 这是常见的情况。
或者它是调用链的一部分,但帧指针未在函数中正确设置,因此我们无法识别它。
这样,无论帧指针是否正确设置,我们始终会打印出真实调用链(以及更多条目) - 但在大多数情况下,我们也会正确获得调用链。 打印的条目严格按照栈顺序排列,因此您也可以从中推断出更多信息。
此方法最重要的属性是我们 _never_ 丢失信息:我们始终努力打印栈上的 _all_ 地址,这些地址看起来像内核文本地址,因此如果调试信息不正确,我们仍然会打印出真实的调用链 - 只是问号比理想情况下更多。