Skip to content

Latest commit

 

History

History
906 lines (686 loc) · 36.1 KB

File metadata and controls

906 lines (686 loc) · 36.1 KB

Linux内核初始化 (第二部分)

0 内核入口点的最后准备

在上一篇的最后,Linux内核在arch/x86/kernel/head_64.S汇编代码中调用x86_64_start_kernel,现在我们进行内核入口点的最后准备。

1 平台入口点(x86_64_start_kernel

x86_64_start_kernel函数在arch/x86/kernel/head64.c中实现,实现过程如下:

1.1 边界检查

首先,进行一些检查工作,针对内核镜像大小,模块区域映射边界检查等。如下:

	BUILD_BUG_ON(MODULES_VADDR < __START_KERNEL_map);
	BUILD_BUG_ON(MODULES_VADDR - __START_KERNEL_map < KERNEL_IMAGE_SIZE);
	BUILD_BUG_ON(MODULES_LEN + KERNEL_IMAGE_SIZE > 2*PUD_SIZE);
	BUILD_BUG_ON((__START_KERNEL_map & ~PMD_MASK) != 0);
	BUILD_BUG_ON((MODULES_VADDR & ~PMD_MASK) != 0);
	BUILD_BUG_ON(!(MODULES_VADDR > __START_KERNEL));
	MAYBE_BUILD_BUG_ON(!(((MODULES_END - 1) & PGDIR_MASK) ==
				(__START_KERNEL & PGDIR_MASK)));
	BUILD_BUG_ON(__fix_to_virt(__end_of_fixed_addresses) <= MODULES_END);

BUILD_BUG_ON是一个宏定义,定义如下:

#define BUILD_BUG_ON(condition) ((void)sizeof(char[1 - 2*!!(condition)]))

!!(condition)等价于condition != 0,如果condition为真,则!!(condition)值为1,否则为0。2*!!(condition)的结果为20。因此,BUILD_BUG_ON执行完后可能产生两个不同的行为:

  • condition为true,产生编译错误,我们尝试获取一个字符数组的-1索引;
  • condition为false,编译正常。

1.2 初始化cr4影子

保存cr4的shadow copy,在禁用中断时CPU的cr4寄存器被保护,需要保存每个CPU中cr4内容。调用cr4_init_shadow函数实现,在arch/x86/include/asm/tlbflush.h中定义。

1.3 重置页表信息

1.3.1 重置早期页表

接下来,调用reset_early_page_tables重置所有的全局目录项(early_top_pgt),并向cr3中写入全局页目录地址。

	memset(early_top_pgt, 0, sizeof(pgd_t)*(PTRS_PER_PGD-1));
	next_early_pgt = 0;
	write_cr3(__sme_pa_nodebug(early_top_pgt));

__sme_pa_nodebug定义为(__pa_nodebug(x) | sme_me_mask)__pa_nodebug定义为__phys_addr_nodebug((unsigned long)(x))__phys_addr_nodebug定义为:

static inline unsigned long __phys_addr_nodebug(unsigned long x)
{
	unsigned long y = x - __START_KERNEL_map;
	/* use the carry flag to determine if x was < __START_KERNEL_map */
	x = y + ((x > y) ? phys_base : (__START_KERNEL_map - PAGE_OFFSET));
	return x;
}

1.3.2 重置bss

调用clear_bss,重置__bss_start__bss_stop区间。

1.3.3 重置init_top_pgt

调用clear_page,重置init_top_pgt页表。

init_top_pgtarch/x86/kernel/head_64.S中定义。如下:

#if defined(CONFIG_XEN_PV) || defined(CONFIG_PVH)
NEXT_PGD_PAGE(init_top_pgt)
	.quad   level3_ident_pgt - __START_KERNEL_map + _KERNPG_TABLE_NOENC
	.org    init_top_pgt + L4_PAGE_OFFSET*8, 0
	.quad   level3_ident_pgt - __START_KERNEL_map + _KERNPG_TABLE_NOENC
	.org    init_top_pgt + L4_START_KERNEL*8, 0
	/* (2^48-(2*1024*1024*1024))/(2^39) = 511 */
	.quad   level3_kernel_pgt - __START_KERNEL_map + _PAGE_TABLE_NOENC
	.fill	PTI_USER_PGD_FILL,8,0
    ...
#else
NEXT_PGD_PAGE(init_top_pgt)
	.fill	512,8,0
	.fill	PTI_USER_PGD_FILL,8,0
#endif

即,init_top_pgt512个空的页表项。

clear_pagearch/x86/include/asm/page_64.h中定义,如下:

static inline void clear_page(void *page)
{
	alternative_call_2(clear_page_orig,
			   clear_page_rep, X86_FEATURE_REP_GOOD,
			   clear_page_erms, X86_FEATURE_ERMS,
			   "=D" (page),
			   "0" (page)
			   : "cc", "memory", "rax", "rcx");
}

clear_page_origarch/x86/lib/clear_page_64.S中定义,如下:

ENTRY(clear_page_orig)
	xorl   %eax,%eax
	movl   $4096/64,%ecx
	.p2align 4
.Lloop:
	decl	%ecx
#define PUT(x) movq %rax,x*8(%rdi)
	movq %rax,(%rdi)
	PUT(1)
	PUT(2)
	PUT(3)
	PUT(4)
	PUT(5)
	PUT(6)
	PUT(7)
	leaq	64(%rdi),%rdi
	jnz	.Lloop
	nop
	ret
ENDPROC(clear_page_orig)
EXPORT_SYMBOL_GPL(clear_page_orig)

clear_page_orig将使用一个64次的循环,每次循环将64字节置零。

1.4 SME初期设置

sme_early_initarch/x86/mm/mem_encrypt.c中定义。

SME启用的情况下(sme_me_mask不为0),通过或间接通过__sme_set修改early_pmd_flags, __supported_pte_mask, protection_map的地址。

__sme_set定义为((x) | sme_me_mask)

1.5 KASAN初期设置

kasan_early_initarch/x86/mm/kasan_init_64.c中定义。

首先,计算pte, pmd, pud, p4d的值;移除不支持__PAGE_KERNEL掩码;初始化kasan_early_shadow_pte, kasan_early_shadow_pmd, kasan_early_shadow_pud, kasan_early_shadow_p4d。以pmd为例,如下:

	pmdval_t pmd_val = __pa_nodebug(kasan_early_shadow_pte) | _KERNPG_TABLE;
	pmd_val &= __default_kernel_pte_mask;
	for (i = 0; i < PTRS_PER_PMD; i++)
		kasan_early_shadow_pmd[i] = __pmd(pmd_val);

在进行上述初始化后,调用kasan_map_early_shadow函数,初始化early_top_pgt, init_top_pgt

kasan_map_early_shadow实现如下:

static void __init kasan_map_early_shadow(pgd_t *pgd)
{
	/* See comment in kasan_init() */
	unsigned long addr = KASAN_SHADOW_START & PGDIR_MASK;
	unsigned long end = KASAN_SHADOW_END;
	unsigned long next;

	pgd += pgd_index(addr);
	do {
		next = pgd_addr_end(addr, end);
		kasan_early_p4d_populate(pgd, addr, next);
	} while (pgd++, addr = next, addr != end);
}

kasan_early_p4d_populate按需分配pgd, p4d页表。

1.6 初期中断表设置

1.6.1 中断介绍

中断(interrupt)是一个事件,该事件通过中断信号改变CPU执行的顺序。当中断信号到达时,CPU暂停当前执行的任务,并且切换到一个新的程序执行,这个程序叫做中断处理程序(Interrupt handler)。中断处理程序对中断进行处理,在完成处理后将控制权交还给之前暂停的任务。

中断通常分为三类:

  • 中断(异步中断)- 由硬件设备产生IRQ(Interrupt ReQuest), 分为可屏蔽中断(maskable interrupt)和非屏蔽中断(nomaskable interrupt);
  • 异常(同步中断)- CPU执行指令时探测到一个错误条件时产生的异常;
  • 软中断 - 软件向CPU发送指令,触发编程异常。有两个常用的用途:系统调用和给调试程序发送特定事件;

每个中断好异常是由0 ~ 255之间的一个数标识,通常这个数叫做向量(vector)。在实践中前32个向量号用来表示异常,32 ~ 255用来表示用户定义的中断。

CPU从APIC或CPU引脚接收中断,使用中断向量号作为中断描述表(Interrupt descriptor table,IDT)的索引。0 ~ 31号异常如下:

----------------------------------------------------------------------------------------------
|Vector|Mnemonic|Description         |Type |Error Code|Source                                |
----------------------------------------------------------------------------------------------
|0     | #DE    |Division by zero    |Fault|NO        |DIV and IDIV                          |
|---------------------------------------------------------------------------------------------
|1     | #DB    |Debug               |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       |An instruction 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                   |
----------------------------------------------------------------------------------------------

中断描述表(Interrupt descriptor table,IDT)是一个系统表,与每一个中断或异常向量相联系,每一个向量在表中有相应的中断或异常处理程序的入口地址。内核在运行中断发生前,必须适当的初始化IDT。

和之前介绍的GDT和LDT类似,IDT表中的每个向量由8字节(32位模式下)或16字节(64位模式下)组成,我们通常把IDT中的每一项叫做门(gate)。CPU通过idtr寄存器存放这个IDT,它指定IDT的线性基地址及其限制长度。在运行中断前,必须用lidt指令初始化lidtr

64模式下IDT每一项的结构如下:

127                                                                            96
 --------------------------------------------------------------------------------
|                                                                               |
|                                Reserved                                       |
|                                                                               |
 --------------------------------------------------------------------------------
95                                                                             64
 --------------------------------------------------------------------------------
|                                                                               |
|                               Offset 63..32                                   |
|                                                                               |
 --------------------------------------------------------------------------------
63                                   48  47 46 45 44     40 39       35 34     32
 --------------------------------------------------------------------------------
|                                      |   |  D  |         |           |        |
|       Offset 31..16                  | P |  P  |   Type  |    zero   |  IST   |
|                                      |   |  L  |         |           |        |
 --------------------------------------------------------------------------------
31                                   16 15                                      0
 --------------------------------------------------------------------------------
|                                      |                                        |
|          Segment Selector            |                 Offset 15..0           |
|                                      |                                        |
 --------------------------------------------------------------------------------

字段说明如下:

  • offset - 到中断处理程序入口点的偏移;
  • DPL - 描述符特权级别;
  • P - Segment Present 标志;
  • Segment selector - 在GDT或LDT中的代码段选择符;
  • IST - 用来为中断处理提供一个新的栈;
  • Type - 描述符的类型,分别为:0x5:任务描述符;0xE:中断描述符;0xF:陷阱描述符。

任务门(task gate) 当中断信号发生时,必须取代当前进程的那个进程的TSS选择符存放在任务门中。

中断门(interrupt gate) 包含段选择符和中断或异常处理程序的段内偏移量。当控制权转移到中断处理程序时,CPU清除IF标记,从而关闭将来会发生的可屏蔽中断。在当前中断处理程序返回时,CPU通过iret指令重新设置IF标记位。

陷阱门(trap gate) 处理过程与中断门相似,在将控制权转移到中断处理程序时不修改IF标记。

CPU处理中断的过程如下:

  • 检查当前特权等级(CPL)和描述符特权等级(DPL);
  • CPU在栈上保存eflags(标记寄存器),cs(代码段寄存器),ip(程序计数器);
  • 如果异常产生了一个硬件出错码,CPU将它保存在栈上;
  • 装载cs,ip寄存器,其值分别为IDT表中门描述中的段选择符偏移量字段;跳转到中断或异常处理程序;
  • 中断或异常处理程序处理完成后,通过lret指令返回,将控制权交给被中断的进程。

1.6.2 Linux设置IDT的过程

Linux内核使用gate_desc来表示IDT,在arch/x86/include/asm/desc_defs.h定义。如下:

struct idt_bits {
	u16		ist	: 3,
			zero	: 5,
			type	: 5,
			dpl	: 2,
			p	: 1;
} __attribute__((packed));

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));

