MIT 6.828课程正式开始 :-D 撒花
Part 1:PC Bootstrap
这一部分主要介绍如何用qemu和gdb联调kernel :-D。
打开两个terminal,都进入到lab目录,然后其中一个输入make qemu-gdb,另一个输入make gdb,即可,你可以看到下面的画面:
刚启动时,计算机处于实模式。可以看到,当机器刚上电时候,此时PC指向的地址是0xffff0,这是硬件工程师故意这样做的。第一条指令是一个长跳转指令,这条指令是跳向BIOS的开头。你可以继续使用SI 命令继续逐条观察,这部分都是BIOS做的工作,大概是通过IO check 各种硬件(reference 里的手册真长),比较的晦涩(以及无聊,所以我大概看了看就略过了…)。
Part 2: The bootloader
BIOS做的最后一件事情是
Eventually, when it finds a bootable disk, the BIOS reads the boot loader from the disk and transfers control to it.
就是BIOS帮我们读了一个扇区(sector)的代码到0x7c00 这个地方,然后跳转到这个地方。这个扇区的代码就是JOS的bootloader。
JOS的bootloader有两部分,先是一段汇编代码,在 boot/boot.S里面,然后是一个C源代码,在boot/main.c。
我们依次来分析这两段代码。
boot/boot.S 从实模式到保护模式
正如boot.S里的注释第一句
Start the CPU: switch to 32-bit protected mode, jump into C.
这段代码的功能就是从实模式切换到保护模式。
我们跳过前面的一些准备代码(tips:boot.S这段代码里有一段是A20的设置,跳过他。
),直接看到48行左右,这才是关键
lgdt gdtdesc
movl %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0
其中第一行代码是加载一个段描述符表,这是进入保护模式前必要的一步,因为保护模式的内存机制以段机制为基础(在加上页机制),具体可以参考我写的这篇: JOS Lab2 保护模式下的内存映射机制:段机制 页机制。
这里的gdtdesc定义在最后面
gdt:
SEG_NULL # null seg
SEG(STA_X|STA_R, 0x0, 0xffffffff) # code seg
SEG(STA_W, 0x0, 0xffffffff) # data seg
gdtdesc:
.word 0x17 # sizeof(gdt) - 1
.long gdt # address gdt
这是一个最简单版本的段描述符,仅仅把内存分为数据段(data seg)和代码段(code seg)。
接下来三句代码,实现的功能就是实模式到保护模式。(其实就是改变了CR0寄存器里的一位,从0变成了1。)
关于CR0寄存器,自己去wiki看啦。
movl %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0
然后通过一个ljmp跳到了prtcseg这一段代码里
movw $PROT_MODE_DSEG, %ax # Our data segment selector
movw %ax, %ds # -> DS: Data Segment
movw %ax, %es # -> ES: Extra Segment
movw %ax, %fs # -> FS
movw %ax, %gs # -> GS
movw %ax, %ss # -> SS: Stack Segment
随便设了一些segment的base值,下一句代码
movl $start, %esp
这句代码很重要,这是进入保护模式之后第一个栈顶,$start的地址大概在0x7c00
后多一些,这个随便取的,在正式的设定之前,0-start 这段空间做为栈,应该足够了:-D。
有了栈,我们可以调用函数了!
于是下一句 call bootmain
我们进入main.c
boot/main.c 勤劳的搬运工
#define ELFHDR ((struct Elf *) 0x10000) // scratch space
首先定义了一个ELFHDR的量,ELF文件的头文件。ELF是一类文件格式的名称,我们的kernel就是ELF,后续还会有很多地方会用到这个ELF,所以你有必要搞明白他,越早越好->wiki是你的朋友。
反正这一段代码就是根据elf hear里的信息,一段一段的把kernel的代码搬运到内存,从0x10000(1M)(这里还是物理内存地址)开始。理解起来挺容易,中间的一个强制类型转换可能会引起困扰:
ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);
其中的(uint8_t *)是为了让ELFHDR指针每+1增加1而不是32位指针默认的+4。
最后一行((void (*)(void)) (ELFHDR->e_entry))();
在elfhdr中,定义了一个入口地址,call它,我们就开始运行kernel的第一行代码,从这里开始bootloader结束。
kern/entry.S 内核!内核!
哈哈哈哈,我们从boot文件夹跳到了kern文件夹,这意味着,我们终于进入内核啦!撒花~
冷静下来继续读代码。
movl $(RELOC(entry_pgdir)), %eax
movl %eax, %cr3
# Turn on paging.
movl %cr0, %eax
orl $(CR0_PE|CR0_PG|CR0_WP), %eax
movl %eax, %cr0
由于虚拟内存机制还没建立(lab2做这事儿),所以老师帮我们手写了4MB的页表,在kern/entrypgdir.c里面,大概功能就是把1M-5M这段物理地址同时映射到从0x10000和0xF0000000开始的高(虚拟)地址中(我们的内核将来就处于从这里开始的256M空间中。)把entry_pgdir加载到cr3寄存器(用来存放页表一级目录基地址的寄存器)。然后打开cr0的页机制(同时打开的还有wp,自己wiki cr0吧…)。
Now paging is enabled, but we’re still running at a low EIP
(why is this okay?).
注释里这样问,为什么可以呢?因为…老师帮我们同时map了原来的地址和加上0xf0000000两段,也就是在虚拟内存里有2个4M空间(好绕口…我实在不知道怎么表述)。
movl $0x0,%ebp # nuke frame pointer
# Set the stack pointer
movl $(bootstacktop),%esp
# now to C code
call i386_init
这里建立了虚拟地址下的栈基地址(随便定的啦…就定0好了)和栈顶地址bootstacktop。这里是建立了保护模式下(简单的)虚拟内存机制(lab2建立完整的)后的栈。于是我们可以调用函数啦,:-D。
于是我们就
call i386_init
开始初始化工作,基本就没有lab1什么事了,到lab4之前基本上都在完善i386_init里的内容,建立内存机制,建立进程(JOS里叫运行环境environment),建立多CPU啦啦啦~