概述
这儿我们以x86-64为例,通常情况下我们使用的都是压缩内核,也就是经过压缩的内核,内核外面被添加了一段自解压程序。对于压缩内核,从引导程序引导后首先运行的是那段字节压程序,其入口为arch/x86/boot/compressed/head_64.S中的startup_32。从那儿开始,将会配置解压内核所需要的环境并解压和跳转到内核。
Grub引导到内核启动各阶段CPU的控制寄存器状态如下表:
阶段 | CR0 | CR3 | CR4 | EFER |
---|---|---|---|---|
GRUB到自解压前 | 0x60000011 | 0x000000000000 | 0x00000000 | 0x00000000 |
自解压程序配置环境后 | 0x80000011 | 0x000002328000 | 0x00000020 | 0x00000500 |
自解压跳转到内核后 | 0x80000011 | 0x000002328000 | 0x00000020 | 0x00000500 |
内核配置新环境后 | 0x80050033 | 0x000001df6000 | 0x000000a0 | 0x00000d01 |
Grub引导到内核启动各阶段CPU的控制寄存器状态如下表:
阶段 | CS | DS | gdtr | idtr | ldtr |
---|---|---|---|---|---|
GRUB到自解压前 | 0x0010 | 0x0018 | 0x00000000000010b0 | 0x0000000000000000 | 0x0000 |
自解压程序配置环境后 | 0x0010 | 0x0018 | 0x0000000001639400 | 0x0000000000000000 | 0x0000 |
自解压跳转到内核后 | 0x0010 | 0x0000 | 0x0000000001639400 | 0x0000000000000000 | 0x0000 |
内核配置新环境后 | 0x0010 | 0x0000 | 0xffffffff81d74000 | 0x0000000000000000 | 0x0000 |
从上表可知,kernel引导经历了几次CPU模式配置,自解压前的配置是为了满足解压内核代码的环境要求,内核启动后由对页表和CPU模式进行了新的配置,以满足内核在虚拟地址下的运行需求。我们接下来具体看一下它是怎么完成这些的。
CPU模式的配置
引导程序完成了对内核的加载与跳转,同时还进行了一些必要的环境配置以满足其自身运行的要求和所要加载的内核的引导要求。从上表可以看到,引导程序将CR0的值配置成了0x60000011。
控制寄存器https://en.wikipedia.org/wiki/Control_register
参考以上对于控制寄存器的介绍,由CR0的值我们可以确定Grub已经使得CPU进入了32位保护模式,但是还没开启分页功能;接着在自解压程序里,CPU开启了分页,CR3保存了页目录的基地址,也关闭了在上一阶段还打开着的高速缓存相关的特性。同时还设置了CR4中的PAE标志位打开了物理地址扩展,EFER寄存器的LME和LMA标志位使得CPU进入64位模式。最后有配置了CR0寄存器,打开了一些新的特性,同时还设置了CR4寄存器的PGE标志位启用了全局页表。以上就是对于linux从引导到启动经历的几次CPU模式切换。
x86架构CPU上电执行第一条指令后就进入了实模式,实模式之后有32位保护模式,64位保护模式,长模式等,我们接下来看看CPU的这些模式是如何相互切换的。
实模式到保护模式的切换
实模式下的CPU寻址能力是有限的,也限制了CPU的许多能力,所以在上电后我们需要配置CPU使得它变得更强大。
从实模式到保护模式的切换比较简单,一步就可完成,那就是将CR0.PE位置1即可进入保护模式,但是有一点需要注意的是,保护模式下采用的是分段机制进行内存寻址,所以如果没有配置号分段机制需要的段描述符表就冒然进入保护模式系统必然会跑飞的。至于具体的代码实现可以参照如下例子:
保护模式到分页模式
如果CR0.PG=0,分页机制就没有启动,处理器将会把线性地址当做物理地址处理。CR4.PAE、IA32_EFER.LME、CR0.WP、CR4.PSE、CR4.PGE、CR4.SMEP、CR4.SMAP和IA32_EFER.NXE也被处理器忽略,但当CR0.PG=1时,只要CR0.PE=1分页机制就被开启。分页有三种模式,
32位分页模式
如果CR0.PG=1与CR4.PAE=0,CPU就会采用32位分页模式
PAE分页模式
如果CR0.PG=1,CR4.PAE=1和IA32_EFER.LME=0,CPU就会采用 PAE分页模式
IA-32e分页模式
如果CR0.PG=1,CR4.PAE=1和IA32_EFER.LME=1,CPU就会采用 IA-32e分页模式
下图展示了开启与切换分页模式的转换图,
这张图清晰展示了个分页模式是怎么切换的,其中顺着箭头不能相关联的模式之间不能切换,这就影响到了我们设置控制寄存器标志位的先后顺序。比如在32位分页模式下,如果我们设置IA32_EFER.LME标志位为1,那就会产生一个常规保护异常(#GP(0)),
各个分页模式下的一些控制行为由如下的控制位决定,CR0.WP、 CR4.PSE、CR4.PGE、CR4.PCIDE、CR4.SMEP、CR4.SMAP、CR4.PKE、IA32_EFER.NXE。
<
ENTRY(startup_32)
/* Load new GDT with the 64bit segments using 32bit descriptor */
leal gdt(%ebp), %eax
movl %eax, gdt+2(%ebp)
lgdt gdt(%ebp)
/* Enable PAE mode */
movl %cr4, %eax
orl $X86_CR4_PAE, %eax
movl %eax, %cr4
/*
* Build early 4G boot pagetable
*/
/* Initialize Page tables to 0 */
leal pgtable(%ebx), %edi
xorl %eax, %eax
movl $(BOOT_INIT_PGT_SIZE/4), %ecx
rep stosl
/* Build Level 4 */
leal pgtable + 0(%ebx), %edi
leal 0x1007 (%edi), %eax
movl %eax, 0(%edi)
/* Build Level 3 */
leal pgtable + 0x1000(%ebx), %edi
leal 0x1007(%edi), %eax
movl $4, %ecx
1: movl %eax, 0x00(%edi)
addl $0x00001000, %eax
addl $8, %edi
decl %ecx
jnz 1b
/* Build Level 2 */
leal pgtable + 0x2000(%ebx), %edi
movl $0x00000183, %eax
movl $2048, %ecx
1: movl %eax, 0(%edi)
addl $0x00200000, %eax
addl $8, %edi
decl %ecx
jnz 1b
/* Enable the boot page tables */
leal pgtable(%ebx), %eax
movl %eax, %cr3
/* Enable Long mode in EFER (Extended Feature Enable Register) */
movl $MSR_EFER, %ecx
rdmsr
btsl $_EFER_LME, %eax
wrmsr
/* After gdt is loaded */
xorl %eax, %eax
lldt %ax
movl $__BOOT_TSS, %eax
ltr %ax
pushl $__KERNEL_CS
leal startup_64(%ebp), %eax
pushl %eax
/* Enter paged protected Mode, activating Long Mode */
movl $(X86_CR0_PG | X86_CR0_PE), %eax /* Enable Paging and Protected mode */
movl %eax, %cr0
/* Jump from 32bit compatibility mode into 64bit mode. */
lret
ENDPROC(startup_32)
从以上代码可看出,Linux先设置了CR4.PAE,然后再设置IA-32e.EFER.LME,最后设置CR0.PG,这个路径正好对应上面模式转换图切换到IA-32e分页模式的切换路径,当然PAE与LME设置的先后是无影响的。
上图展示了CR3及各级页表项的结构,这可以帮助我们了解页表构建过程中为什么需要填充那些值。有兴趣的可以参照上图的结构定义对照着去解析上面代码中页表构建过程每一步的意义。
我们通过最初的观察发现linux启动过程中经历了几次控制寄存器值的变化,查阅代码会发现,从开启IA-32e分页模式后,CPU就一直工作在了这个模式,但是还有很多功能特性没有打开,所以在之后的代码中逐步配置并打开了相应的功能特性,如CR4.PGE,CR4.PSE等,正是因为这样才出现了进入IA-32e分页模式后控制寄存器仍然在发生变化。
对于cpu来讲,进入IA-32e分页模式后,大体的配置基本就已经完成了,但是对于linux来讲还有一个最重要的步骤就是使得内核代码运行在内核地址空间,也就是我们所谓的内核虚拟地址空间。这关键的步骤其实就是通过建立相应的页表映射结构完成的,内核代码实际存放的物理地址不变,但在页表映射中将其映射到内核虚拟地址空间中,这样在用线性地址寻址是就能够找到对应的物理地址。
以上就是Linux在平台初始化过程中做的最重要的工作,当然还有中断门,系统调用门的设置,这将在以后说到。每个平台可能硬件特性有差别,但一些最基本的处理原则应该都是一致的,那就是内存寻址,因为只有能够完全操作内存后,我们才能依靠CPU去做更多的事。