typedef struct gate_struct gate_desc;

idt_setup_early_handlerarch/x86/kernel/idt.c中定义。如下:

	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);

可以看到,循环调用set_intr_gate后,调用load_idt设置idt

  • early_idt_handler_array定义

首先,我们看下early_idt_handler_array的定义,它在arch/x86/include/asm/segment.h中定义,如下:

#define IDT_ENTRIES			256
#define NUM_EXCEPTION_VECTORS		32

#define EARLY_IDT_HANDLER_SIZE 9

extern const char early_idt_handler_array[NUM_EXCEPTION_VECTORS][EARLY_IDT_HANDLER_SIZE];

可以看到early_idt_handler_array是一个32项的数组,每一项9字节。其中2个字节备用,用于向栈中压入错误码(没有错误码时,压入0);2个字节用于向栈中压入向量号;5个字节用于异常处理程序地址。

  • set_intr_gate

set_intr_gate在同一个文件中定义,如下:

	struct idt_data data;

	BUG_ON(n > 0xFF);

	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(idt_table, &data, 1, false);

使用idt_data结构来进行中间转换,进行必要检查后,填充相关字段后,调用idt_setup_from_tableidt_table是所有的IDT信息,如下:

gate_desc idt_table[IDT_ENTRIES] __page_aligned_bss;
  • idt_setup_from_table

