diff --git a/Initialization/linux-initialization-2.md b/Initialization/linux-initialization-2.md index 41b738c..0bc6002 100644 --- a/Initialization/linux-initialization-2.md +++ b/Initialization/linux-initialization-2.md @@ -4,9 +4,9 @@ 初期中断和异常处理 -------------------------------------------------------------------------------- -在上一个 [部分](http://xinqiu.gitbooks.io/linux-insides-cn/content/Initialization/linux-initialization-1.html) 我们谈到了初期中断初始化。目前我们已经处于解压缩后的Linux内核中了,还有了用于初期启动的基本的[分页](https://en.wikipedia.org/wiki/Page_table)机制。我们的目标是在内核的主体代码执行前做好准备工作。 +在上一个 [部分](http://xinqiu.gitbooks.io/linux-insides-cn/content/Initialization/linux-initialization-1.html) 我们谈到了初期中断初始化。目前我们已经处于解压缩后的Linux内核中了,还有了用于初期启动的基本的 [分页](https://en.wikipedia.org/wiki/Page_table) 机制。我们的目标是在内核的主体代码执行前做好准备工作。 -我们已经在[本章](https://xinqiu.gitbooks.io/linux-insides-cn/content/Initialization/index.html)的[第一部分](http://xinqiu.gitbooks.io/linux-insides-cn/content/Initialization/linux-initialization-1.html)做了一些工作,在这一部分中我们会继续分析关于中断和异常处理部分的代码。 +我们已经在 [本章](https://xinqiu.gitbooks.io/linux-insides-cn/content/Initialization/index.html) 的 [第一部分](http://xinqiu.gitbooks.io/linux-insides-cn/content/Initialization/linux-initialization-1.html) 做了一些工作,在这一部分中我们会继续分析关于中断和异常处理部分的代码。 我们在上一部分谈到了下面这个循环: @@ -20,19 +20,19 @@ for (i = 0; i < NUM_EXCEPTION_VECTORS; i++) 理论 -------------------------------------------------------------------------------- -中断是一种由软件或硬件产生的、向CPU发出的事件。例如,如果用户按下了键盘上的一个按键时,就会产生中断。此时CPU将会暂停当前的任务,并且将控制流转到特殊的程序中——[中断处理程序(Interrupt Handler)](https://en.wikipedia.org/wiki/Interrupt_handler)。一个中断处理程序会对中断进行处理,然后将控制权交还给之前暂停的任务中。中断分为三类: +中断是一种由软件或硬件产生的、向CPU发出的事件。例如,如果用户按下了键盘上的一个按键时,就会产生中断。此时CPU将会暂停当前的任务,并且将控制流转到特殊的程序中—— [中断处理程序(Interrupt Handler)](https://en.wikipedia.org/wiki/Interrupt_handler)。一个中断处理程序会对中断进行处理,然后将控制权交还给之前暂停的任务中。中断分为三类: * 软件中断 - 当一个软件可以向CPU发出信号,表明它需要系统内核的相关功能时产生。这些中断通常用于系统调用; * 硬件中断 - 当一个硬件有任何事件发生时产生,例如键盘的按键被按下; * 异常 - 当CPU检测到错误时产生,例如发生了除零错误或者访问了一个不存在的内存页。 -每一个中断和异常都可以由一个数来表示,这个数叫做`向量号`,它可以取从 `0` 到 `255` 中的任何一个数。通常在实践中前 `32` 个向量号用来表示异常,`32` 到 `255` 用来表示用户定义的中断。可以看到在上面的代码中,`NUM_EXCEPTION_VECTORS` 就定义为: +每一个中断和异常都可以由一个数来表示,这个数叫做 `向量号` ,它可以取从 `0` 到 `255` 中的任何一个数。通常在实践中前 `32` 个向量号用来表示异常,`32` 到 `255` 用来表示用户定义的中断。可以看到在上面的代码中,`NUM_EXCEPTION_VECTORS` 就定义为: ```C #define NUM_EXCEPTION_VECTORS 32 ``` -CPU会从[APIC](http://en.wikipedia.org/wiki/Advanced_Programmable_Interrupt_Controller)或者CPU引脚接收中断,并使用中断向量号作为 `中断描述符表` 的索引。下面的表中列出了 `0-31` 号异常: +CPU会从 [APIC](http://en.wikipedia.org/wiki/Advanced_Programmable_Interrupt_Controller) 或者 CPU 引脚接收中断,并使用中断向量号作为 `中断描述符表` 的索引。下面的表中列出了 `0-31` 号异常: ``` ---------------------------------------------------------------------------------------------- @@ -84,9 +84,9 @@ CPU会从[APIC](http://en.wikipedia.org/wiki/Advanced_Programmable_Interrupt_Con ---------------------------------------------------------------------------------------------- ``` -为了能够对中断进行处理,CPU使用了一种特殊的结构 - 中断描述符表(IDT)。IDT是一个由描述符组成的数组,其中每个描述符都为8个字节,与全局描述附表一致;不过不同的是,我们把IDT中的每一项叫做`门(gate)`。为了获得某一项描述符的起始地址,CPU会把向量号乘以8,在64位模式中则会乘以16。在前面我们已经见过,CPU使用一个特殊的 `GDTR` 寄存器来存放全局描述符表的地址,中断描述符表也有一个类似的寄存器 `IDTR`,同时还有用于将基地址加载入这个寄存器的指令 `lidt`。 +为了能够对中断进行处理,CPU使用了一种特殊的结构 - 中断描述符表(IDT)。IDT 是一个由描述符组成的数组,其中每个描述符都为8个字节,与全局描述附表一致;不过不同的是,我们把IDT中的每一项叫做 `门(gate)` 。为了获得某一项描述符的起始地址,CPU 会把向量号乘以8,在64位模式中则会乘以16。在前面我们已经见过,CPU使用一个特殊的 `GDTR` 寄存器来存放全局描述符表的地址,中断描述符表也有一个类似的寄存器 `IDTR` ,同时还有用于将基地址加载入这个寄存器的指令 `lidt` 。 -64位模式下IDT的每一项的结构如下: +64位模式下 IDT 的每一项的结构如下: ``` 127 96 @@ -129,9 +129,9 @@ CPU会从[APIC](http://en.wikipedia.org/wiki/Advanced_Programmable_Interrupt_Con * 中断描述符 * 陷阱描述符 -中断和陷阱描述符包含了一个指向中断处理程序的远(far)指针,二者唯一的不同在于CPU处理 `IF` 标志的方式。如果是由中断门进入中断处理程序的,CPU会清除 `IF` 标志位,这样当当前中断处理程序执行时,CPU不会对其他的中断进行处理;只有当当前的中断处理程序返回时,CPU 才在 `iret` 指令执行时重新设置 `IF` 标志位。 +中断和陷阱描述符包含了一个指向中断处理程序的远 (far) 指针,二者唯一的不同在于CPU处理 `IF` 标志的方式。如果是由中断门进入中断处理程序的,CPU 会清除 `IF` 标志位,这样当当前中断处理程序执行时,CPU 不会对其他的中断进行处理;只有当当前的中断处理程序返回时,CPU 才在 `iret` 指令执行时重新设置 `IF` 标志位。 -中断门的其他位为保留位,必须为0。下面我们来看一下CPU是如何处理中断的: +中断门的其他位为保留位,必须为0。下面我们来看一下 CPU 是如何处理中断的: * CPU 会在栈上保存标志寄存器、`cs`段寄存器和程序计数器IP; * 如果中断是由错误码引起的(比如 `#PF`), CPU会在栈上保存错误码; @@ -149,7 +149,7 @@ for (i = 0; i < NUM_EXCEPTION_VECTORS; i++) set_intr_gate(i, early_idt_handler_array[i]); ``` -这里循环内部调用了 `set_intr_gate`,它接受两个参数: +这里循环内部调用了 `set_intr_gate` ,它接受两个参数: * 中断号,即 `向量号`; * 中断处理程序的地址。 @@ -165,7 +165,7 @@ extern const char early_idt_handler_array[NUM_EXCEPTION_VECTORS][EARLY_IDT_HANDL `early_idt_handler_array` 是一个大小为 `288` 字节的数组,每一项为 `9` 个字节,其中2个字节的备用指令用于向栈中压入默认错误码(如果异常本身没有提供错误码的话),2个字节的指令用于向栈中压入向量号,剩余5个字节用于跳转到异常处理程序。 -在上面的代码中,我们只通过一个循环向 `IDT` 中填入了前32项内容,这是因为在整个初期设置阶段,中断是禁用的。`early_idt_handler_array` 数组中的每一项指向的都是同一个通用中断处理程序,定义在 [arch/x86/kernel/head_64.S](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/head_64.S)。我们先暂时跳过这个数组的内容,看一下 `set_intr_gate` 的定义。 +在上面的代码中,我们只通过一个循环向 `IDT` 中填入了前32项内容,这是因为在整个初期设置阶段,中断是禁用的。`early_idt_handler_array` 数组中的每一项指向的都是同一个通用中断处理程序,定义在 [arch/x86/kernel/head_64.S](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/head_64.S) 。我们先暂时跳过这个数组的内容,看一下 `set_intr_gate` 的定义。 `set_intr_gate` 宏定义在 [arch/x86/include/asm/desc.h](https://github.com/torvalds/linux/blob/master/arch/x86/include/asm/desc.h): @@ -219,7 +219,7 @@ static inline void pack_gate(gate_desc *gate, unsigned type, unsigned long func, #define PTR_HIGH(x) ((unsigned long long)(x) >> 32) ``` -调用 `PTR_LOW` 可以得到x的低 `2` 个字节,调用 `PTR_MIDDLE` 可以得到x的中间 `2` 个字节,调用 `PTR_HIGH` 则能够得到x的高 `4` 个字节。接下来我们来位中断处理程序设置段选择子,即内核代码段 `__KERNEL_CS`。然后将 `Interrupt Stack Table` 和 `描述符特权等级` (最高特权等级)设置为0,以及在最后设置 `GAT_INTERRUPT` 类型。 +调用 `PTR_LOW` 可以得到 x 的低 `2` 个字节,调用 `PTR_MIDDLE` 可以得到 x 的中间 `2` 个字节,调用 `PTR_HIGH` 则能够得到 x 的高 `4` 个字节。接下来我们来位中断处理程序设置段选择子,即内核代码段 `__KERNEL_CS`。然后将 `Interrupt Stack Table` 和 `描述符特权等级` (最高特权等级)设置为0,以及在最后设置 `GAT_INTERRUPT` 类型。 现在我们已经设置好了IDT中的一项,那么通过调用 `native_write_idt_entry` 函数来把复制到 `IDT`: @@ -248,14 +248,14 @@ struct desc_ptr idt_descr = { NR_VECTORS * 16 - 1, (unsigned long) idt_table }; asm volatile("lidt %0"::"m" (*dtr)); ``` -你可能已经注意到了,在代码中还有对 `_trace_*` 函数的调用。这些函数会用跟 `_set_gate` 同样的方法对 `IDT` 门进行设置,但仅有一处不同:这些函数并不设置 `idt_table`,而是 `trace_idt_table`,用于设置追踪点(tracepoint,我们将会在其他章节介绍这一部分)。 +你可能已经注意到了,在代码中还有对 `_trace_*` 函数的调用。这些函数会用跟 `_set_gate` 同样的方法对 `IDT` 门进行设置,但仅有一处不同:这些函数并不设置 `idt_table` ,而是 `trace_idt_table` ,用于设置追踪点(tracepoint,我们将会在其他章节介绍这一部分)。 -好了,至此我们已经了解到,通过设置并加载 `中断描述符表`,能够让CPU在发生中断时做出相应的动作。下面让我们来看一下如何编写中断处理程序。 +好了,至此我们已经了解到,通过设置并加载 `中断描述符表` ,能够让CPU在发生中断时做出相应的动作。下面让我们来看一下如何编写中断处理程序。 初期中断处理程序 -------------------------------------------------------------------------------- -在上面的代码中,我们用 `early_idt_handler_array` 的地址来填充了 `IDT`,这个 `early_idt_handler_array` 定义在 [arch/x86/kernel/head_64.S](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/head_64.S): +在上面的代码中,我们用 `early_idt_handler_array` 的地址来填充了 `IDT` ,这个 `early_idt_handler_array` 定义在 [arch/x86/kernel/head_64.S](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/head_64.S): ```assembly .globl early_idt_handler_array @@ -272,7 +272,7 @@ early_idt_handlers: .endr ``` -这段代码自动生成为前 `32` 个异常生成了中断处理程序。首先,为了统一栈的布局,如果一个异常没有返回错误码,那么我们就手动在栈中压入一个 `0`。然后再在栈中压入中断向量号,最后跳转至通用的中断处理程序 `early_idt_handler_common`。我们可以通过 `objdump` 命令的输出一探究竟: +这段代码自动生成为前 `32` 个异常生成了中断处理程序。首先,为了统一栈的布局,如果一个异常没有返回错误码,那么我们就手动在栈中压入一个 `0`。然后再在栈中压入中断向量号,最后跳转至通用的中断处理程序 `early_idt_handler_common` 。我们可以通过 `objdump` 命令的输出一探究竟: ``` $ objdump -D vmlinux @@ -293,7 +293,7 @@ ffffffff81fe5014: 6a 02 pushq $0x2 ... ``` -由于在中断发生时,CPU会在栈上压入标志寄存器、`CS` 段寄存器和 `RIP` 寄存器的内容。因此在 `early_idt_handler` 执行前,栈的布局如下: +由于在中断发生时,CPU 会在栈上压入标志寄存器、`CS` 段寄存器和 `RIP` 寄存器的内容。因此在 `early_idt_handler` 执行前,栈的布局如下: ``` |--------------------| @@ -304,7 +304,7 @@ ffffffff81fe5014: 6a 02 pushq $0x2 |--------------------| ``` -下面我们来看一下 `early_idt_handler_common` 的实现。它也定义在 [arch/x86/kernel/head_64.S](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/head_64.S#L343) 文件中。首先它会检查当前中断是否为 [不可屏蔽中断(NMI)](http://en.wikipedia.org/wiki/Non-maskable_interrupt),如果是则简单地忽略它们: +下面我们来看一下 `early_idt_handler_common` 的实现。它也定义在 [arch/x86/kernel/head_64.S](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/head_64.S#L343) 文件中。首先它会检查当前中断是否为 [不可屏蔽中断(NMI)](http://en.wikipedia.org/wiki/Non-maskable_interrupt),如果是则简单地忽略它们: ```assembly cmpl $2,(%rsp) @@ -319,9 +319,9 @@ is_nmi: INTERRUPT_RETURN ``` -这段程序首先从栈顶弹出错误码和中断向量号,然后通过调用 `INTERRUPT_RETURN`,即 `iretq` 指令直接返回。 +这段程序首先从栈顶弹出错误码和中断向量号,然后通过调用 `INTERRUPT_RETURN` ,即 `iretq` 指令直接返回。 -如果当前中断不是 `NMI`,则首先检查 `early_recursion_flag` 以避免在 `early_idt_handler_common` 程序中递归地产生中断。如果一切都没问题,就先在栈上保存通用寄存器,为了防止中断返回时寄存器的内容错乱: +如果当前中断不是 `NMI` ,则首先检查 `early_recursion_flag` 以避免在 `early_idt_handler_common` 程序中递归地产生中断。如果一切都没问题,就先在栈上保存通用寄存器,为了防止中断返回时寄存器的内容错乱: ```assembly pushq %rax @@ -342,7 +342,7 @@ is_nmi: jne 11f ``` -段选择子必须为内核代码段,如果不是则跳转到标签 `11`,输出 `PANIC` 信息并打印栈的内容。然后我们来检查向量号,如果是 `#PF` 即 [缺页中断(Page Fault)](https://en.wikipedia.org/wiki/Page_fault),那么就把 `cr2` 寄存器中的值赋值给 `rdi`,然后调用 `early_make_pgtable` (详见后文): +段选择子必须为内核代码段,如果不是则跳转到标签 `11` ,输出 `PANIC` 信息并打印栈的内容。然后我们来检查向量号,如果是 `#PF` 即 [缺页中断(Page Fault)](https://en.wikipedia.org/wiki/Page_fault),那么就把 `cr2` 寄存器中的值赋值给 `rdi` ,然后调用 `early_make_pgtable` (详见后文): ```assembly cmpl $14,72(%rsp) @@ -353,7 +353,7 @@ is_nmi: jz 20f ``` -如果向量号不是 `#PF`,那么就恢复通用寄存器: +如果向量号不是 `#PF` ,那么就恢复通用寄存器: ```assembly popq %r11 popq %r10 @@ -373,7 +373,7 @@ is_nmi: 缺页中断处理程序 -------------------------------------------------------------------------------- -在上一节中我们第一次见到了初期中断处理程序,它检查了缺页中断的中断号,并调用了 `early_make_pgtable`来建立新的页表。在这里我们需要提供 `#PF` 中断处理程序,以便为之后将内核加载至 `4G` 地址以上,并且能访问位于4G以上的 `boot_params` 结构体。 +在上一节中我们第一次见到了初期中断处理程序,它检查了缺页中断的中断号,并调用了 `early_make_pgtable` 来建立新的页表。在这里我们需要提供 `#PF` 中断处理程序,以便为之后将内核加载至 `4G` 地址以上,并且能访问位于4G以上的 `boot_params` 结构体。 `early_make_pgtable` 的实现在 [arch/x86/kernel/head64.c](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/head64.c),它接受一个参数:从 `cr2` 寄存器得到的地址,这个地址引发了内存中断。下面让我们来看一下: @@ -397,7 +397,7 @@ int __init early_make_pgtable(unsigned long address) typedef unsigned long pgdval_t; ``` -此外,我们还会遇见 `*_t` (不带val)的类型,比如 `pgd_t`……这些类型都定义在 [arch/x86/include/asm/pgtable_types.h](https://github.com/torvalds/linux/blob/master/arch/x86/include/asm/pgtable_types.h),形式如下: +此外,我们还会遇见 `*_t` (不带val)的类型,比如 `pgd_t` ……这些类型都定义在 [arch/x86/include/asm/pgtable_types.h](https://github.com/torvalds/linux/blob/master/arch/x86/include/asm/pgtable_types.h),形式如下: ```C typedef struct { pgdval_t pgd; } pgd_t; @@ -418,7 +418,7 @@ pgd_p = &early_level4_pgt[pgd_index(address)].pgd; pgd = *pgd_p; ``` -接下来我们检查一下 `pgd`,如果它包含了正确的全局页表项的话,我们就把这一项的物理地址处理后赋值给 `pud_p`: +接下来我们检查一下 `pgd` ,如果它包含了正确的全局页表项的话,我们就把这一项的物理地址处理后赋值给 `pud_p` : ```C @@ -445,7 +445,7 @@ pud_p = (pudval_t *)((pgd & PTE_PFN_MASK) + __START_KERNEL_map - phys_base); 它是一个46bit大小的页帧屏蔽值。 -如果 `pgd` 没有包含有效的地址,我们就检查 `next_early_pgt` 与 `EARLY_DYNAMIC_PAGE_TABLES`(即 `64`)的大小。`EARLY_DYNAMIC_PAGE_TABLES` 它是一个固定大小的缓冲区,用来在需要的时候建立新的页表。如果 `next_early_pgt` 比 `EARLY_DYNAMIC_PAGE_TABLES` 大,我们就用一个上层页目录指针指向当前的动态页表,并将它的物理地址与 `_KERPG_TABLE` 访问权限一起写入全局页目录表: +如果 `pgd` 没有包含有效的地址,我们就检查 `next_early_pgt` 与 `EARLY_DYNAMIC_PAGE_TABLES`(即 `64` )的大小。`EARLY_DYNAMIC_PAGE_TABLES` 它是一个固定大小的缓冲区,用来在需要的时候建立新的页表。如果 `next_early_pgt` 比 `EARLY_DYNAMIC_PAGE_TABLES` 大,我们就用一个上层页目录指针指向当前的动态页表,并将它的物理地址与 `_KERPG_TABLE` 访问权限一起写入全局页目录表: ```C if (next_early_pgt >= EARLY_DYNAMIC_PAGE_TABLES) {