xv6 启动过程

开机启动

主要是 BIOS 将控制权交给 bootloader,进而进入 OS 内核的过程。这部分是 Intel x86 特有的,不用特别在意,看不懂也没关系。

开机启动后,会将 0x55aa 标记出的引导扇区复制到内存的 0x7c00 在实模式执行。即执行 bootasm.S

bootasm.S

这个程序做的事情:

  1. start 节:关闭中断,清理 AX, DS, ES, SS 寄存器。
  2. seta20 节:开启 A20 地址线,从而能够访问 1MB 以上的内存空间。
  3. start32 节:进入保护模式。之后 call bootmain
  4. gdt 节 初始化 GDT

bootmain

这个程序就是 bootloader。作用:

  1. 把磁盘上的 ELF 格式内核读到内存。
  2. 跳转到内核的入口执行。

对于理解这个程序的底层原理,我们可以瞄一眼 x86.h

// Routines to let C code use special x86 instructions.

这里对常用的汇编指令都进行了封装。

读磁盘扇区 readsect 的底层原理是 out/in 指令.

bootmain.c 58:

// Read a single sector at offset into dst.
void
readsect(void *dst, uint offset)
{
  // Issue command.
  waitdisk();
  outb(0x1F2, 1);   // count = 1
  outb(0x1F3, offset);
  outb(0x1F4, offset >> 8);
  outb(0x1F5, offset >> 16);
  outb(0x1F6, (offset >> 24) | 0xE0);
  outb(0x1F7, 0x20);  // cmd 0x20 - read sectors

  // Read data.
  waitdisk();
  insl(0x1F0, dst, SECTSIZE/4);
}
  • 0x1F2 是硬盘端口。1 代表读一个扇区(扇区:硬盘读写的基本单位,一般 512 Octet)

  • 28 位的扇区号分成 4 段,分别写入端口 0x1f3 ~ 0x1f6 号端口

  • 端口 0x1f7 写入 0x20,表示读命令。

x86.h 21:

static inline void
outb(ushort port, uchar data)
{
  asm volatile("out %0,%1" : : "a" (data), "d" (port));
}

waitdisk 用于等待读完成:

bootmain.c 50:

void
waitdisk(void)
{
  // Wait for disk ready.
  while((inb(0x1F7) & 0xC0) != 0x40)
    ;
}

原理:

硬盘工作时,将 0x1f7 端口的第 7 位 SET,表明 BUSY。完成后,位清零。所以 CPU 可以反复读取此端口,直到为 0. (0x40 =01000000,第 7 位)

x86.h 12:

static inline void
insl(int port, void *addr, int cnt)
{
  asm volatile("cld; rep insl" :
               "=D" (addr), "=c" (cnt) :
               "d" (port), "0" (addr), "1" (cnt) :
               "memory", "cc");
}
  • insl 是啥?

    Opcode Instruction Clocks Description Example
    6C insb 15,pm=9*/29** Input byte from port DX into ES:(E)DI insb
    6D insw 15,pm=9*/29** Input word from port DX into ES:(E)DI insw
    6D insl 15,pm=9*/29** Input dword from port DX into ES:(E)DI insl
  • rep 前缀用于重复执行 insl,重复的次数由 ecx 决定

以后咱们尽量少谈汇编,毕竟这东西只有 x86 用,也不存在什么精妙的设计。有意思的是文件系统、内存管理、进程管理这些。

Makefile 94:

bootblock: bootasm.S bootmain.c

这俩会编译成 bootblock,而 kernel 则是另一个故事了。

进入内核

Makefile 114:

kernel: $(OBJS) entry.o entryother initcode kernel.ld

我们注意到,内核入口文件开头是一个 Multiboot Header:

Multiboot

entry.S 1:

# Multiboot header, for multiboot boot loaders like GNU Grub.
# http://www.gnu.org/software/grub/manual/multiboot/multiboot.html
#
# Using GRUB 2, you can boot xv6 from a file stored in a
# Linux file system by copying kernel or kernelmemfs to /boot
# and then adding this menu entry:
#
# menuentry "xv6" {
# 	insmod ext2
# 	set root='(hd0,msdos1)'
# 	set kernel='/boot/kernel'
# 	echo "Loading ${kernel}..."
# 	multiboot ${kernel} ${kernel}
# 	boot
# }

entry

做了:

  1. 启用 PSE 来支持 4M 页。涉及 CR4 寄存器。(默认只支持 4K Page size)
  2. 设置页目录基址。涉及 CR3 寄存器。
  3. 启用分页。设计 CR0 寄存器。
  4. 设置栈指针。复制到 esp 即可。
  5. 跳转到 main ()

img

main

main 就干了两件事情:

  1. 对各个模块进行初始化
  2. 准备第一个用户进程。
  3. 开始调度。

main

模块初始化

包括:

  • 内核内存(KMEM)初始化

  • 多处理器(MP)初始化

  • LAPIC 初始化。LAPIC 是 Local APIC 的缩写。用于管理外部中断。

  • 段初始化。主要是 GDT。

    ​ 关键代码:

      lgdt(c->gdt, sizeof(c->gdt));
      loadgs(SEG_KCPU << 3);
    
    • gs 表示段寄存器。
  • PIC(可编程中断控制器)初始化(使用的是 8259A 芯片)。通过 outb 实现。

  • IOAPIC(IO 高级可编程中断控制器)初始化。

  • 控制台初始化

  • UART 初始化(通用异步收发传输器),串口。

  • 进程表初始化。主要初始化锁。

  • TV(Trap 向量)初始化。

  • Buffer cache 初始化。这东西用来给硬盘做缓存。

  • 文件初始化。给文件表 ftable 上锁。

  • inode 初始化。给 inode 缓存表 icache 上锁。

  • IDE 初始化。硬盘。

  • 启动其它内核。

  • 初始化内核内存。填入垃圾数据。

初始化第一个用户进程

创建了 initcode 进程。并设置为 RUNNABLE 状态。

启动各 CPU,开始调度进程

通过原子指令 xchg 启动 CPU。scheduler 来开始调度。

参考

如何使用 x86 汇编对硬盘进行读写_scyatcs 的专栏 - CSDN 博客

PSE 页面大小扩展和 PAE 物理地址扩展_LauZyHou 的笔记 - CSDN 博客