ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux 内核揭秘:中断描述符表(IDT)——门描述符、错误代码与 x86_64 中断处理机制全解

Linux 内核揭秘:中断描述符表(IDT)——门描述符、错误代码与 x86_64 中断处理机制全解 【免费下载链接】linux-insides-zhLinux 内核揭秘项目地址https://gitcode.com/hust-open-atom-club/linux-insides-zh点击查看免费下载中断描述符表Interrupt Descriptor Table简称 IDT是 x86/x86_64 体系结构上连接 CPU 中断信号与内核处理程序的核心数据结构。本文以 linux-kernelstructure-1.md 为主体结合本仓库 中断与中断处理、内核初始化早期中断设置 等章节系统讲解中断与异常的来源与分类、向量号分配、两类错误代码格式、三种门描述符的位布局、Linux 内核中的gate_struct64表示以及 NMI、IST中断堆栈表等进阶机制。读完本文你将能够读懂 IDT 的每一个位域理解 Linux 内核如何在启动早期用set_intr_gate等 API 构建 IDT并掌握门描述符在真实内核代码中的落点。中断与异常的来源和分类处理器在处理正常指令流之外还会响应来自软件或硬件的事件。这些事件统称为中断/异常在 x86 体系结构上可以归纳为三个常见来源异常Exception——同步synchronous事件由 CPU 在执行指令时检测到错误条件产生例如除零、访问不存在的内存页软中断Software Interrupt——同步事件由软件通过INT n等指令主动触发常用于系统调用或断点调试外部中断External Interrupt——异步asynchronous事件由外部硬件键盘、网卡、时钟等通过中断线或 APIC 总线发出。异常与正在执行的指令严格同步因此可以被细分为三类其语义差别直接决定了被中断程序能否继续执行类型报告时机%rip指向被中断程序能否恢复故障Fault在导致异常的指令之前被准确报告指向故障指令本身可以指令被修正后重新执行陷阱Trap在导致异常的指令之后被准确报告指向故障指令可以终止Abort不明确的异常无法定位精确指令——通常不允许程序不能可靠地再次启动可屏蔽中断与不可屏蔽中断根据是否受标志寄存器影响中断又分为两类可屏蔽中断Maskable Interrupt只有在RFLAGS.IF 1时才会触发中断处理程序只要IF位被清零例如执行cli指令它们就持续处于等待处理状态。Linux 内核中native_irq_disable/native_irq_enable正是封装了cli/sti指令来操作IF位详见 深入 Linux 内核中的中断。不可屏蔽中断NMI不受RFLAGS.IF位影响无论IF为何值都会触发。一个 NMI 发生时会进一步屏蔽后续的其他 NMI直到执行IRET中断返回指令为止。这一嵌套屏蔽行为对内核的 NMI 处理栈设计影响深远后文专门展开。中断向量号与前 32 个保留向量每一个异常和中断都被分配了一个固定的向量标识号也称中断向量或简称向量。中断处理程序使用中断向量号在 IDT 中定位对应的门描述符从而找到相应的系统软件处理程序。向量号范围从0到255至多 256 个前 32 个向量0–31由处理器保留用于预定义的异常和中断条件向量32–255留给用户定义中断通常分配给外部 I/O 设备。Linux 内核在 arch/x86/include/asm/traps.h 中用枚举定义了前 32 个保留向量的助记符/* 中断/异常 */ enum { X86_TRAP_DE 0, /* 0, 除零错误 */ X86_TRAP_DB, /* 1, 调试 */ X86_TRAP_NMI, /* 2, 不可屏蔽中断 */ X86_TRAP_BP, /* 3, 断点 */ X86_TRAP_OF, /* 4, 溢出 */ X86_TRAP_BR, /* 5, 超出范围 */ X86_TRAP_UD, /* 6, 操作码无效 */ X86_TRAP_NM, /* 7, 设备不可用 */ X86_TRAP_DF, /* 8, 双精度浮点错误 */ X86_TRAP_OLD_MF, /* 9, 协处理器段溢出 */ X86_TRAP_TS, /* 10, 无效的 TSS */ X86_TRAP_NP, /* 11, 段不存在 */ X86_TRAP_SS, /* 12, 堆栈段故障 */ X86_TRAP_GP, /* 13, 一般保护故障 */ X86_TRAP_PF, /* 14, 页错误 */ X86_TRAP_SPURIOUS, /* 15, 伪中断 */ X86_TRAP_MF, /* 16, x87 浮点异常 */ X86_TRAP_AC, /* 17, 对齐检查 */ X86_TRAP_MC, /* 18, 机器检测 */ X86_TRAP_XF, /* 19, SIMD 单指令多数据结构浮点异常 */ X86_TRAP_IRET 32, /* 32, IRET 中断返回异常 */ };向量0–31与具体异常的对应关系助记符、类型、是否有错误码、产生源可以查阅本仓库 早期中断和异常处理 中的完整表格那篇文章还展示了 Linux 内核在启动早期如何通过for (i 0; i NUM_EXCEPTION_VECTORS; i)循环把early_idt_handler_array中的早期处理程序逐一填入 IDT。注意内核在写入中断门时会对向量号做合法性检查例如set_intr_gate中的BUG_ON((unsigned)n 0xFF)确保向量号不超过 255。错误代码Error Code两种位格式详解某些异常例如一般保护故障#GP、页错误#PF、无效 TSS#TS等会产生一个错误代码。在控制权转交给异常处理程序期间处理器会把错误代码压入堆栈供处理程序读取。错误代码存在两种格式选择子错误代码格式多数异常如段类异常使用此格式报告出错的选择子信息31 16 15 3 2 1 0 ------------------------------------------------------------------------------- | | | T | I | E | | Reserved | Selector Index | - | D | X | | | | I | T | T | -------------------------------------------------------------------------------各位含义如下EXT位 0——若为 1异常源在处理器外部若为 0异常源位于处理器内部IDT位 1——若为 1Selector Index字段引用**中断描述符表IDT**中的门描述符若为 0则引用 GDT 或 LDT 中的描述符具体由TI位指示TI位 2——若为 1Selector Index引用LDT中的描述符若为 0引用GDT中的描述符Selector Index位 3–15——指定索引为 GDT、LDT 或 IDT 的哪一个表项由IDT和TI位共同决定。页错误代码格式页错误#PF使用专门的格式用于报告错误访问的详细信息31 4 3 2 1 0 ------------------------------------------------------------------------------- | | | R | U | R | - | | Reserved | I/D | S | - | - | P | | | | V | S | W | - | -------------------------------------------------------------------------------各位含义如下I/D位 4——若为 1造成页错误的访问是取指instruction fetchRSV位 3——若为 1页错误是处理器从保留给分页表的区域中读取 1 的结果页表项中的保留位被置位U/S位 2——若为 0由管理员模式CPL 0, 1 或 2的访问导致页错误若为 1由用户模式CPL 3的访问导致R/W位 1——若为 0导致页错误的是内存读取若为 1导致页错误的是内存写入P位 0——若为 0页错误由不存在的页面引起若为 1页错误由违反页保护引起。中断控制传输三种门描述符IDT 的每一项称为一个门gate。与 GDT 不同GDT 的表项叫描述符且首项必须是 NULL 描述符IDT 的第一项可以直接是一个门这不是强制要求。IDT 可以包含以下三种门描述符中的任何一种任务门Task Gate——包含用于异常/中断处理程序任务的 TSS 段选择子中断门Interrupt Gate——包含段选择子和偏移量处理器据此把程序执行转移到中断处理程序陷阱门Trap Gate——包含段选择子和偏移量处理器据此把程序执行转移到异常处理程序。中断门与陷阱门的唯一区别在于处理器处理IF标志的方式经中断门进入处理程序时 CPU 会清除IF从而屏蔽后续中断直到iret返回时恢复陷阱门不修改IF。关于这一差异以及中断处理的三步基本骨架暂停当前任务 → 查找处理程序并转交控制权 → 恢复被中断进程详见 中断和中断处理 Part 1。门的一般格式x86_64 的 16 字节门在 64 位模式下IDT 的每一项是 16 字节x86 下为 8 字节。门的一般格式如下127 96 ------------------------------------------------------------------------------- | | | Reserved | | | -------------------------------------------------------------------------------- 95 64 ------------------------------------------------------------------------------- | | | Offset 63..32 | | | ------------------------------------------------------------------------------- 63 48 47 46 44 42 39 34 32 ------------------------------------------------------------------------------- | | | D | | | | | | | | Offset 31..16 | P | P | 0 |Type |0 0 0 | 0 | 0 | IST | | | | L | | | | | | | ------------------------------------------------------------------------------- 31 16 15 0 ------------------------------------------------------------------------------- | | | | Segment Selector | Offset 15..0 | | | | -------------------------------------------------------------------------------各字段说明Selector位 16–31——目标代码段的段选择子Offset位 0–15、32–47、64–95——处理程序入口点的 64 位偏移量被拆成三段存放DPL位 45–46——描述符权限级别Descriptor Privilege Level0 为最高特权级3 为用户级P位 47——段存在标志PresentIST位 32–34——中断堆栈表索引x86_64新机制用于无条件切换栈TYPE位 40–43——描述符类型可为 LDT 段描述符、TSS 描述符、调用门、中断门、陷阱门或任务门之一其余为保留位必须为 0。Linux 内核中的gate_struct64IDT描述符在 Linux 内核中由gate_struct64结构表示仅适用于x86_64它定义在 arch/x86/include/asm/desc_defs.hstruct gate_struct64 { u16 offset_low; u16 segment; unsigned ist : 3, zero0 : 5, type : 5, dpl : 2, p : 1; u16 offset_middle; u32 offset_high; u32 zero1; } __attribute__((packed));可以看到该结构把 64 位处理程序地址拆成offset_low、offset_middle、offset_high三段与上面门格式图中 Offset 的三个位置一一对应segment对应 Segment Selectorist、type、dpl、p是按位域紧凑排列的。__attribute__((packed))保证结构体严格按 16 字节布局不做任何填充对齐。内核通过typedef struct gate_struct64 gate_desc;将gate_desc定义为 IDT 表项类型整个 IDT 就是一个gate_desc idt_table[]数组。任务门描述符ldttss_desc64任务门描述符不包含IST字段其格式与中断/陷阱门不同Linux 内核用ldttss_desc64结构表示struct ldttss_desc64 { u16 limit0; u16 base0; unsigned base1 : 8, type : 5, dpl : 2, p : 1; unsigned limit1 : 4, zero0 : 3, g : 1, base2 : 8; u32 base3; u32 zero1; } __attribute__((packed));该结构同样定义在 arch/x86/include/asm/desc_defs.h其中limit0/limit1组合成 20 位段限长、g为粒度位、base0–base3组合成 64 位基地址——整体对应 LDT/TSS 描述符的经典布局在 IDT 中被任务门引用时指向承担异常/中断处理任务的 TSS。任务切换期间的异常在传统 x86 模式下任务切换在加载段选择子期间可能发生异常页错误也可能在访问 TSS 时出现。在这些情况下由硬件任务切换机构先从 TSS 加载新的任务状态然后再触发适当的异常处理。但需要特别注意在长模式long modex86_64下硬件任务切换机构被禁用因此在任务切换期间不会发生此类异常。这也是 x86_64 上任务门实际上不再被用于常规中断处理、异常处理主要依赖中断门/陷阱门配合 IST 机制的原因之一。中断描述符表的加载IDTR、lidt与早期空 IDTIDT 可以加载在线性地址空间的任意位置x86 上要求 8 字节对齐x86_64 上要求 16 字节对齐。IDT 的基址存储在一个特殊的寄存器——IDTR 中与之配套的两条指令是LIDT——加载 IDT 基址到 IDTRSIDT——把 IDTR 的内容读取/存储到指定操作数。在 x86 上 IDTR 是 48 位寄存器布局如下--------------------------------------------------------- | | | | Base address of the IDT | Limit of the IDT | | | | --------------------------------------------------------- 47 16 15 0Linux 内核启动早期尚不需要响应任何中断时用 NULL 门加载 IDT见 arch/x86/boot/pm.c 的setup_idtstatic void setup_idt(void) { static const struct gdt_ptr null_idt {0, 0}; asm volatile(lidtl %0 : : m (null_idt)); }其中gdt_ptr结构由u16 len和u32 ptr两个字段组成共 48 位与 IDTR 的Limit Base布局完全一致内核之所以复用gdt_ptr而没有单独定义idt_ptr正是因为这个结构与 IDTR 相同而只是名称不同没必要重复定义详细讨论见 深入 Linux 内核中的中断。注意这里 32 位保护模式入口使用的是lidtl32 位操作数形式。中断堆栈表IST与 x86_64 特殊栈ISTInterrupt Stack Table是x86_64引入的新机制用于替代传统 x86 架构在响应中断时自动切换栈帧的机制。与传统模式相比IST 在使能后可以无条件切换栈并且只对与特定中断关联的 IDT 条目生效——并非所有中断都使用 IST其他中断仍可沿用传统栈切换模式。IST 机制在任务状态段TSS中提供了 7 个 IST 指针每一个指针都可以被 IDT 中的中断门引用。当一个使用 IST 的中断发生时硬件把IST索引读入rsp自动切到对应的新栈然后继续压入旧的ss、rsp、rflags、cs、rip以及错误码。在 64 位模式下中断栈帧大小固定为 8 字节入栈后的栈布局如下--------------- | | | SS | 40 | RSP | 32 | RFLAGS | 24 | CS | 16 | RIP | 8 | Error code | 0 | | ---------------如果中断向量没有错误码内核会压入一个虚拟错误码以保证栈的一致性。随后处理器从门描述符加载段选择子到CS通过验证 GDT 中L位第 21 位确认目标代码是 64 位代码段再从门描述符加载偏移到rip作为处理程序入口处理程序结束后通过iret无条件弹出ss:rsp恢复被中断进程。per-cpu 中断栈与irq_stack_unionx86_64 上每个活动的线程拥有一个由THREAD_SIZE定义的大内核栈CONFIG_KASAN关闭时为 16KB开启时为 32KBKASAN 是运行时内存调试器。除线程栈外每个 CPU 还有 per-cpu 特殊栈其中最典型的是供外部中断使用的中断栈#define IRQ_STACK_ORDER (2 KASAN_STACK_ORDER) #define IRQ_STACK_SIZE (PAGE_SIZE IRQ_STACK_ORDER)即 16KB。per-cpu 中断栈在 arch/x86/include/asm/processor.h 中用联合体irq_stack_union描述union irq_stack_union { char irq_stack[IRQ_STACK_SIZE]; struct { char gs_base[40]; unsigned long stack_canary; }; };irq_stack——16KB 的中断栈数组gs_base——40 字节gs寄存器总是指向irq_stack_union底部。x86_64 中 per-cpu 变量与 stack canary 共享gs寄存器虽然段内存模式早已废弃但内核可通过特殊模块寄存器MSR为fs/gs设置基址MSR_GS_BASE编号0xc0000101配合wrmsr指令即可把gs指向 per-cpu 区域的开始stack_canary——栈保护者Stack canary用于验证中断栈是否被修改。GCC要求 canary 位于固定偏移x86_64 上gs值必须为 40x86 上为 20因此gs_base恰好是 40 字节。irq_stack_union是 percpu 区域的第一个数据System.map中__per_cpu_start与irq_stack_union同址。内核用DECLARE_PER_CPU_FIRST(union irq_stack_union, irq_stack_union)声明它并在 arch/x86/kernel/setup_percpu.c 的setup_per_cpu_areas中为每个可能 CPU 初始化irq_stack_ptrper_cpu(irq_stack_ptr, cpu) per_cpu(irq_stack_union.irq_stack, cpu) IRQ_STACK_SIZE - 64;irq_stack_ptr指向中断栈顶减去 64 字节的位置另有 per-cpu 变量irq_count用于检查 CPU 是否已在中断栈上。栈 canary 的初始化由boot_init_stack_canary完成它先用BUILD_BUG_ON(offsetof(union irq_stack_union, stack_canary) ! 40)编译期校验偏移再用随机数和时戳计数器TSC合成 canary 值并写入irq_stack_union.stack_canary详见 深入 Linux 内核中的中断。7 个 IST 入口与set_intr_gate_ist每个 CPU 最多可有 7 个 IST per-cpu 入口其中一部分包括#define DOUBLEFAULT_STACK 1 #define NMI_STACK 2 #define DEBUG_STACK 3 #define MCE_STACK 4所有需要 IST 切换新栈的中断门描述符都由set_intr_gate_ist函数初始化。例如 arch/x86/kernel/traps.c 的early_trap_init中void __init early_trap_init(void) { set_intr_gate_ist(X86_TRAP_DB, debug, DEBUG_STACK); set_system_intr_gate_ist(X86_TRAP_BP, int3, DEBUG_STACK); load_idt(idt_descr); }这里为调试异常#DB和断点#BP设置了带DEBUG_STACKIST 索引的中断门set_intr_gate_ist的 DPL 为 0内核态而set_system_intr_gate_ist的 DPL 为 0x3用户态也可通过int3触发断点。load_idt最终只是执行asm volatile(lidt %0::m (*dtr))指令其中idt_descr { NR_VECTORS * 16 - 1, (unsigned long) idt_table }。异常/中断的汇编入口点则定义在 arch/x86/entry/entry_64.S例如idtentry double_fault do_double_fault has_error_code1 paranoid2 read_cr21而nmi、double_fault等处理程序在 arch/x86/include/asm/traps.h 中声明为asmlinkage void nmi(void);、asmlinkage void double_fault(void);。不可屏蔽中断NMI嵌套屏蔽与处理机制NMI 是不能通过标准屏蔽技术忽略的硬件中断产生方式有两种外部硬件向 CPU 的 NMI 引脚置位或处理器在系统总线/APIC 串行总线上收到投递模式为NMI的消息。NMI 的中断向量号为2内核在trap_init中用set_intr_gate_ist(X86_TRAP_NMI, nmi, NMI_STACK);为其设置带NMI_STACKIST 索引的中断门。NMI 处理面临一个特殊的栈问题CPU 在第一个 NMI 完成前不会执行另一个 NMI而 NMI 处理程序以iret结束。如果 NMI 处理期间触发了页错误或断点等同样以iret返回的异常CPU 会离开 NMI 上下文从而允许新的 NMI 抢占正在运行的 NMI 处理程序——后到的 NMI 会覆盖被抢占 NMI 的栈顶破坏栈数据。因此内核在 NMI 入口处先在栈上分配一个临时变量位来标记当前是否正在执行 NMI进入时压入1若是嵌套 NMIcs为内核段且标记已置位则走特殊的嵌套处理路径否则在first_nmi标签处建立新的标记。完整的nmi汇编实现PARAVIRT_ADJUST_EXCEPTION_FRAME、pushq %rdx、cmpl $__KERNEL_CS, 16(%rsp)、jne first_nmi等见 处理不可屏蔽中断。Linux 内核的 IDT 编程 API理解了 IDT 的硬件格式后再看内核如何编程式地构建它。核心 API 集中在 arch/x86/include/asm/desc.h#define set_intr_gate(n, addr) \ do { \ BUG_ON((unsigned)n 0xFF); \ _set_gate(n, GATE_INTERRUPT, (void *)addr, 0, 0, \ __KERNEL_CS); \ _trace_set_gate(n, GATE_INTERRUPT, (void *)trace_##addr, \ 0, 0, __KERNEL_CS); \ } while (0)BUG_ON先校验向量号 ≤ 255随后_set_gate通过pack_gate填充gate_desc结构并写入idt_tabletrace_*系列则写入trace_idt_table供 tracepoint 使用static inline void pack_gate(gate_desc *gate, unsigned type, unsigned long func, unsigned dpl, unsigned ist, unsigned seg) { gate-offset_low PTR_LOW(func); gate-segment __KERNEL_CS; gate-ist ist; gate-p 1; gate-dpl dpl; gate-zero0 0; gate-zero1 0; gate-type type; gate-offset_middle PTR_MIDDLE(func); gate-offset_high PTR_HIGH(func); }其中PTR_LOW、PTR_MIDDLE、PTR_HIGH三个宏分别截取处理程序地址的低 16 位、中间 16 位和高 32 位正好填充gate_struct64的三段偏移字段segment固定为内核代码段__KERNEL_CStype可取GATE_INTERRUPT、GATE_TRAP、GATE_CALL、GATE_TASK之一。最终native_write_idt_entry通过memcpy(idt[entry], gate, sizeof(*gate))把门复制进 IDT 数组。有了这些 API异常处理程序的真实实现才有落点。以 异常处理的实现 为例Linux 用DO_ERROR宏批量生成divide_error、overflow、invalid_op、alignment_check等处理函数DO_ERROR(X86_TRAP_DE, SIGFPE, divide error, divide_error) DO_ERROR(X86_TRAP_OF, SIGSEGV, overflow, overflow) DO_ERROR(X86_TRAP_UD, SIGILL, invalid opcode, invalid_op) DO_ERROR(X86_TRAP_TS, SIGSEGV, invalid TSS, invalid_TSS)宏展开后每个处理函数形如dotraplinkage void do_##name(struct pt_regs *regs, long error_code)内部统一走do_error_trap→do_trap→ 向被中断进程发送信号如除零发SIGFPE的链路若异常发生在内核态且无法修复则调用die打印栈与寄存器信息并触发 kernel oops。小结中断描述符表是 x86/x86_64 中断处理的基础设施处理器用向量号索引 IDT门描述符中的段选择子与 64 位偏移指向处理程序入口错误代码向处理程序提供出错细节IST 机制则为 NMI、双重错误等关键异常提供独立栈。本文从硬件位的角度完整剖析了 IDT 的门格式、两类错误代码、三种门类型并延伸到 Linux 内核的gate_struct64、pack_gate/set_intr_gateAPI、早期 IDT 加载、per-cpu 中断栈与 NMI 嵌套保护。与之配套的实操细节还可继续阅读本仓库的 中断和中断处理 Part 1IDT 加载与 IST 概念、早期中断和异常处理早期 IDT 与缺页处理程序、深入 Linux 内核中的中断栈 canary 与early_trap_init以及 处理不可屏蔽中断NMI 完整实现。赞分享【免费下载链接】linux-insides-zhLinux 内核揭秘项目地址https://gitcode.com/hust-open-atom-club/linux-insides-zh点击查看免费下载相关推荐Linux内核揭秘文件描述符管理的终极指南Linux内核揭秘文件描述符管理的终极指南 在Linux系统中文件描述符是连接用户程序与内核资源的重要桥梁。无论是打开文件、创建管道还是建立网络连接都离不文档教程操作系统Linux 内核揭秘中断与中断处理入门Part 1——从硬件事件到 IDT 与中断栈Linux 内核揭秘中断与中断处理入门Part 1——从硬件事件到 IDT 与中断栈 导读 中断interrupt是 Linux 内核与外部世界交互的Linux generic IRQ 框架详解中断描述符、流控处理器与 irq_chip 三层抽象Linux generic IRQ 框架详解中断描述符、流控处理器与 irq_chip 三层抽象 导读 本文以内核文档 Documentation/core操作系统内核驱动驱动开发虚拟化嵌入式网络存储上一篇【免费下载】 3dsconv一款实用的Nintendo 3DS游戏文件转换工具下一篇深入解析 pi coding agent 会话流与持久化格式Sandcastle 中 pi resume 支持的样本实证创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表