idt_setup_from_table也在同一个文件中定义,如下:

static 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);
		if (sys)
			set_bit(t->vector, system_vectors);
	}
}

首先,将idt_data转换为gate_desc;然后,调用write_idt_entry写入idt中对应向量中;最后,如果是系统向量,修改system_vectors对应bit项。

  • load_idt

load_idt加载idt_descrldtr寄存器中。idt_descr定义如下:

struct desc_ptr idt_descr __ro_after_init = {
	.size		= (IDT_ENTRIES * 2 * sizeof(unsigned long)) - 1,
	.address	= (unsigned long) idt_table,
};

1.6.3 初期中断处理程序

  • early_idt_handler_array的定义

在上一部分,我们将early_idt_handler_array填充到IDT中,这部分我们对其一探究竟。在arch/x86/kernel/head_64.S中我们找到其定义,如下:

ENTRY(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
END(early_idt_handler_array)

可以看到,每个项都类似如下代码:

6a 00                   pushq  $0x0
6a 00                   pushq  $0x0
e9 17 01 00 00          jmpq   <early_idt_handler_common>
  • early_idt_handler_common的实现

接下来,我们来看early_idt_handler_common的实现,如下:

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

	cmpq $14,%rsi		/* Page fault? */
	jnz 10f
	GET_CR2_INTO(%rdi)	/* can clobber %rax if pv */
	call early_make_pgtable
	andl %eax,%eax
	jz 20f			/* All good */

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

20:
	decl early_recursion_flag(%rip)
	jmp restore_regs_and_return_to_kernel
END(early_idt_handler_common)

执行过程如下:

  1. 增加early_recursion_flag的值,预防递归调用;early_recursion_flag定义如下:

        .balign 4
    GLOBAL(early_recursion_flag)
        .long 0
  2. 保存通用寄存器的值; 首先,获取中断向量,保存到rsi寄存器中;然后保存通用集群器到栈上;

  3. 根据向量值,执行不同的中断处理程序; 如果是14,即#PF页错误(Page Fault),调用early_make_pgtable函数; 如果是其他值,调用early_fixup_exception.

  4. 减少early_recursion_flag值;

  5. 调用restore_regs_and_return_to_kernel恢复到之前的处理状态;

1.6.4 页错误(#PF)中断处理程序

在上一节中,我们检查中断向量值是缺页的情况下调用early_make_pgtable来创建新的页表。这里我们提供#PF中断处理程序,便于之后将内核加载到4G地址以上,并且能够访问位于4G以上的boot_params结构。

  • early_make_pgtable

early_make_pgtablearch/x86/kernel/head64.c中定义,它有一个参数,cr2寄存器里的值,即引起缺页的地址。代码如下:

int __init early_make_pgtable(unsigned long address)
{
	unsigned long physaddr = address - __PAGE_OFFSET;
	pmdval_t pmd;

	pmd = (physaddr & PMD_MASK) + early_pmd_flags;

	return __early_make_pgtable(address, pmd);
}

__PAGE_OFFSETarch/x86/include/asm/page_64_types.h中定义,表示__PAGE_OFFSET_BASE_L4__PAGE_OFFSET_BASE_L5(5级页表启用的情况下).

#define __PAGE_OFFSET_BASE_L5	_AC(0xff11000000000000, UL)
#define __PAGE_OFFSET_BASE_L4	_AC(0xffff888000000000, UL)

#ifdef CONFIG_DYNAMIC_MEMORY_LAYOUT
#define __PAGE_OFFSET           page_offset_base
#else
#define __PAGE_OFFSET           __PAGE_OFFSET_BASE_L4
#endif /* CONFIG_DYNAMIC_MEMORY_LAYOUT */

_AC是个宏定义,如下:

#ifdef __ASSEMBLY__
#define _AC(X,Y)	X
#define _AT(T,X)	X
#else
#define __AC(X,Y)	(X##Y)
#define _AC(X,Y)	__AC(X,Y)
#define _AT(T,X)	((T)(X))
#endif

即,__PAGE_OFFSET展开为0xffff8880000000000xff11000000000000。但是,为什么虚拟地址减去__PAGE_OFFSET就是物理地址? 我们在Documentation/x86/x86_64/mm.rst找到相关答案。

#Complete virtual memory map with 4-level page tables
ffff888000000000 | -119.5  TB | ffffc87fffffffff |   64 TB | direct mapping of all physical memory (page_offset_base)

#Complete virtual memory map with 5-level page tables
ff11000000000000 |  -59.75 PB | ff90ffffffffffff |   32 PB | direct mapping of all physical memory (page_offset_base)

以4级页表为例,0xffff888000000000 ~ 0xffffc87fffffffff这个区间直接映射了所有的物理内存。当内核访问所有的物理内存时,使用直接映射即可。

  • __early_make_pgtable

early_make_pgtable在初始化pmd后,和address一起传入__early_make_pgtable__early_make_pgtable在同一个文件中定义,如下:

/* Create a new PMD entry */
int __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;
    ...
}
  1. 改函数从定义*val_t类型的变量开始,这些所有的类型都使用typedef被声明为unsigned long的别名;

  2. 在检查物理地址有效后,在early_top_pgt获取pgd条目的地址;如下:

    again:
     pgd_p = &early_top_pgt[pgd_index(address)].pgd;
     pgd = *pgd_p;
  3. 检查是否支持5级页表,不支持5级页表的情况下,获取p4d_p = pgd_p

  4. 在支持5级页表的情况下,检查pgd是否存在。存在的话,将pgd的基地址分配给p4d_p,如下:

     	p4d_p = (p4dval_t *)((pgd & PTE_PFN_MASK) + __START_KERNEL_map - phys_base);

    PTE_PFN_MASK是一个宏定义,是(pte|pmd|pud|pgd)val_t4KB大小页掩码。

  5. pgd不存在的情况下,从不超过EARLY_DYNAMIC_PAGE_TABLES(即,64)个页表中按需设置页表; 如果超过了EARLY_DYNAMIC_PAGE_TABLES,我们重置页表,并从跳转到again重新开始。如下:

    	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;
  6. p4d_p指向正确的页表项,并将其值赋值给p4d;

  7. 重复步骤4-6;获取pud_ppmd_p;

  8. 最后,将pmd赋值给pmd_p的某个条目:

     pmd_p[pmd_index(address)] = pmd;

经过上述步骤后,early_top_pgt中包含有效地址的条目。

1.6.5 其他异常中断处理程序

在初期中断阶段,除页错误之外的其他异常,由early_fixup_exception处理,它接受两个参数 - 指向内核堆栈的指针和中断向量。

early_fixup_exceptionarch/x86/mm/extable.c中定义。如下:

/* Restricted version used during very early boot */
void __init early_fixup_exception(struct pt_regs *regs, int trapnr)
{
    ...
    ...
    ...
}
  • 必要的检查

首先,我们进行一些检查,包括:忽略NMI;确保我们没有处于递归情况;运行在正确的代码段下。

  • fixup_exception

之后,我们调用fixup_exception函数找到实际的中断处理程序并调用它。如下:

int fixup_exception(struct pt_regs *regs, int trapnr, unsigned long error_code,
		    unsigned long fault_addr)
{
	const struct exception_table_entry *e;
	ex_handler_t handler;

#ifdef CONFIG_PNPBIOS
...
#endif

	e = search_exception_tables(regs->ip);
	if (!e)
		return 0;

	handler = ex_fixup_handler(e);
	return handler(e, regs, trapnr, error_code, fault_addr);
}

ex_handler_t是一个函数指针,定义如下:

typedef bool (*ex_handler_t)(const struct exception_table_entry *,
			    struct pt_regs *, int, unsigned long,
			    unsigned long);

search_exception_tables函数在kernel/extable.c中定义,其功能是从异常表中查找给定的地址,(即,ELF文件中__ex_table部分)。

之后,通过ex_fixup_handler获取实际地址,最后,我们调用实际的处理程序。

关于异常表的更多信息,可以参考Documentation/x86/exception-tables.rst

  • fixup_bug

search_exception_tables函数在arch/x86/kernel/traps.c中定义。如下:

int fixup_bug(struct pt_regs *regs, int trapnr)
{
	if (trapnr != X86_TRAP_UD)
		return 0;

	switch (report_bug(regs->ip, regs)) {
	case BUG_TRAP_TYPE_NONE:
	case BUG_TRAP_TYPE_BUG:
		break;

	case BUG_TRAP_TYPE_WARN:
		regs->ip += LEN_UD2;
		return 1;
	}

	return 0;
}

该函数在中断向量是#UD(或者,无效操作符(Invalid Opcode))的情况下,并且report_bugBUG_TRAP_TYPE_WARN的情况下返回1,其他情况下返回0。

1.7 复制启动信息

接下来,我们调用copy_bootdata(__va(real_mode_data));函数,复制boot_paramsboot_command_linecopy_bootdataarch/x86/kernel/head64.c文件中定义。

首先,我们来看下__va的定义,__vaarch/x86/include/asm/page.h中定义,如下:

#ifndef __va
#define __va(x)			((void *)((unsigned long)(x)+PAGE_OFFSET))
#endif

PAGE_OFFSET在上节提到,即__PAGE_OFFSET,是虚拟地址与物理地址之间映射的偏移量。

boot_paramsarch/x86/kernel/setup.c中定义,boot_command_lineinit/main.c定义。

copy_bootdata在复制boot_params时,调用sanitize_boot_params(&boot_params);函数,填充引导阶段未能正常初始化boot_params中的一些字段,比如:ext_ramdisk_image等。

get_cmd_line_ptr函数获取命令行的64位地址,如下:

static unsigned long get_cmd_line_ptr(void)
{
	unsigned long cmd_line_ptr = boot_params.hdr.cmd_line_ptr;
	cmd_line_ptr |= (u64)boot_params.ext_cmd_line_ptr << 32;
	return cmd_line_ptr;
}

1.8 加载早期微代码

Microcode是CPU和指令集之间的一层组件技术,用于调整和更改CPU电路状态。这里调用load_ucode_bsp函数加载。

1.9 内核地址映射

在前面reset_early_page_tables函数中,我们清除了大部分的页表项,只保留了内核高地址映射。并且通过clear_page(init_top_pgt)函数将init_top_pgt全部清零。现在将init_top_pgt最后一项设置为内核高地址映射。

init_top_pgt[511] = early_top_pgt[511];

1.10 调用x86_64_start_reservations

经过上面的初始化后,调用x86_64_start_reservations进行后续初始化。

2 平台相关设置(x86_64_start_reservations

x86_64_start_reservations同样在arch/x86/kernel/head64.c中定义,如下:

void __init x86_64_start_reservations(char *real_mode_data)
{
	/* version is always not zero if it is copied */
	if (!boot_params.hdr.version)
		copy_bootdata(__va(real_mode_data));

	x86_early_init_platform_quirks();

	switch (boot_params.hdr.hardware_subarch) {
	case X86_SUBARCH_INTEL_MID:
		x86_intel_mid_early_setup();
		break;
	default:
		break;
	}

	start_kernel();
}

2.1 检查并复制启动信息

首先,检查boot_params.hdr.version信息,如果不存在,再次调用copy_bootdata

2.2 x86平台早期初始化

接下来,调用x86_early_init_platform_quirks,在arch/x86/kernel/platform-quirks.c中定义,实现如下:

void __init x86_early_init_platform_quirks(void)
{
	x86_platform.legacy.i8042 = X86_LEGACY_I8042_EXPECTED_PRESENT;
	x86_platform.legacy.rtc = 1;
	x86_platform.legacy.warm_reset = 1;
	x86_platform.legacy.reserve_bios_regions = 0;
	x86_platform.legacy.devices.pnpbios = 1;

	switch (boot_params.hdr.hardware_subarch) {
	case X86_SUBARCH_PC:
		x86_platform.legacy.reserve_bios_regions = 1;
		break;
	case X86_SUBARCH_XEN:
		x86_platform.legacy.devices.pnpbios = 0;
		x86_platform.legacy.rtc = 0;
		break;
	case X86_SUBARCH_INTEL_MID:
	case X86_SUBARCH_CE4100:
		x86_platform.legacy.devices.pnpbios = 0;
		x86_platform.legacy.rtc = 0;
		x86_platform.legacy.i8042 = X86_LEGACY_I8042_PLATFORM_ABSENT;
		break;
	}

	if (x86_platform.set_legacy_features)
		x86_platform.set_legacy_features();
}

可以看到,改函数是对x86_platform字段进行初始化。x86_platformarch/x86/include/asm/x86_init.h声明,

struct x86_platform_ops {
	unsigned long (*calibrate_cpu)(void);
	unsigned long (*calibrate_tsc)(void);
	void (*get_wallclock)(struct timespec64 *ts);
	int (*set_wallclock)(const struct timespec64 *ts);
	void (*iommu_shutdown)(void);
	bool (*is_untracked_pat_range)(u64 start, u64 end);
	void (*nmi_init)(void);
	unsigned char (*get_nmi_reason)(void);
	void (*save_sched_clock_state)(void);
	void (*restore_sched_clock_state)(void);
	void (*apic_post_init)(void);
	struct x86_legacy_features legacy;
	void (*set_legacy_features)(void);
	struct x86_hyper_runtime hyper;
};

...

extern struct x86_platform_ops x86_platform;

可以看到,struct x86_platform_ops是一个结构体,封装了x86架构CPU的属性信息和一些操作的回调函数。x86_early_init_platform_quirks根据不同系列CPU(如:PC,XEN,MID)设置x86_platform.legacy信息。x86_platform中的回调函数在arch/x86/kernel/x86_init.c中进行了初始化,如下:

struct x86_platform_ops x86_platform __ro_after_init = {
	.calibrate_cpu			= native_calibrate_cpu_early,
	.calibrate_tsc			= native_calibrate_tsc,
	.get_wallclock			= mach_get_cmos_time,
	.set_wallclock			= mach_set_rtc_mmss,
	.iommu_shutdown			= iommu_shutdown_noop,
	.is_untracked_pat_range		= is_ISA_range,
	.nmi_init			= default_nmi_init,
	.get_nmi_reason			= default_get_nmi_reason,
	.save_sched_clock_state 	= tsc_save_sched_clock_state,
	.restore_sched_clock_state 	= tsc_restore_sched_clock_state,
	.hyper.pin_vcpu			= x86_op_int_noop,
};

2.3 x86_intel移动平台早期初始化

接下来,判断CPU是移动平台(即,X86_SUBARCH_INTEL_MID),调用x86_intel_mid_early_setup函数进行初始化。

x86_intel_mid_early_setuparch/x86/platform/intel-mid/intel-mid.c中定义。实现过程同x86_early_init_platform_quirks类似,对x86_init, x86_cpuinit, x86_platform, legacy_pic, pm_power_off, machine_ops进行了修改或设置。

x86_init, x86_cpuinit, x86_platformarch/x86/include/asm/x86_init.h进行声明,在arch/x86/kernel/x86_init.c进行了默认初始化。

legacy_pic, default_legacy_pic, null_legacy_picarch/x86/include/asm/i8259.h进行定义,在arch/x86/kernel/i8259.c进行了默认初始化。

pm_power_offinclude/linux/pm.h进行声明,在arch/x86/kernel/reboot.c中定义。

machine_opsarch/x86/include/asm/reboot.h进行声明,在arch/x86/kernel/reboot.c进行了默认初始化。

2.4 start_kernel

在经过上述的早期初始后,我们终于完成了进入内核入口点的所有准备工作,现在进行早期初始化的最后一步,调用start_kernel进入内核入口点。

	start_kernel();

3 结束语

本文描述了Linux内核平台入口点前的初始化,主要进行中断处理函数设置和平台相关设置。

本系列文章翻译自linux-insides,如果你有任何问题或者建议,请联系0xAX或者创建 issue

如果你发现中文翻译有任何问题,请提交PR或者创建issue