在Linux内核中,IST(Interrupt Stack Table,中断栈表)的配置是一个系统性的过程,核心涉及在任务状态段(TSS)中定义栈指针、在中断描述符表(IDT)中标记使用哪个IST栈,以及由内核在初始化时分配物理内存并建立映射。
IST栈的定义与分配
IST栈是每个CPU核心专用的,其大小和用途在内核启动时就已经预定好。根据x86_64架构的设计,一个CPU最多可以有7个IST条目。Linux内核将这些硬件条目分配给了特定的关键异常:
ESTACK_DF:用于#DF(双重故障),大小为PAGE_SIZE(4K)。ESTACK_NMI:用于NMI(不可屏蔽中断),大小为PAGE_SIZE。ESTACK_DB:用于#DB(调试异常),大小为PAGE_SIZE。但请注意,为了支持嵌套,内核实际上为它准备了两个物理栈实例。ESTACK_MCE:用于#MC(机器检查异常),大小为PAGE_SIZE。
此外,对于像AMD SEV-ES这类特殊功能,内核也会为其虚拟化异常(#VC)动态分配额外的IST栈。
如何在IDT中为中断"选择"IST栈
定义了栈还不够,内核需要在中断门描述符中明确指示,当某个中断发生时,CPU应该切换到哪个IST栈。
这个过程是通过IDT(中断描述符表)初始化代码完成的。在Linux内核源码中,通常使用宏来定义IDT条目,可以清晰地看到IST的指定。
1. 标准中断(不使用IST)
大部分硬件中断和普通异常,不启用IST功能,使用默认栈。代码中通常以INTG宏标记:
// 使用默认栈 (DEFAULT_STACK) INTG(X86_TRAP_DE, asm_exc_divide_error); // #DE 除法错误 INTG(X86_TRAP_GP, asm_exc_general_protection); // #GP 通用保护
这种配置下,bits.ist字段的值为DEFAULT_STACK(即0)。
2. 关键异常(使用IST)
对于NMI、#DF等关键异常,则会使用ISTG宏来指定IST索引:
// ISTG 宏的第三个参数指定了 IST 索引 // 例如 NMI (不可屏蔽中断) ISTG(X86_TRAP_NMI, asm_exc_nmi, IST_INDEX_NMI); // 例如 #DF (双重故障) ISTG(X86_TRAP_DF, asm_exc_double_fault, IST_INDEX_DF);
ISTG宏在定义中断门时,会根据传入的IST_INDEX_*值设置bits.ist字段。CPU在处理该中断时,看到这个非零的IST字段,就会自动从TSS中对应的IST条目加载新的栈指针。
实际配置过程:cpu_init()的初始化
实际的IST栈指针是在每个CPU初始化时,通过cpu_init()函数写入TSS的。
代码核心逻辑如下:
void cpu_init(void) { struct tss_struct *t = &per_cpu(cpu_tss_rw, cpu); // ... // 只有当 TSS 中的 IST 未被设置时才进行初始化 if (!t->x86_tss.ist[0]) { t->x86_tss.ist[DOUBLEFAULT_IST] = __this_cpu_ist_top_va(DF); t->x86_tss.ist[NMI_IST] = __this_cpu_ist_top_va(NMI); t->x86_tss.ist[DEBUG_IST] = __this_cpu_ist_top_va(DB); t->x86_tss.ist[MCE_IST] = __this_cpu_ist_top_va(MCE); // ... } }这段代码清晰展示了每个IST栈(如DOUBLEFAULT_IST)被赋予一个具体的栈顶虚拟地址,然后写入当前CPU的TSS结构中。