BootLoader

  • 查看对应的文件使用runoff.list文件

计算机启动时的硬件动作

  • PC 机上电时运行的第一条指令总是存储在 ROM 中的 BIOS 指令,BIOS 固件对硬件进行自检然后按照规范总是从磁盘的中的第一个扇区载入程序,并将其放入 0x07c00 地址处,一般情况下这个便是 BootLoader,有些 BootLoader 较大无法用一个扇区存放,所以一般会分为好几部分,由最初的部分将它们载入到内存,然后将控制权交给 BootLoader。

设置 A20 地址线

  # Physical address line A20 is tied to zero so that the first PCs
# with 2 MB would run software that assumed 1 MB. Undo that.
seta20.1:
inb $0x64,%al # Wait for not busy
testb $0x2,%al
jnz seta20.1

movb $0xd1,%al # 0xd1 -> port 0x64
outb %al,$0x64

seta20.2:
inb $0x64,%al # Wait for not busy
testb $0x2,%al
jnz seta20.2

movb $0xdf,%al # 0xdf -> port 0x60
outb %al,$0x60

切换到保护模式

  • 在保护模式下,cs 等段寄存器作为索引值存在的,cs 的值作为索引在 GDT(全局描述符表)中找到对应的段描述符,段描述符记录着段的起始地址,线性地址便由段起始地址+偏移组成
    xv6 在 BootLoader 下首先设置了临时的 GDT:

    # Bootstrap GDT
    .p2align 2 # force 4 byte alignment
    gdt:
    SEG_NULLASM # null seg
    SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg
    SEG_ASM(STA_W, 0x0, 0xffffffff) # data seg

    gdtdesc:
    .word (gdtdesc - gdt - 1) # sizeof(gdt) - 1
    .long gdt # address gdt
  • BootLoader 只划分了两个段,一个是 04G 的代码段,可执行,可读,另一个是 04G 的数据段,可写,两个段的起始地址都是 0,于是进程中的虚拟地址直接等于线性地址。
    GDT 准备好了,接下来便可以载入 GDT 描述符到寄存器并开启保护模式,代码如下:

    # Switch from real to protected mode.  Use a bootstrap GDT that makes
    # virtual addresses map directly to physical addresses so that the
    # effective memory map doesn't change during the transition.
    lgdt gdtdesc
    movl %cr0, %eax
    orl $CR0_PE, %eax
    movl %eax, %cr0
  • 但是此时指令仍然是实模式下的 16 位代码,在汇编文件中用.code16 标识,这时通过长跳转跳至 32 位代码:

    # Complete the transition to 32-bit protected mode by using a long jmp
    # to reload %cs and %eip. The segment descriptors are set up with no
    # translation, so that the mapping is still the identity mapping.
    ljmp $(SEG_KCODE<<3), $start32
  • 注意:此时并没有设置分页机制,地址空间是虚拟地址——>物理地址

调入 C 函数

  • 注意:但是在进入 C 函数前有个问题是,C 函数需要使用栈,此时栈并未初始化,BootLoader 将开始(%ip=7c00)处的0x07c00设置为临时用的调用栈,然后进入 C 函数 bootmain

    # Set up the stack pointer and call into C.
    movl $start, %esp
    call bootmain
  • bootmain 函数只做一件事:将存放在硬盘的内核载入内存

通过将 elf 载入内存然后通过 elf 头的信息得到每个 Program Header 的加载地址,然后通过读扇区将内核载入内存,最后通过入口地址将控制权交给内核。

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 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();
}
  • 内核二进制文件是 ELF 格式的,所以 bootmain 通过 elf 文件格式可以得到内核的程序入口,在说明 ELF 文件格式之前,必须要知道内核二进制文件到底是如何链接的,打开 kernel.ld 文件,可以发现,内核入口地址为标号_start 地址

    OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
    OUTPUT_ARCH(i386)
    ENTRY(_start)
  • 这个_start的地址其实是在内核代码文件 entry.S 是内核入口虚拟地址 entry 对应的物理地址,由于此时虚拟地址直接等于物理地址,_start 将作为 ELF 文件头中的elf->entry的值。

  • 内核文件中加载地址和链接地址是不一样的,链接地址是程序中所有标号、各种符号的地址,一般也就是内存中的虚拟地址,但是加载地址是为了在生成 ELF 文件时,指定各个段应该为加载的物理地址,这个地址作为每个段的 p->paddr 的值。