Files
linux-insides-zh/Initialization/linux-initialization-2.md
2024-05-05 10:11:17 +08:00

497 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
内核初始化 第二部分
================================================================================
初期中断和异常处理
--------------------------------------------------------------------------------
在上一个 [部分](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) 做了一些工作,在这一部分中我们会继续分析关于中断和异常处理部分的代码。
我们在上一部分谈到了下面这个循环:
```C
for (i = 0; i < NUM_EXCEPTION_VECTORS; i++)
set_intr_gate(i, early_idt_handler_array[i]);
```
这段代码位于 [arch/x86/kernel/head64.c](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/head64.c)。在分析这段代码之前,我们先来了解一些关于中断和中断处理程序的知识。
理论
--------------------------------------------------------------------------------
中断是一种由软件或硬件产生的、向CPU发出的事件。例如如果用户按下了键盘上的一个按键时就会产生中断。此时CPU将会暂停当前的任务并且将控制流转到特殊的程序中—— [中断处理程序(Interrupt Handler)](https://en.wikipedia.org/wiki/Interrupt_handler)。一个中断处理程序会对中断进行处理,然后将控制权交还给之前暂停的任务中。中断分为三类:
* 软件中断 - 当一个软件可以向CPU发出信号表明它需要系统内核的相关功能时产生。这些中断通常用于系统调用
* 硬件中断 - 当一个硬件有任何事件发生时产生,例如键盘的按键被按下;
* 异常 - 当CPU检测到错误时产生例如发生了除零错误或者访问了一个不存在的内存页。
每一个中断和异常都可以由一个数来表示,这个数叫做 `向量号` ,它可以取从 `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` 号异常:
```
----------------------------------------------------------------------------------------------
|Vector|Mnemonic|Description |Type |Error Code|Source |
----------------------------------------------------------------------------------------------
|0 | #DE |Divide Error |Fault|NO |DIV and IDIV |
|---------------------------------------------------------------------------------------------
|1 | #DB |Reserved |F/T |NO | |
|---------------------------------------------------------------------------------------------
|2 | --- |NMI |INT |NO |external NMI |
|---------------------------------------------------------------------------------------------
|3 | #BP |Breakpoint |Trap |NO |INT 3 |
|---------------------------------------------------------------------------------------------
|4 | #OF |Overflow |Trap |NO |INTO instruction |
|---------------------------------------------------------------------------------------------
|5 | #BR |Bound Range Exceeded|Fault|NO |BOUND instruction |
|---------------------------------------------------------------------------------------------
|6 | #UD |Invalid Opcode |Fault|NO |UD2 instruction |
|---------------------------------------------------------------------------------------------
|7 | #NM |Device Not Available|Fault|NO |Floating point or [F]WAIT |
|---------------------------------------------------------------------------------------------
|8 | #DF |Double Fault |Abort|YES |Ant instrctions which can generate NMI|
|---------------------------------------------------------------------------------------------
|9 | --- |Reserved |Fault|NO | |
|---------------------------------------------------------------------------------------------
|10 | #TS |Invalid TSS |Fault|YES |Task switch or TSS access |
|---------------------------------------------------------------------------------------------
|11 | #NP |Segment Not Present |Fault|NO |Accessing segment register |
|---------------------------------------------------------------------------------------------
|12 | #SS |Stack-Segment Fault |Fault|YES |Stack operations |
|---------------------------------------------------------------------------------------------
|13 | #GP |General Protection |Fault|YES |Memory reference |
|---------------------------------------------------------------------------------------------
|14 | #PF |Page fault |Fault|YES |Memory reference |
|---------------------------------------------------------------------------------------------
|15 | --- |Reserved | |NO | |
|---------------------------------------------------------------------------------------------
|16 | #MF |x87 FPU fp error |Fault|NO |Floating point or [F]Wait |
|---------------------------------------------------------------------------------------------
|17 | #AC |Alignment Check |Fault|YES |Data reference |
|---------------------------------------------------------------------------------------------
|18 | #MC |Machine Check |Abort|NO | |
|---------------------------------------------------------------------------------------------
|19 | #XM |SIMD fp exception |Fault|NO |SSE[2,3] instructions |
|---------------------------------------------------------------------------------------------
|20 | #VE |Virtualization exc. |Fault|NO |EPT violations |
|---------------------------------------------------------------------------------------------
|21-31 | --- |Reserved |INT |NO |External interrupts |
----------------------------------------------------------------------------------------------
```
为了能够对中断进行处理CPU使用了一种特殊的结构 - 中断描述符表IDT。IDT 是一个由描述符组成的数组其中每个描述符都为8个字节与全局描述附表一致不过不同的是我们把IDT中的每一项叫做 `门(gate)` 。为了获得某一项描述符的起始地址CPU 会把向量号乘以8在64位模式中则会乘以16。在前面我们已经见过CPU使用一个特殊的 `GDTR` 寄存器来存放全局描述符表的地址,中断描述符表也有一个类似的寄存器 `IDTR` ,同时还有用于将基地址加载入这个寄存器的指令 `lidt`
64位模式下 IDT 的每一项的结构如下:
```
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 15 16 0
--------------------------------------------------------------------------------
| | |
| Segment Selector | Offset 15..0 |
| | |
--------------------------------------------------------------------------------
```
其中:
* `Offset` - 代表了到中断处理程序入口点的偏移;
* `DPL` - 描述符特权级别;
* `P` - Segment Present 标志;
* `Segment selector` - 在GDT或LDT中的代码段选择子
* `IST` - 用来为中断处理提供一个新的栈。
最后的 `Type` 域描述了这一项的类型,中断处理程序共分为三种:
* 任务描述符
* 中断描述符
* 陷阱描述符
中断和陷阱描述符包含了一个指向中断处理程序的远 (far) 指针二者唯一的不同在于CPU处理 `IF` 标志的方式。如果是由中断门进入中断处理程序的CPU 会清除 `IF` 标志位这样当当前中断处理程序执行时CPU 不会对其他的中断进行处理只有当当前的中断处理程序返回时CPU 才在 `iret` 指令执行时重新设置 `IF` 标志位。
中断门的其他位为保留位必须为0。下面我们来看一下 CPU 是如何处理中断的:
* CPU 会在栈上保存标志寄存器、`cs`段寄存器和程序计数器IP
* 如果中断是由错误码引起的(比如 `#PF` CPU会在栈上保存错误码
* 在中断处理程序执行完毕后,由`iret`指令返回。
OK接下来我们继续分析代码。
设置并加载 IDT
--------------------------------------------------------------------------------
我们分析到了如下代码:
```C
for (i = 0; i < NUM_EXCEPTION_VECTORS; i++)
set_intr_gate(i, early_idt_handler_array[i]);
```
这里循环内部调用了 `set_intr_gate` ,它接受两个参数:
* 中断号,即 `向量号`
* 中断处理程序的地址。
同时,这个函数还会将中断门插入至 `IDT` 表中,代码中的 `&idt_descr` 数组即为 `IDT`。 首先让我们来看一下 `early_idt_handler_array` 数组,它定义在 [arch/x86/include/asm/segment.h](https://github.com/torvalds/linux/blob/master/arch/x86/include/asm/segment.h) 头文件中包含了前32个异常处理程序的地址
```C
#define EARLY_IDT_HANDLER_SIZE 9
#define NUM_EXCEPTION_VECTORS 32
extern const char early_idt_handler_array[NUM_EXCEPTION_VECTORS][EARLY_IDT_HANDLER_SIZE];
```
`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` 的定义。
`set_intr_gate` 宏定义在 [arch/x86/include/asm/desc.h](https://github.com/torvalds/linux/blob/master/arch/x86/include/asm/desc.h)
```C
#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因为我们最多只有 `256` 个中断。然后它调用了 `_set_gate` 函数,它会将中断门写入 `IDT`
```C
static inline void _set_gate(int gate, unsigned type, void *addr,
unsigned dpl, unsigned ist, unsigned seg)
{
gate_desc s;
pack_gate(&s, type, (unsigned long)addr, dpl, ist, seg);
write_idt_entry(idt_table, gate, &s);
write_trace_idt_entry(gate, &s);
}
```
`_set_gate` 函数的开始,它调用了 `pack_gate` 函数。这个函数会使用给定的参数填充 `gate_desc` 结构:
```C
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);
}
```
在这个函数里,我们把从主循环中得到的中断处理程序入口点地址拆成三个部分,填入门描述符中。下面的三个宏就用来做这个拆分工作:
```C
#define PTR_LOW(x) ((unsigned long long)(x) & 0xFFFF)
#define PTR_MIDDLE(x) (((unsigned long long)(x) >> 16) & 0xFFFF)
#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` 类型。
现在我们已经设置好了IDT中的一项那么通过调用 `native_write_idt_entry` 函数来把复制到 `IDT`
```C
static inline void native_write_idt_entry(gate_desc *idt, int entry, const gate_desc *gate)
{
memcpy(&idt[entry], gate, sizeof(*gate));
}
```
主循环结束后,`idt_table` 就已经设置完毕了,其为一个 `gate_desc` 数组。然后我们就可以通过下面的代码加载 `中断描述符表`
```C
load_idt((const struct desc_ptr *)&idt_descr);
```
其中,`idt_descr` 为:
```C
struct desc_ptr idt_descr = { NR_VECTORS * 16 - 1, (unsigned long) idt_table };
```
`load_idt` 函数只是执行了一下 `lidt` 指令:
```C
asm volatile("lidt %0"::"m" (*dtr));
```
你可能已经注意到了,在代码中还有对 `_trace_*` 函数的调用。这些函数会用跟 `_set_gate` 同样的方法对 `IDT` 门进行设置,但仅有一处不同:这些函数并不设置 `idt_table` ,而是 `trace_idt_table` 用于设置追踪点tracepoint我们将会在其他章节介绍这一部分
好了,至此我们已经了解到,通过设置并加载 `中断描述符表` 能够让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)
```assembly
.globl early_idt_handler_array
early_idt_handlers:
i = 0
.rept NUM_EXCEPTION_VECTORS
.if (EXCEPTION_ERRCODE_MASK >> i) & 1
pushq $0
.endif
pushq $i
jmp early_idt_handler_common
i = i + 1
.fill early_idt_handler_array + i*EARLY_IDT_HANDLER_SIZE - ., 1, 0xcc
.endr
```
这段代码自动生成为前 `32` 个异常生成了中断处理程序。首先,为了统一栈的布局,如果一个异常没有返回错误码,那么我们就手动在栈中压入一个 `0`。然后再在栈中压入中断向量号,最后跳转至通用的中断处理程序 `early_idt_handler_common` 。我们可以通过 `objdump` 命令的输出一探究竟:
```
$ objdump -D vmlinux
...
...
...
ffffffff81fe5000 <early_idt_handler_array>:
ffffffff81fe5000: 6a 00 pushq $0x0
ffffffff81fe5002: 6a 00 pushq $0x0
ffffffff81fe5004: e9 17 01 00 00 jmpq ffffffff81fe5120 <early_idt_handler_common>
ffffffff81fe5009: 6a 00 pushq $0x0
ffffffff81fe500b: 6a 01 pushq $0x1
ffffffff81fe500d: e9 0e 01 00 00 jmpq ffffffff81fe5120 <early_idt_handler_common>
ffffffff81fe5012: 6a 00 pushq $0x0
ffffffff81fe5014: 6a 02 pushq $0x2
...
...
...
```
由于在中断发生时CPU 会在栈上压入标志寄存器、`CS` 段寄存器和 `RIP` 寄存器的内容。因此在 `early_idt_handler` 执行前,栈的布局如下:
```
|--------------------|
| %rflags |
| %cs |
| %rip |
| rsp --> error code |
|--------------------|
```
下面我们来看一下 `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)
je .Lis_nmi
```
其中 `is_nmi` 为:
```assembly
is_nmi:
addq $16,%rsp
INTERRUPT_RETURN
```
这段程序首先从栈顶弹出错误码和中断向量号,然后通过调用 `INTERRUPT_RETURN` ,即 `iretq` 指令直接返回。
如果当前中断不是 `NMI` ,则首先检查 `early_recursion_flag` 以避免在 `early_idt_handler_common` 程序中递归地产生中断。如果一切都没问题,就先在栈上保存通用寄存器,为了防止中断返回时寄存器的内容错乱:
```assembly
pushq %rax
pushq %rcx
pushq %rdx
pushq %rsi
pushq %rdi
pushq %r8
pushq %r9
pushq %r10
pushq %r11
```
然后我们检查栈上的段选择子:
```assembly
cmpl $__KERNEL_CS,96(%rsp)
jne 11f
```
段选择子必须为内核代码段,如果不是则跳转到标签 `11` ,输出 `PANIC` 信息并打印栈的内容。然后我们来检查向量号,如果是 `#PF` 即 [缺页中断Page Fault](https://en.wikipedia.org/wiki/Page_fault),那么就把 `cr2` 寄存器中的值赋值给 `rdi` ,然后调用 `early_make_pgtable` (详见后文):
```assembly
cmpl $14,72(%rsp)
jnz 10f
GET_CR2_INTO(%rdi)
call early_make_pgtable
andl %eax,%eax
jz 20f
```
如果向量号不是 `#PF` ,那么就恢复通用寄存器:
```assembly
popq %r11
popq %r10
popq %r9
popq %r8
popq %rdi
popq %rsi
popq %rdx
popq %rcx
popq %rax
```
并调用 `iret` 从中断处理程序返回。
第一个中断处理程序到这里就结束了。由于它只是一个初期中段处理程序,因此只处理缺页中断。下面让我们首先来看一下缺页中断处理程序,其他中断的处理程序我们之后再进行分析。
缺页中断处理程序
--------------------------------------------------------------------------------
在上一节中我们第一次见到了初期中断处理程序,它检查了缺页中断的中断号,并调用了 `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` 寄存器得到的地址,这个地址引发了内存中断。下面让我们来看一下:
```C
int __init early_make_pgtable(unsigned long address)
{
unsigned long physaddr = address - __PAGE_OFFSET;
unsigned long i;
pgdval_t pgd, *pgd_p;
pudval_t pud, *pud_p;
pmdval_t pmd, *pmd_p;
...
...
...
}
```
首先它定义了一些 `*val_t` 类型的变量。这些类型均为:
```C
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),形式如下:
```C
typedef struct { pgdval_t pgd; } pgd_t;
```
例如,
```C
extern pgd_t early_level4_pgt[PTRS_PER_PGD];
```
在这里 `early_level4_pgt` 代表了初期顶层页表目录,它是一个 `pdg_t` 类型的数组,其中的 `pgd` 指向了下一级页表。
在确认不是非法地址后,我们取得页表中包含引起 `#PF` 中断的地址的那一项,将其赋值给 `pgd` 变量:
```C
pgd_p = &early_level4_pgt[pgd_index(address)].pgd;
pgd = *pgd_p;
```
接下来我们检查一下 `pgd` ,如果它包含了正确的全局页表项的话,我们就把这一项的物理地址处理后赋值给 `pud_p`
```C
pud_p = (pudval_t *)((pgd & PTE_PFN_MASK) + __START_KERNEL_map - phys_base);
```
其中 `PTE_PFN_MASK` 是一个宏:
```C
#define PTE_PFN_MASK ((pteval_t)PHYSICAL_PAGE_MASK)
```
展开后将为:
```C
(~(PAGE_SIZE-1)) & ((1 << 46) - 1)
```
或者写为:
```
0b1111111111111111111111111111111111111111111111
```
它是一个46bit大小的页帧屏蔽值。
如果 `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) {
reset_early_page_tables();
goto again;
}
pud_p = (pudval_t *)early_dynamic_pgts[next_early_pgt++];
for (i = 0; i < PTRS_PER_PUD; i++)
pud_p[i] = 0;
*pgd_p = (pgdval_t)pud_p - __START_KERNEL_map + phys_base + _KERNPG_TABLE;
```
然后我们来修正上层页目录的地址:
```C
pud_p += pud_index(address);
pud = *pud_p;
```
下面我们对中层页目录重复上面同样的操作。最后我们利用 In the end we fix address of the page middle directory which contains maps kernel text+data virtual addresses:
```C
pmd = (physaddr & PMD_MASK) + early_pmd_flags;
pmd_p[pmd_index(address)] = pmd;
```
到此缺页中断处理程序就完成了它所有的工作,此时 `early_level4_pgt` 就包含了指向合法地址的项。
小结
--------------------------------------------------------------------------------
本书的第二部分到此结束了。
如果你有任何问题或建议请在twitter上联系我 [0xAX](https://twitter.com/0xAX),或者通过[邮件](anotherworldofworld@gmail.com)与我沟通,还可以新开[issue](https://github.com/hust-open-atom-club/linux-insides-zh/issues/new)。
接下来我们将会看到进入内核入口点 `start_kernel` 函数之前剩下所有的准备工作。
相关链接
--------------------------------------------------------------------------------
* [GNU assembly .rept](https://sourceware.org/binutils/docs-2.23/as/Rept.html)
* [APIC](http://en.wikipedia.org/wiki/Advanced_Programmable_Interrupt_Controller)
* [NMI](http://en.wikipedia.org/wiki/Non-maskable_interrupt)
* [Page table](https://en.wikipedia.org/wiki/Page_table)
* [Interrupt handler](https://en.wikipedia.org/wiki/Interrupt_handler)
* [Page Fault](https://en.wikipedia.org/wiki/Page_fault),
* [Previous part](http://xinqiu.gitbooks.io/linux-insides-cn/content/Initialization/linux-initialization-1.html)