目录
- 虚拟地址转换为物理地址
- 内核启动
- Multiboot头部结构
- 启动时的寄存器状态
- real_start
- 段选择子
- 初始化BSS段
- 页表转换设置
- CR4、CR3、EFER寄存器设置
- 页表映射
- 初始化IDT,执行lk_main
虚拟地址转换为物理地址
// start.S
#define PHYS_LOAD_ADDRESS (MEMBASE + KERNEL_LOAD_OFFSET)
#define PHYS_ADDR_DELTA (KERNEL_BASE + KERNEL_LOAD_OFFSET - PHYS_LOAD_ADDRESS)
#define PHYS(x) ((x) - PHYS_ADDR_DELTA)
PHYS(x) 将x转换为物理地址
内核启动
Multiboot头部结构
// start.S
.section ".text.boot"
.code32
.global _start
_start:
jmp real_start
.align 8
/* flags for multiboot header */
#define MULTIBOOT_HEADER_FLAGS (MULTIBOOT_PAGE_ALIGN | MULTIBOOT_MEMORY_INFO | MULTIBOOT_AOUT_KLUDGE)
//MULTIBOOT_PAGE_ALIGN 0x00000001 MULTIBOOT_MEMORY_INFO 0x00000002 MULTIBOOT_AOUT_KLUDGE 0x00010000
.type multiboot_header,STT_OBJECT
multiboot_header:
/* magic */
.int MULTIBOOT_HEADER_MAGIC
/* flags */
.int MULTIBOOT_HEADER_FLAGS
/* checksum */
.int -(MULTIBOOT_HEADER_MAGIC + MULTIBOOT_HEADER_FLAGS)
/* header_addr */
.int PHYS(multiboot_header)
/* load_addr */
.int PHYS(_start)
/* load_end_addr */
.int PHYS(__data_end)
/* bss_end_addr */
.int PHYS(__bss_end)
/* entry_addr */
.int PHYS(real_start)
刚启动时,使用32位指令集,MULTIBOOT_HEADER_FLAGS
指定启动加载程序的功能,此处设置了4K字节对齐、multiboot_info需要包含mem_*字段以及设Multiboot偏移12-28处的字段有效
图中代码在multiboot.h
图中代码在start.S
Multiboot header地址含义可以参考Multiboot技术文档3.1.3小节,Multiboot_info可参考3.3小节
启动时的寄存器状态
Multiboot协议规定,EAX = 0x2BADB002(魔数) 表明操作系统是被符合Multiboot的加载程序进行加载的,此外Multiboot协议规定,EBX必须包含Multiboot_info的32位物理地址。有关机器启动时的状态可参考文档3.2小节。
real_start
// start.S
real_start:
cmpl $MULTIBOOT_BOOTLOADER_MAGIC, %eax
jne 0f
movl %ebx, PHYS(_multiboot_info)
0:
/* load our new gdt by physical pointer */
lgdt PHYS(_gdtr_phys)
/* load our data selectors */
movw $DATA_SELECTOR, %ax
movw %ax, %ds
movw %ax, %es
movw %ax, %fs
movw %ax, %ss
movw %ax, %gs
movw %ax, %ss
/* load initial stack pointer */
movl $PHYS(_kstack + 4096), %esp
/* far jump to load the CS from our GDT */
pushl $CODE_SELECTOR
pushl $PHYS(.Lfarjump)
lret
在real_start开始部分,先检查EAX中的值是否等于Multiboot魔数,等于则将EBX的值加载到multiboot_info的物理地址,否则直接跳转到标号0处执行。
将全局描述符表gdt的物理地址加载到GDTR中,然后将段寄存器的值设置为DATA_SELECTOR = 0x10 = 0001 0000
段选择子
Requestor Privilege-Level (RPL)表示处理器正在运行的特权级别
Table Indicator (TI)表示选择哪个描述符表,TI=0使用GDT,TI=1使用LDT
Selector Index Field(SI)表示索引
因此DATA_SELECTOR = 0x10 = 0001 0000
表示CPL=0,即最高权限;使用GDT,Index为2,使用GDT中的第二个段描述符
图中代码在gdt.S
段寄存器设置好后,用一个4K的数组作为栈,数组末尾作为栈顶,然后将CODE_SELECTOR和.Lfarjump的物理地址压栈,再跳转到.Lfarjump处运行。
CODE_SELECTOR = 0x08 = 0000 1000
即选择GDT的第一个段描述符
初始化BSS段
//start.S
.Lfarjump:
/* zero the bss section */
bss_setup:
movl $PHYS(__bss_start), %edi /* starting address of the bss */
movl $PHYS(__bss_end), %ecx /* find the length of the bss in bytes */
subl %edi, %ecx
shrl $2, %ecx /* convert to 32 bit words, since the bss is aligned anyway */
2:
movl $0, (%edi)
addl $4, %edi
loop 2b
初始化BSS段,其中_bss_start, _bss_end在kernel.ld文件中,在链接阶段分配地址
页表转换设置
CR4、CR3、EFER寄存器设置
//start.S
paging_setup:
/* Preparing 64 bit paging. We will use 2MB pages covering 1GB
* for initial bootstrap, this page table will be 1 to 1.
*/
/* PAE bit must be enabled for 64 bit paging*/
mov %cr4, %eax
btsl $(5), %eax
mov %eax, %cr4
/* load the physical pointer to the top level page table */
movl $PHYS(kernel_pml4), %eax
mov %eax, %cr3
/* Long Mode Enabled at this point*/
movl $MSR_EFER ,%ecx
rdmsr
orl $EFER_LME,%eax
wrmsr
将CR4位5置1,启用PAE(Physical-Address Extensions),并将PML4的地址存储在CR3中,然后设置MSR_EFER寄存器,启用长模式。
//start.S
#define MSR_EFER 0xc0000080
#define EFER_LME 0x00000100
MSR_EFER = 0xc0000080
看似是一个宏定义,其实是EFER寄存器的地址(在AMD手册3.1.7中给出)
页表映射
//mmu.c
/* top level kernel page tables, initialized in start.S */
map_addr_t kernel_pml4[NO_OF_PT_ENTRIES] __ALIGNED(PAGE_SIZE);
map_addr_t kernel_pdp[NO_OF_PT_ENTRIES] __ALIGNED(PAGE_SIZE); /* temporary */
map_addr_t kernel_pte[NO_OF_PT_ENTRIES] __ALIGNED(PAGE_SIZE);
/* top level pdp needed to map the -512GB..0 space */
map_addr_t kernel_pdp_high[NO_OF_PT_ENTRIES] __ALIGNED(PAGE_SIZE);
/* a big pile of page tables needed to map 64GB of memory into kernel space using 2MB pages */
map_addr_t kernel_linear_map_pdp[(64ULL*GB) / (2*MB)];
kernel_pml4、kernel_pdp、kernel_pte都是一个4K大小的数组,kernel_linear_map_pdp是一个4*64K大小的数组,在这里的作用是作为64个4K的kernel_pte
//start.S
/* Setting the First PML4E with a PDP table reference at index 0 */
movl $PHYS(kernel_pdp), %eax
orl $X86_KERNEL_PD_FLAGS, %eax
movl %eax, PHYS(kernel_pml4)
/* Setting the First PDPTE with a Page table reference at index 0 */
movl $PHYS(kernel_pte), %eax
orl $X86_KERNEL_PD_FLAGS, %eax
movl %eax, PHYS(kernel_pdp)
/* point the pml4e at the second high PDP (for -2GB mapping) at index 511 */
movl $PHYS(kernel_pdp_high), %eax
orl $X86_KERNEL_PD_FLAGS, %eax
movl %eax, PHYS(kernel_pml4 + 8*511)
/* point the second pdp at the same low level page table */
movl $PHYS(kernel_pte), %eax
orl $X86_KERNEL_PD_FLAGS, %eax
movl %eax, PHYS(kernel_pdp_high + 8*510)
/* map the first 1GB in this table */
movl $PHYS(kernel_pte), %esi
movl $0x200, %ecx /* 512 entries */
xor %eax, %eax /* start off at address 0 */
0:
mov %eax, %ebx
shll $21, %ebx
orl $X86_KERNEL_PD_LP_FLAGS, %ebx
movl %ebx, (%esi)
addl $8,%esi
inc %eax
loop 0b /* dec ecx and loop while > 0 */
使用的是2M的页表,实际上kernel_pte换成kernel_pde会更好,但只是个名字,并不影响实际运行。映射的结果如图:
/* set up a linear map of the first 64GB at 0xffffff8000000000 */
movl $PHYS(kernel_linear_map_pdp), %esi
movl $32768, %ecx
xor %eax, %eax
/* loop across these page tables, incrementing the address by 2MB */
0:
mov %eax, %ebx
shll $21, %ebx
orl $X86_KERNEL_PD_LP_FLAGS, %ebx # lower word of the entry
movl %ebx, (%esi)
mov %eax, %ebx
shrl $11, %ebx # upper word of the entry
movl %ebx, 4(%esi)
addl $8,%esi
inc %eax
loop 0b
/* point the high pdp at our linear mapping page tables */
movl $PHYS(kernel_pdp_high), %esi
movl $64, %ecx
movl $PHYS(kernel_linear_map_pdp), %eax
orl $X86_KERNEL_PD_FLAGS, %eax
0:
movl %eax, (%esi)
add $8, %esi
addl $4096, %eax
loop 0b
/* Enabling Paging and from this point we are in 32 bit compatibility mode */
mov %cr0, %eax
btsl $(31), %eax
mov %eax, %cr0
页表初始化完成后,通过CR0启动页表,映射的结果如下,其中PA大小是64G,内核区为1G。图中有个问题,PA前1G和内核是同一个区域,并不是分开的
初始化IDT,执行lk_main
/* Use a far jump to get into 64bit mode */
pushl $CODE_64_SELECTOR
pushl $PHYS(farjump64)
lret
.align 8
.code64
farjump64:
/* branch to our high address */
mov $highaddr, %rax
jmp *%rax
highaddr:
/* load the high kernel stack */
mov $(_kstack + 4096), %rsp
/* reload the gdtr */
lgdt _gdtr
/* set up the idt */
call setup_idt
/* call the main module */
call lk_main
0: /* just sit around waiting for interrupts */
hlt /* interrupts will unhalt the processor */
pause
jmp 0b /* so jump back to halt to conserve power */
最后将CODE_64_SELECTOR和farjump64物理地址压栈,CODE_64_SELECTOR = 0x28 = 0010 1000
,选择GDT第5个段描述符。
重新初始化栈顶以及GDTR,并调用setup_idt初始化IDT,以及调用lk_main。
此时因为已经启用页表,所以不再使用物理地址,而是逻辑地址。