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_32
	for ( ; i < NR_VECTORS; i++)
		set_intr_gate(i, early_ignore_irq);
#endif
	load_idt(&idt_descr);
}

这段代码位于 arch/x86/kernel/head64.c。在分析这段代码之前,我们先来了解一些关于中断和中断处理程序的知识。

理论

中断是一种由软件或硬件产生的、向CPU发出的事件。例如,如果用户按下了键盘上的一个按键时,就会产生中断。此时CPU将会暂停当前的任务,并且将控制流转到特殊的程序中—— 中断处理程序(Interrupt Handler)。一个中断处理程序会对中断进行处理,然后将控制权交还给之前暂停的任务中。中断分为三类:

  • 软件中断 - 当一个软件可以向CPU发出信号,表明它需要系统内核的相关功能时产生。这些中断通常用于系统调用;
  • 硬件中断 - 当一个硬件有任何事件发生时产生,例如键盘的按键被按下;
  • 异常 - 当CPU检测到错误时产生,例如发生了除零错误或者访问了一个不存在的内存页。

每一个中断和异常都可以由一个数来表示,这个数叫做 向量号 ,它可以取从 0255 中的任何一个数。通常在实践中前 32 个向量号用来表示异常,32255 用来表示用户定义的中断。可以看到在上面的代码中,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_64
	u32		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	32

extern 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` 宏确保了传入的中断向量号不会大于255

	memset(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) & 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

在5.10.13中为:

SYM_CODE_START(early_idt_handler_array)
	i = 0
	.rept NUM_EXCEPTION_VECTORS
	.if ((EXCEPTION_ERRCODE_MASK >> i) & 1) == 0
		UNWIND_HINT_IRET_REGS
		pushq $0	//# Dummy error code, to make stack frame uniform
	.else
		UNWIND_HINT_IRET_REGS offset=8
	.endif
	pushq $i		//# 72(%rsp) Vector number
	jmp early_idt_handler_common
	UNWIND_HINT_IRET_REGS
	i = i + 1
	.fill early_idt_handler_array + i*EARLY_IDT_HANDLER_SIZE - ., 1, 0xcc
	.endr
	UNWIND_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.
	 */
	cld

	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_REGS

	movq %rsp,%rdi		/* RDI = pt_regs; RSI is already trapnr */
	call do_early_exception

	decl 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,%rsp
	INTERRUPT_RETURN

这段程序首先从栈顶弹出错误码和中断向量号,然后通过调用 INTERRUPT_RETURN ,即 iretq 指令直接返回。

如果当前中断不是 NMI ,则首先检查 early_recursion_flag 以避免在 early_idt_handler_common 程序中递归地产生中断。如果一切都没问题,就先在栈上保存通用寄存器,为了防止中断返回时寄存器的内容错乱:

	pushq %rax
	pushq %rcx
	pushq %rdx
	pushq %rsi
	pushq %rdi
	pushq %r8
	pushq %r9
	pushq %r10
	pushq %r11

然后我们检查栈上的段选择子

	cmpl $__KERNEL_CS,96(%rsp)
	jne 11f

段选择子必须为内核代码段,如果不是则跳转到标签 11 ,输出 PANIC 信息并打印栈的内容。然后我们来检查向量号,如果是 #PF缺页中断(Page Fault),那么就把 cr2 寄存器中的值赋值给 rdi ,然后调用 early_make_pgtable (详见后文):

	cmpl $14,72(%rsp)
	jnz 10f
	GET_CR2_INTO(%rdi)
	call early_make_pgtable
	andl %eax,%eax
	jz 20f

如果向量号不是 #PF ,那么就恢复通用寄存器:

	popq %r11
	popq %r10
	popq %r9
	popq %r8
	popq %rdi
	popq %rsi
	popq %rdx
	popq %rcx
	popq %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_REGS

	movq %rsp,%rdi		/* RDI = pt_regs; RSI is already trapnr */
	call do_early_exception

	decl 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_pgtEARLY_DYNAMIC_PAGE_TABLES(即 64 )的大小。EARLY_DYNAMIC_PAGE_TABLES 它是一个固定大小的缓冲区,用来在需要的时候建立新的页表。如果 next_early_pgtEARLY_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 函数之前剩下所有的准备工作。

相关链接

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值