bootloader作用
一句话描述: 加载内核到内存,建立内存空间的映射,将系统的控制权交给内核并且运行,PC的BIOS和bootloader分开了(先BIOS后bootloader),嵌入式一般一起集成到了bootloader
组成
XV6上的bootloader分为bootasm.S与bootmain.c
加载流程
bootasm.S
处理器在BIOS运行完成后运行bootasm.S,并且处于实模式(只能访问20位的地址空间1MB),有8个16位的寄存器可用,但是处理器需要发送的是20位的地址,因此地址的计算方式为segment:offset, segment由cs,es,ds,ss四个寄存器提供,bootasm.S的操作如下
.code16 # Assemble for 16-bit mode
.globl start
start:cli # BIOS enabled interrupts; disable# Zero data segment registers DS, ES, and SS.xorw %ax,%ax # Set %ax to zeromovw %ax,%ds # -> Data Segmentmovw %ax,%es # -> Extra Segmentmovw %ax,%ss # -> Stack Segment
前面提到的地址计算方式中由于segment<<4 + offset中的segment和offset都是16位的,因此最大值为0xffff0 + 0xffff = 0x10ffef也即21位,因此bootasm.S需要激活第21位的地址线,这是通过IO端口的输出实现的(需要0x64口和0x60为0xd1和0xdf)
相应的代码如下
seta20.1:inb $0x64,%al # Wait for not busytestb $0x2,%aljnz seta20.1movb $0xd1,%al # 0xd1 -> port 0x64outb %al,$0x64seta20.2:inb $0x64,%al # Wait for not busytestb $0x2,%aljnz seta20.2movb $0xdf,%al # 0xdf -> port 0x60outb %al,$0x60
在激活了A20之后,需要转入到保护模式,指定临时的GDT代码如下,这里将cr0寄存器的数值或上了CR0_PE,打开了保护模式
lgdt gdtdesc
movl %cr0, %eax
orl $CR0_PE, %eax
movl %eax, %cr0
之后需要填充cs,寄存器的值,cs寄存器的值无法直接设置,使用ljmp汇编命令设置
ljmp $(SEG_KCODE<<3), $start32start32:# Set up the protected-mode data segment registersmovw $(SEG_KDATA<<3), %ax # Our data segment selectormovw %ax, %ds # -> DS: Data Segmentmovw %ax, %es # -> ES: Extra Segmentmovw %ax, %ss # -> SS: Stack Segmentmovw $0, %ax # Zero segments not ready for usemovw %ax, %fs # -> FSmovw %ax, %gs # -> GS# SEG_KCODE被定义成1,并且翻译成段选择子的意义之后代表着offset为0,选择gdt中的第一项
最后设置栈顶寄存器的值,这里直接设置成了$start,也即0x7c00(引导器自己的位置,栈向下生长至0x0000,会离引导器越来越远)最后调用bootmain,代码如下
# Set up the stack pointer and call into C.movl $start, %espcall bootmain
bootmain.c
bootmain.c的作用就是从磁盘上加载内核的ELF的前4096个字节并且拷贝到0x10000处,代码如下
void
bootmain(void)
{struct elfhdr *elf;struct proghdr *ph, *eph;void (*entry)(void);uchar* pa;elf = (struct elfhdr*)0x10000; // scratch space// Read 1st page off diskreadseg((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();
}
而entry.S的定义如下
entry:# Turn on page size extension for 4Mbyte pagesmovl %cr4, %eaxorl $(CR4_PSE), %eaxmovl %eax, %cr4# Set page directorymovl $(V2P_WO(entrypgdir)), %eaxmovl %eax, %cr3# Turn on paging.movl %cr0, %eaxorl $(CR0_PG|CR0_WP), %eaxmovl %eax, %cr0# Set up the stack pointer.movl $(stack + KSTACKSIZE), %esp# Jump to main(), and switch to executing at# high addresses. The indirect call is needed because# the assembler produces a PC-relative instruction# for a direct jump.mov $main, %eaxjmp *%eax
开启了分页并且跳转到了main函数开启了内核的运行
习题
问:基于扇区大小,文中提到的调用 readseg 的作用和 readseg((uchar*)0x100000, 0x1000, 0xb500) 的作用是相同的。实际上,这个草率的实现并不会导致错误。这是为什么呢?
答由于扇区为512字节大小,b500为512的整数倍,会被忽略
问:假设你希望 bootmain() 能把内核加载到 0x200000 而非 0x100000,于是你在 bootmain() 中把每个 ELF 段的 va 都加上了 0x100000。这样做是会导致错误发生的,请说明会发生什么错误。
答:
分页机制尚未被开启。在kernel.ld中指明了内核的paddr是0x00100000,kernel.asm中也有类似的定义,不能简单的这样设置
参考文献
https://th0ar.gitbooks.io/xv6-chinese/content/content/AppendixB.html