内核初始化
根据博客的内容,我大致画了计算机启动时内核初始化的流程图
计算机启动时存放在ROM中的BIOS程序从磁盘中的第一个扇区(引导扇区)读取程序,加载到内存地址为0x7c00处,然后设置程序计数器%ip,跳转至该地址,执行BootLoader(引导加载器)。BootLoader负责从实模式切换到保护模式并且将存在存储设备的操作系统二进制文件读入内存,最后将控制权交给操作系统。
XV6文档提到,xv6的BootLoader包含一个16位的汇编代码文件bootasm.s和C程序文件bootmain.c
设置A20地址线使得CPU完全使用所有地址总线。通过源码可以看出,xv6是通过0x64和0x60端口的,看端口的第二位是否为1,如果为1则该位就可以继续使用。
;写端口数据设置A20地址线
seta20.1:
inb $0x64,%al
testb $0x2,%al
jnz seta20.1
movb $0xd1,%al
outb %al,$0x64
seta20.2:
inb $0x64,%al
testb $0x2,%al
jnz seta20.2
movb $0xdf,%al
outb %al,$0x60
从实模式切换到保护模式。
在实模式下,段寄存器 保存段描述表的索引。段描述表内,每一条记录段的基物理地址,界限,权限位。
上面是段描述表,从代码可以看出,有两个段,一个代码段一个数据段,并且他们都是从物理地址值的0开始,段大小都是4GB,线性地址=段地址+偏移,所以此时线性地址等于虚拟地址。也就是说xv6实际上并没有使用段
;载入GDT描述符到寄存器,开启保护模式
lgdt gdtdesc
movl %cr0, %eax
orl $CR0_PE, %eax
movl %eax, %cr0
ljmp $(SEG_KCODE<<3), $start32
值得说明的一点是,这时候并没有允许分页硬件工作,所以线性地址就是物理地址。
然后初始化栈,因为调用C语言的函数需要用栈。xv6的BootLoader将0x7c00处设为临时栈。可以从代码看出这个start就是0x7c00,BootyLoader是从0x7c00开始向上增长,而栈是向下增长,所以并不会重合。
设置好临时栈后,调用bootmain函数,他的任务就是加载并运行内核。
elf = (struct elfhdr*)0x10000; // scratch space
// Read 1st page off disk
readseg((uchar*)elf, 4096, 0);
// Is this an ELF executable?
if(elf->magic != ELF_MAGIC)
return; // let bootasm.S handle error
// Load each program segment (ignores ph flags).
ph = (struct proghdr*)((uchar*)elf + elf->phoff);
eph = ph + elf->phnum;
for(; ph < eph; ph++){
pa = (uchar*)ph->paddr;
readseg(pa, ph->filesz, ph->off);
if(ph->memsz > ph->filesz)
stosb(pa + ph->filesz, 0, ph->memsz - ph->filesz);
}
// Call the entry point from the ELF header.
// Does not return!
entry = (void(*)(void))(elf->entry);
entry();
bootmain函数的过程流程图说的很清楚,唯一要提的一点是最后,通过入口地址将控制权交给内核,调用entry();进入内核,而那个入口地址,就是entry的地址。通过文档知道,查看kernel发现开始地址是0x0010000c
进入内核后,此时内核现在存在于内存的低地址处,而内核的虚拟地址是在高地址(0x80000000),所以之后需要就分页。
先设置页表,并开始分页。
movl %cr4, %eax
orl $(CR4_PSE), %eax
movl %eax, %cr4
#Set page directory
movl $(V2P_WO(entrypgdir)), %eax
movl %eax, %cr3
###### Turn on paging.
movl %cr0, %eax
orl $(CR0_PG|CR0_WP), %eax
movl %eax, %cr0
页表的物理地址entrypgdir就存在%CR3寄存器中
这是entrypgdir的内容
_attribute__((__aligned__(PGSIZE)))
pde_t entrypgdir[NPDENTRIES] = {
// Map VA's [0, 4MB) to PA's [0, 4MB)
[0] = (0) | PTE_P | PTE_W | PTE_PS,
// Map VA's [KERNBASE, KERNBASE+4MB) to PA's [0, 4MB)
[KERNBASE>>PDXSHIFT] = (0) | PTE_P | PTE_W | PTE_PS,
};
可以看出虚拟地址(线性地址)-KERNBASE(0x80000000) = 物理地址 ,页表将内核虚拟地址的4Mb内存映射到物理地址的低内存。
开启页表后,就调用main函数,进行一系列的初始化。
int main(void)
{
kinit1(end, P2V(4*1024*1024)); // phys page allocator
kvmalloc(); // kernel page table
mpinit(); // detect other processors
lapicinit(); // interrupt controller
seginit(); // segment descriptors
cprintf("\ncpu%d: starting xv6\n\n", cpunum());
picinit(); // another interrupt controller
ioapicinit(); // another interrupt controller
consoleinit(); // console hardware
uartinit(); // serial port
pinit(); // process table
tvinit(); // trap vectors
binit(); // buffer cache
fileinit(); // file table
ideinit(); // disk
if(!ismp)
timerinit(); // uniprocessor timer
startothers(); // start other processors
kinit2(P2V(4*1024*1024), P2V(PHYSTOP)); // must come after startothers()
userinit(); // first user process
mpmain(); // finish this processor's setup
}