Linux开机启动过程(8):初期中断(缺页中断)和异常处理
内核初始化 第二部分
在原文的基础上添加了5.10.13部分的源码解读。
初期中断和异常处理
在上一个 部分 我们谈到了初期中断初始化。目前我们已经处于解压缩后的Linux内核中了,还有了用于初期启动的基本的 分页 机制。我们的目标是在内核的主体代码执行前做好准备工作。
我们已经在 本章 的 第一部分 做了一些工作,在这一部分中我们会继续分析关于中断和异常处理部分的代码。
我们在上一部分谈到了下面这个循环:
/*** idt_setup_early_handler - Initializes the idt table with early handlers*/
void __init idt_setup_early_handler(void)
{int i;for (i = 0; i < NUM_EXCEPTION_VECTORS; i++)set_intr_gate(i, early_idt_handler_array[i]);
#ifdef CONFIG_X86_32for ( ; i < NR_VECTORS; i++)set_intr_gate(i, early_ignore_irq);
#endifload_idt(&idt_descr);
}
这段代码位于 arch/x86/kernel/head64.c。在分析这段代码之前,我们先来了解一些关于中断和中断处理程序的知识。
理论
中断是一种由软件或硬件产生的、向CPU发出的事件。例如,如果用户按下了键盘上的一个按键时,就会产生中断。此时CPU将会暂停当前的任务,并且将控制流转到特殊的程序中—— 中断处理程序(Interrupt Handler)。一个中断处理程序会对中断进行处理,然后将控制权交还给之前暂停的任务中。中断分为三类:
- 软件中断 - 当一个软件可以向CPU发出信号,表明它需要系统内核的相关功能时产生。这些中断通常用于系统调用;
- 硬件中断 - 当一个硬件有任何事件发生时产生,例如键盘的按键被按下;
- 异常 - 当CPU检测到错误时产生,例如发生了除零错误或者访问了一个不存在的内存页。
每一个中断和异常都可以由一个数来表示,这个数叫做 向量号
,它可以取从 0
到 255
中的任何一个数。通常在实践中前 32
个向量号用来表示异常,32
到 255
用来表示用户定义的中断。可以看到在上面的代码中,NUM_EXCEPTION_VECTORS
就定义为:
#define NUM_EXCEPTION_VECTORS 32
其中详细定义如下:
#define X86_TRAP_DE 0 /* Divide-by-zero */ /* 被 0 除 */
#define X86_TRAP_DB 1 /* Debug */
#define X86_TRAP_NMI 2 /* Non-maskable Interrupt 不可屏蔽中断, 严重问题 */
#define X86_TRAP_BP 3 /* Breakpoint */ /* 断点 */
#define X86_TRAP_OF 4 /* Overflow */
#define X86_TRAP_BR 5 /* Bound Range Exceeded */
#define X86_TRAP_UD 6 /* Invalid Opcode */
#define X86_TRAP_NM 7 /* Device Not Available */
#define X86_TRAP_DF 8 /* Double Fault */
#define X86_TRAP_OLD_MF 9 /* Coprocessor Segment Overrun */
#define X86_TRAP_TS 10 /* Invalid TSS */
#define X86_TRAP_NP 11 /* Segment Not Present */
#define X86_TRAP_SS 12 /* Stack Segment Fault */
#define X86_TRAP_GP 13 /* General Protection Fault */
#define X86_TRAP_PF 14 /* Page Fault */ /* 页 fault */
#define X86_TRAP_SPURIOUS 15 /* Spurious Interrupt */
#define X86_TRAP_MF 16 /* x87 Floating-Point Exception */
#define X86_TRAP_AC 17 /* Alignment Check */
#define X86_TRAP_MC 18 /* Machine Check */
#define X86_TRAP_XF 19 /* SIMD Floating-Point Exception */
#define X86_TRAP_VE 20 /* Virtualization Exception */
#define X86_TRAP_CP 21 /* Control Protection Exception */
#define X86_TRAP_VC 29 /* VMM Communication Exception 硬件虚拟化之vmm接管异常中断 */
#define X86_TRAP_IRET 32 /* IRET Exception */
CPU会从 APIC 或者 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
- 用来为中断处理提供一个新的栈。
门(gate)
结构如下:
struct idt_bits {/* 中断 索引 */u16 ist : 3,zero : 5,type : 5,dpl : 2,p : 1;
} __attribute__((packed));struct idt_data {/* 中断描述符表 数据 */unsigned int vector;unsigned int segment;struct idt_bits bits;const void *addr;
};struct gate_struct {/* 门 */u16 offset_low;u16 segment;struct idt_bits bits;u16 offset_middle;
#ifdef CONFIG_X86_64u32 offset_high;u32 reserved;
#endif
} __attribute__((packed));
最后的 Type
域描述了这一项的类型,中断处理程序共分为三种:
- 任务描述符
- 中断描述符
- 陷阱描述符
在5.10.13中已经为四项:
enum {GATE_INTERRUPT = 0xE, /* 中断门 */GATE_TRAP = 0xF, /* 陷阱 */GATE_CALL = 0xC, GATE_TASK = 0x5, /* 任务 */
};
中断和陷阱描述符包含了一个指向中断处理程序的远 (far) 指针,二者唯一的不同在于CPU处理 IF
标志的方式。如果是由中断门进入中断处理程序的,CPU 会清除 IF
标志位,这样当当前中断处理程序执行时,CPU 不会对其他的中断进行处理;只有当当前的中断处理程序返回时,CPU 才在 iret
指令执行时重新设置 IF
标志位。
中断门的其他位为保留位,必须为0。下面我们来看一下 CPU 是如何处理中断的:
- CPU 会在栈上保存标志寄存器、
cs
段寄存器和程序计数器IP; - 如果中断是由错误码引起的(比如
#PF
), CPU会在栈上保存错误码; - 在中断处理程序执行完毕后,由
iret
指令返回。
OK,接下来我们继续分析代码。
设置并加载 IDT
我们分析到了如下代码:
for (i = 0; i < NUM_EXCEPTION_VECTORS; i++)set_intr_gate(i, early_idt_handler_array[i]);
这里循环内部调用了 set_intr_gate
,它接受两个参数:
- 中断号,即
向量号
; - 中断处理程序的地址。
static __init void set_intr_gate(unsigned int n, const void *addr)
{struct idt_data data;init_idt_data(&data, n, addr);//将中断门插入至 `IDT` 表中idt_setup_from_table(idt_table, &data, 1, false);
}
同时,这个函数还会将中断门插入至 IDT
表中,代码中的 &idt_descr
数组即为 IDT
。 首先让我们来看一下 early_idt_handler_array
数组,它定义在 arch/x86/include/asm/segment.h 头文件中,包含了前32个异常处理程序的地址:
#define EARLY_IDT_HANDLER_SIZE 9
#define NUM_EXCEPTION_VECTORS 32extern const char early_idt_handler_array[NUM_EXCEPTION_VECTORS][EARLY_IDT_HANDLER_SIZE];
early_idt_handler_array
是一个大小为 288
字节的数组,每一项为 9
个字节,其中2个字节的备用指令用于向栈中压入默认错误码(如果异常本身没有提供错误码的话),2个字节的指令用于向栈中压入向量号,剩余5个字节用于跳转到异常处理程序。
for (i = 0; i < NUM_EXCEPTION_VECTORS; i++)set_intr_gate(i, early_idt_handler_array[i]);
在上面的代码中,我们只通过一个循环向 IDT
中填入了前32项内容,这是因为在整个初期设置阶段,中断是禁用的。early_idt_handler_array
数组中的每一项指向的都是同一个通用中断处理程序,定义在 arch/x86/kernel/head_64.S 。我们先暂时跳过这个数组的内容,看一下 set_intr_gate
的定义。
set_intr_gate
宏定义在 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,因为我们最多只有 256
个中断。然后它调用了 _set_gate
函数,它会将中断门写入 IDT
:
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
结构:
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);
}
在这个函数里,我们把从主循环中得到的中断处理程序入口点地址拆成三个部分,填入门描述符中。下面的三个宏就用来做这个拆分工作:
#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
类型。
在5.10.13中已经修改为:
static __init void set_intr_gate(unsigned int n, const void *addr)
{struct idt_data data;init_idt_data(&data, n, addr);//将中断门插入至 `IDT` 表中idt_setup_from_table(idt_table, &data, 1, false);
}
init_idt_data如下:
/* 初始化一个 中断描述符 */
static inline void init_idt_data(struct idt_data *data, unsigned int n,const void *addr)/* */
{BUG_ON(n > 0xFF); //`BUG_ON` 宏确保了传入的中断向量号不会大于255memset(data, 0, sizeof(*data));data->vector = n; /* 终端号 */data->addr = addr; /* 处理地址 */data->segment = __KERNEL_CS; /* 内核代码段 */data->bits.type = GATE_INTERRUPT;/* 中断描述符 */data->bits.p = 1; /* */
}
idt_setup_from_table如下:
static __init void
idt_setup_from_table(gate_desc *idt, const struct idt_data *t, int size/* 个数 */, bool sys)/* */
{/* 门 */gate_desc desc;for (; size > 0; t++, size--) {idt_init_desc(&desc, t); /* */write_idt_entry(idt, t->vector, &desc); /* 写入 CPU */if (sys)set_bit(t->vector, system_vectors);}
}
这里的write_idt_entry为:
static inline void write_idt_entry(gate_desc *dt, int entry, const gate_desc *g)
{ /* */PVOP_VCALL3(cpu.write_idt_entry, dt, entry, g);
}
如果没有定义:CONFIG_PARAVIRT_XXL
#ifdef CONFIG_PARAVIRT_XXL
#include <asm/paravirt.h>
#else
#define write_idt_entry(dt, entry, g) native_write_idt_entry(dt, entry, g)
#endif /* CONFIG_PARAVIRT_XXL */
现在我们已经设置好了IDT中的一项,那么通过调用 native_write_idt_entry
函数来把复制到 IDT
:
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
数组。
/* Must be page-aligned because the real IDT is used in the cpu entry area */
static gate_desc __page_aligned_bss idt_table[IDT_ENTRIES] ; /* 中断描述符表 */
而我们的idt_descr为:
static struct desc_ptr __ro_after_init idt_descr = {.size = IDT_TABLE_SIZE - 1,.address = (unsigned long) idt_table,
};
然后我们就可以通过下面的代码加载 中断描述符表
:
load_idt((const struct desc_ptr *)&idt_descr);
其中,idt_descr
为:
struct desc_ptr idt_descr = { NR_VECTORS * 16 - 1, (unsigned long) idt_table };
load_idt
函数只是执行了一下 lidt
指令:
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:
.globl early_idt_handler_array
early_idt_handlers:i = 0.rept NUM_EXCEPTION_VECTORS.if (EXCEPTION_ERRCODE_MASK >> i) & 1pushq $0.endifpushq $ijmp early_idt_handler_commoni = i + 1.fill early_idt_handler_array + i*EARLY_IDT_HANDLER_SIZE - ., 1, 0xcc.endr
在5.10.13中为:
SYM_CODE_START(early_idt_handler_array)i = 0.rept NUM_EXCEPTION_VECTORS.if ((EXCEPTION_ERRCODE_MASK >> i) & 1) == 0UNWIND_HINT_IRET_REGSpushq $0 //# Dummy error code, to make stack frame uniform.elseUNWIND_HINT_IRET_REGS offset=8.endifpushq $i //# 72(%rsp) Vector numberjmp early_idt_handler_commonUNWIND_HINT_IRET_REGSi = i + 1.fill early_idt_handler_array + i*EARLY_IDT_HANDLER_SIZE - ., 1, 0xcc.endrUNWIND_HINT_IRET_REGS offset=16
SYM_CODE_END(early_idt_handler_array)
这段代码自动生成为前 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
的实现。
SYM_CODE_START_LOCAL(early_idt_handler_common)/** The stack is the hardware frame, an error code or zero, and the* vector number.*/cldincl early_recursion_flag(%rip)/* The vector number is currently in the pt_regs->di slot. */pushq %rsi /* pt_regs->si */movq 8(%rsp), %rsi /* RSI = vector number */movq %rdi, 8(%rsp) /* pt_regs->di = RDI */pushq %rdx /* pt_regs->dx */pushq %rcx /* pt_regs->cx */pushq %rax /* pt_regs->ax */pushq %r8 /* pt_regs->r8 */pushq %r9 /* pt_regs->r9 */pushq %r10 /* pt_regs->r10 */pushq %r11 /* pt_regs->r11 */pushq %rbx /* pt_regs->bx */pushq %rbp /* pt_regs->bp */pushq %r12 /* pt_regs->r12 */pushq %r13 /* pt_regs->r13 */pushq %r14 /* pt_regs->r14 */pushq %r15 /* pt_regs->r15 */UNWIND_HINT_REGSmovq %rsp,%rdi /* RDI = pt_regs; RSI is already trapnr */call do_early_exceptiondecl early_recursion_flag(%rip)jmp restore_regs_and_return_to_kernel
SYM_CODE_END(early_idt_handler_common)
它也定义在 arch/x86/kernel/head_64.S 文件中。首先它会检查当前中断是否为 不可屏蔽中断(NMI),如果是则简单地忽略它们:
cmpl $2,(%rsp)je .Lis_nmi
其中 is_nmi
为:
is_nmi:addq $16,%rspINTERRUPT_RETURN
这段程序首先从栈顶弹出错误码和中断向量号,然后通过调用 INTERRUPT_RETURN
,即 iretq
指令直接返回。
如果当前中断不是 NMI
,则首先检查 early_recursion_flag
以避免在 early_idt_handler_common
程序中递归地产生中断。如果一切都没问题,就先在栈上保存通用寄存器,为了防止中断返回时寄存器的内容错乱:
pushq %raxpushq %rcxpushq %rdxpushq %rsipushq %rdipushq %r8pushq %r9pushq %r10pushq %r11
然后我们检查栈上的段选择子:
cmpl $__KERNEL_CS,96(%rsp)jne 11f
段选择子必须为内核代码段,如果不是则跳转到标签 11
,输出 PANIC
信息并打印栈的内容。然后我们来检查向量号,如果是 #PF
即 缺页中断(Page Fault),那么就把 cr2
寄存器中的值赋值给 rdi
,然后调用 early_make_pgtable
(详见后文):
cmpl $14,72(%rsp)jnz 10fGET_CR2_INTO(%rdi)call early_make_pgtableandl %eax,%eaxjz 20f
如果向量号不是 #PF
,那么就恢复通用寄存器:
popq %r11popq %r10popq %r9popq %r8popq %rdipopq %rsipopq %rdxpopq %rcxpopq %rax
并调用 iret
从中断处理程序返回。
在5.10.13中是这样的:
SYM_CODE_START_LOCAL(early_idt_handler_common)/** The stack is the hardware frame, an error code or zero, and the* vector number.*/cld//检查 `early_recursion_flag` 以避免在 `early_idt_handler_common` 程序中递归地产生中断incl early_recursion_flag(%rip)/* The vector number is currently in the pt_regs->di slot. *///在栈上保存通用寄存器,为了防止中断返回时寄存器的内容错乱pushq %rsi /* pt_regs->si */movq 8(%rsp), %rsi /* RSI = vector number */movq %rdi, 8(%rsp) /* pt_regs->di = RDI */pushq %rdx /* pt_regs->dx */pushq %rcx /* pt_regs->cx */pushq %rax /* pt_regs->ax */pushq %r8 /* pt_regs->r8 */pushq %r9 /* pt_regs->r9 */pushq %r10 /* pt_regs->r10 */pushq %r11 /* pt_regs->r11 */pushq %rbx /* pt_regs->bx */pushq %rbp /* pt_regs->bp */pushq %r12 /* pt_regs->r12 */pushq %r13 /* pt_regs->r13 */pushq %r14 /* pt_regs->r14 */pushq %r15 /* pt_regs->r15 */UNWIND_HINT_REGSmovq %rsp,%rdi /* RDI = pt_regs; RSI is already trapnr */call do_early_exceptiondecl early_recursion_flag(%rip)jmp restore_regs_and_return_to_kernel
SYM_CODE_END(early_idt_handler_common)
其调用了do_early_exception处理缺页中断:
void __init do_early_exception(struct pt_regs *regs, int trapnr)
{if (trapnr == X86_TRAP_PF/*缺页中断(Page Fault)*/ &&early_make_pgtable(native_read_cr2()))return;if (IS_ENABLED(CONFIG_AMD_MEM_ENCRYPT) &&trapnr == X86_TRAP_VC && handle_vc_boot_ghcb(regs))return;early_fixup_exception(regs, trapnr);
}
early_make_pgtable创建新的页表,在其内部将分别对各级页表进行创建
/* 建立新的页表 */
static bool __init early_make_pgtable(unsigned long address)
{unsigned long physaddr = address - __PAGE_OFFSET;pmdval_t pmd;pmd = (physaddr & PMD_MASK) + early_pmd_flags;/* Create a new PMD entry */return __early_make_pgtable(address, pmd);
}
下面的X86_TRAP_VC干嘛的?与硬件虚拟化vmm接管异常中断有关。接着,如果不是这两个中断号,进入early_fixup_exception:
TODO 2021年3月17日 荣涛 RToax。
第一个中断处理程序到这里就结束了。由于它只是一个初期中段处理程序,因此只处理缺页中断。下面让我们首先来看一下缺页中断处理程序,其他中断的处理程序我们之后再进行分析。
缺页中断处理程序
在上一节中我们第一次见到了初期中断处理程序,它检查了缺页中断的中断号,并调用了 early_make_pgtable
来建立新的页表。在这里我们需要提供 #PF
中断处理程序,以便为之后将内核加载至 4G
地址以上,并且能访问位于4G以上的 boot_params
结构体。
early_make_pgtable
的实现在 arch/x86/kernel/head64.c,它接受一个参数:从 cr2
寄存器得到的地址,这个地址引发了内存中断。下面让我们来看一下:
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
类型的变量。这些类型均为:
typedef unsigned long pgdval_t;
此外,我们还会遇见 *_t
(不带val)的类型,比如 pgd_t
……这些类型都定义在 arch/x86/include/asm/pgtable_types.h,形式如下:
typedef struct { pgdval_t pgd; } pgd_t;
例如,
extern pgd_t early_level4_pgt[PTRS_PER_PGD];
在这里 early_level4_pgt
代表了初期顶层页表目录,它是一个 pdg_t
类型的数组,其中的 pgd
指向了下一级页表。
在确认不是非法地址后,我们取得页表中包含引起 #PF
中断的地址的那一项,将其赋值给 pgd
变量:
pgd_p = &early_level4_pgt[pgd_index(address)].pgd;
pgd = *pgd_p;
接下来我们检查一下 pgd
,如果它包含了正确的全局页表项的话,我们就把这一项的物理地址处理后赋值给 pud_p
:
pud_p = (pudval_t *)((pgd & PTE_PFN_MASK) + __START_KERNEL_map - phys_base);
其中 PTE_PFN_MASK
是一个宏:
#define PTE_PFN_MASK ((pteval_t)PHYSICAL_PAGE_MASK)
展开后将为:
(~(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
访问权限一起写入全局页目录表:
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;
然后我们来修正上层页目录的地址:
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:
pmd = (physaddr & PMD_MASK) + early_pmd_flags;
pmd_p[pmd_index(address)] = pmd;
到此缺页中断处理程序就完成了它所有的工作,此时 early_level4_pgt
就包含了指向合法地址的项。
给出5.10.13中early_make_pgtable
完整的代码:
/* Create a new PMD entry */
bool __init __early_make_pgtable(unsigned long address, pmdval_t pmd)
{unsigned long physaddr = address - __PAGE_OFFSET;pgdval_t pgd, *pgd_p;p4dval_t p4d, *p4d_p;pudval_t pud, *pud_p;pmdval_t *pmd_p;/* Invalid address or early pgt is done ? */if (physaddr >= MAXMEM || read_cr3_pa() != __pa_nodebug(early_top_pgt))return false;again:pgd_p = &early_top_pgt[pgd_index(address)].pgd;pgd = *pgd_p;/** The use of __START_KERNEL_map rather than __PAGE_OFFSET here is* critical -- __PAGE_OFFSET would point us back into the dynamic* range and we might end up looping forever...*/if (!pgtable_l5_enabled())p4d_p = pgd_p;else if (pgd)p4d_p = (p4dval_t *)((pgd & PTE_PFN_MASK) + __START_KERNEL_map - phys_base);else {if (next_early_pgt >= EARLY_DYNAMIC_PAGE_TABLES) {reset_early_page_tables();goto again;}p4d_p = (p4dval_t *)early_dynamic_pgts[next_early_pgt++];memset(p4d_p, 0, sizeof(*p4d_p) * PTRS_PER_P4D);*pgd_p = (pgdval_t)p4d_p - __START_KERNEL_map + phys_base + _KERNPG_TABLE;}p4d_p += p4d_index(address);p4d = *p4d_p;if (p4d)pud_p = (pudval_t *)((p4d & PTE_PFN_MASK) + __START_KERNEL_map - phys_base);else {if (next_early_pgt >= EARLY_DYNAMIC_PAGE_TABLES) {reset_early_page_tables();goto again;}pud_p = (pudval_t *)early_dynamic_pgts[next_early_pgt++];memset(pud_p, 0, sizeof(*pud_p) * PTRS_PER_PUD);*p4d_p = (p4dval_t)pud_p - __START_KERNEL_map + phys_base + _KERNPG_TABLE;}pud_p += pud_index(address);pud = *pud_p;if (pud)pmd_p = (pmdval_t *)((pud & PTE_PFN_MASK) + __START_KERNEL_map - phys_base);else {if (next_early_pgt >= EARLY_DYNAMIC_PAGE_TABLES) {reset_early_page_tables();goto again;}pmd_p = (pmdval_t *)early_dynamic_pgts[next_early_pgt++];memset(pmd_p, 0, sizeof(*pmd_p) * PTRS_PER_PMD);*pud_p = (pudval_t)pmd_p - __START_KERNEL_map + phys_base + _KERNPG_TABLE;}pmd_p[pmd_index(address)] = pmd;return true;
}/* 建立新的页表 */
static bool __init early_make_pgtable(unsigned long address)
{unsigned long physaddr = address - __PAGE_OFFSET;pmdval_t pmd;pmd = (physaddr & PMD_MASK) + early_pmd_flags;/* Create a new PMD entry */return __early_make_pgtable(address, pmd);
}
小结
本书的第二部分到此结束了。
如果你有任何问题或建议,请在twitter上联系我 0xAX,或者通过邮件与我沟通,还可以新开issue。
接下来我们将会看到进入内核入口点 start_kernel
函数之前剩下所有的准备工作。
相关链接
- GNU assembly .rept
- APIC
- NMI
- Page table
- Interrupt handler
- Page Fault,
- Previous part
Linux开机启动过程(8):初期中断(缺页中断)和异常处理相关推荐
- Linux开机启动过程:从点下电源键到系统正常运行
学习内核,只要是要以柔克刚,不能急于求成.共勉 <Linux开机启动过程(1):内核引导过程> <Linux开机启动过程(2):内核启动的第一步> <Linux开机启动过 ...
- linux 打开上一级目录,linux开机启动过程、PATH、过滤一级目录、cd的参数、ls -lrt、命令切割日志...
第二波命令正向我方来袭 :开机启动过程.PATH.过滤一级目录.cd的参数.ls -lrt.命令切割日志 1.1 linux开机启动过程 1.1.1 开机自检(BIOS)-- MBR引导-- GRUB ...
- linux开机启动过程(简述)
简述linux开机启动过程 第一步:加电 第二步:加载BIOS设置,选择启动盘. 这是因为因为BIOS中包含了CPU的相关信息.设备启动顺序信息.硬盘信息.内存信息.时钟信 息.PnP特性等等.在此之 ...
- Linux开机启动过程(9):进入内核入口点之前最后的准备工作
内核初始化 第三部分 在原文的基础上添加了5.10.13部分的源码解读. 进入内核入口点之前最后的准备工作 这是 Linux 内核初始化过程的第三部分.在上一个部分 中我们接触到了初期中断和异常处理, ...
- Linux开机启动过程(7):内核执行入口点
内核初始化 第一部分 踏入内核代码的第一步(TODO: Need proofreading) 上一章是引导过程的最后一部分.从现在开始,我们将深入探究 Linux 内核的初始化过程.在解压缩完 Lin ...
- Linux开机启动过程(4):切换到64位模式-长模式(直到内核解压缩之前)
内核引导过程. Part 4. 本文是在原文基础上经过本人的修改. 切换到64位模式 直到内核解压缩之前的所有步骤 这是 内核引导过程 的第四部分,我们将会看到在保护模式中的最初几步,比如确认CPU是 ...
- Linux开机启动过程详细分析
from: http://www.linuxidc.com/Linux/2007-11/8701.htm 由于操作系统正在变得越来越复杂,所以开机引导和关机下电的过程也越来越智能化.从简单的DOS系统 ...
- Linux开机启动过程(3):显示模式初始化和进入保护模式
内核启动过程,第三部分 本文是在原文基础上经过本人的修改. 显示模式初始化和进入保护模式 这一章是内核启动过程的第三部分,在前一章中,我们的内核启动过程之旅停在了对 set_video 函数的调用(这 ...
- Linux开机启动过程(2):内核启动的第一步
在内核安装代码的第一步 本文是在原文基础上经过本人的修改. 内核启动的第一步 在上一节中我们开始接触到内核启动代码,并且分析了初始化部分,最后我们停在了对main函数(main函数是第一个用C写的函数 ...
最新文章
- setTimeout 定时器的使用
- 依赖版本控制-pom文件介绍
- HTTP协议快速入门
- 保密 | 利用DOS命令将文本信息隐藏在图片中
- 《淘宝网开店 SEO 推广 营销 爆款 实战200招》——1.3 网上开店的热门行业有哪些...
- python代码编写_高质量Python代码编写的5个优化技巧
- SuperIndicator 专做轮播图库,没有之一,支持无限循环
- TM1620 led显示芯片用stm8来驱动
- apache ab压测与参数传递
- 如何用Java运行.jar文件
- iOS开发中配置开发者中心证书
- 实战篇:VMware Workstation 虚拟机安装 Linux 系统
- Linux虚拟机下FTP服务器的搭建(详细)
- 如何优雅地使用Sublime Text3
- UML之顺序图(时序图)
- ubuntu 17.10安装64位Chrome浏览器
- SpringCloud微服务架构实战:商家权限体系设计及开发
- gaussian用法 matlab_matlab中的twomodegauss函数-双峰高斯函数
- 2020 - 04 - 16 个人笔记
- 设置textarea不可拖动
热门文章
- 让元素固定_原神雷元素不如火元素吗?阵容搭配与圣遗物强化攻略
- c程序设计停车场收费管理系统_智能车牌识别停车收费管理系统
- js去掉第一个换行符_通过异步迭代简化Node.js流程
- java中的post的作用,JSP、Servlet中get请求和post请求的区别总结
- leetcode23-合并K个升序链表
- js 解析url中search时存在中文乱码问题解决方案
- http报文和协议首部
- Net学习日记_ASP.Net_一般处理程序_笔记
- 用jquery或js实现三个div自动循环轮播
- 案例:演示<jsp:include>动作元素