---恢复内容开始---
开机流程回忆
以Intel 80386为例,计算机加电后,CPU从物理地址0xFFFFFFF0(由初始化的CS:EIP确定,此时CS和IP的值分别是0xF000和0xFFF0))开始执行。在0xFFFFFFF0这里只是存放了一条跳转指令,通过跳转指令跳到BIOS例行程序起始点。BIOS做完计算机硬件自检和初始化后,会选择一个启动设备(例如软盘、硬盘、光盘等),并且读取该设备的第一扇区(即主引导扇区或启动扇区)到内存一个特定的地址0x7c00处,然后CPU控制权会转移到那个地址继续执行。至此BIOS的初始化工作做完了,进一步的工作交给了ucore的bootloader。
简单来说,就是
加电-->CPU执行内存地址为0xFFFFFFF0的指令(一条跳转指令)--->跳到BIOS程序起始点,执行BIOS程序(功能:计算机硬件自检和初始化),自检等完成后--->选择一启动设备,并读取第一扇区(主引导扇区)到内存的0x7c00处--->BIOS所有工作完成后, CPU执行内存的0x7c00中的程序,即bootloader。
bootloader启动过程
BIOS将通过读取硬盘主引导扇区到内存,并转跳到对应内存中的位置执行bootloader。bootloader完成的工作包括:
- 打开A20地址线,设置CR0,切换到保护模式,启用分段机制
- 读磁盘中ELF执行文件格式的ucore操作系统到内存
- 显示字符串信息
- 把控制权交给ucore操作系统
对应其工作的实现文件在lab1中的boot目录下的三个文件asm.h、bootasm.S和bootmain.c。
代码简单分析
bootasm.S
1 .code16 # Assemble for 16-bit mode 2 cli # Disable interrupts 3 cld # String operations increment 4 5 # Set up the important data segment registers (DS, ES, SS). 6 xorw %ax, %ax # Segment number zero 7 movw %ax, %ds # -> Data Segment 8 movw %ax, %es # -> Extra Segment 9 movw %ax, %ss # -> Stack Segment
注释:.code16表示为16位的实模式,cli表示屏蔽系统中断 6-9为清空ds , es , ss 等段寄存器内容。
1 # Enable A20: 2 # For backwards compatibility with the earliest PCs, physical 3 # address line 20 is tied low, so that addresses higher than 4 # 1MB wrap around to zero by default. This code undoes this. 5 seta20.1: 6 inb $0x64, %al # Wait for not busy(8042 input buffer empty). 7 testb $0x2, %al 8 jnz seta20.1 9 10 movb $0xd1, %al # 0xd1 -> port 0x64 11 outb %al, $0x64 # 0xd1 means: write data to 8042‘s P2 port 12 13 seta20.2: 14 inb $0x64, %al # Wait for not busy(8042 input buffer empty). 15 testb $0x2, %al 16 jnz seta20.2 17 18 movb $0xdf, %al # 0xdf -> port 0x60 19 outb %al, $0x60 # 0xdf = 11011111, means set P2‘s A20 bit(the 1 bit) to 1
注释: 参考“百度文库 激活A20地址线详解”
1 # Switch from real to protected mode, using a bootstrap GDT 2 # and segment translation that makes virtual addresses 3 # identical to physical addresses, so that the 4 # effective memory map does not change during the switch. 5 lgdt gdtdesc 6 movl %cr0, %eax 7 orl $CR0_PE_ON, %eax 8 movl %eax, %cr0
注释:通过将CR0寄存器第一位置1,开启保护模式。
1 # Set up the stack pointer and call into C. The stack region is from 0--start(0x7c00) 2 movl $0x0, %ebp 3 movl $start, %esp 4 call bootmain
注释:跳转到bootmain.c
bootmain.c
bootloader让CPU进入保护模式后,下一步的工作就是从硬盘上加载并运行OS。考虑到实现的简单性,bootloader的访问硬盘都是LBA模式的PIO(Program IO)方式,即所有的IO操作是通过CPU访问硬盘的IO地址寄存器完成。
一般主板有2个IDE通道,每个通道可以接2个IDE硬盘。访问第一个硬盘的扇区可设置IO地址寄存器0x1f0-0x1f7实现的,具体参数见下表。一般第一个IDE通道通过访问IO地址0x1f0-0x1f7来实现,第二个IDE通道通过访问0x170-0x17f实现。每个通道的主从盘的选择通过第6个IO偏移地址寄存器来设置。
表一 磁盘IO地址和对应功能
第6位:为1=LBA模式;0 = CHS模式 第7位和第5位必须为1
IO地址 | 功能 |
0x1f0 | 读数据,当0x1f7不为忙状态时,可以读。 |
0x1f2 | 要读写的扇区数,每次读写前,你需要表明你要读写几个扇区。最小是1个扇区 |
0x1f3 | 如果是LBA模式,就是LBA参数的0-7位 |
0x1f4 | 如果是LBA模式,就是LBA参数的8-15位 |
0x1f5 | 如果是LBA模式,就是LBA参数的16-23位 |
0x1f6 | 第0~3位:如果是LBA模式就是24-27位 第4位:为0主盘;为1从盘 |
0x1f7 | 状态和命令寄存器。操作时先给命令,再读取,如果不是忙状态就从0x1f0端口读数据 |
当前 硬盘数据是储存到硬盘扇区中,一个扇区大小为512字节。读一个扇区的流程(可参看boot/bootmain.c中的readsect函数实现)大致如下:
- 等待磁盘准备好
- 发出读取扇区的命令
- 等待磁盘准备好
- 把磁盘扇区数据读到指定内存
1 /* waitdisk - wait for disk ready */ 2 static void 3 waitdisk(void) { 4 while ((inb(0x1F7) & 0xC0) != 0x40) 5 /* do nothing */; 6 } 7 8 /* readsect - read a single sector at @secno into @dst */ 9 static void 10 readsect(void *dst, uint32_t secno) { 11 // wait for disk to be ready 12 waitdisk(); 13 14 outb(0x1F2, 1); // count = 1 15 outb(0x1F3, secno & 0xFF); 16 outb(0x1F4, (secno >> 8) & 0xFF); 17 outb(0x1F5, (secno >> 16) & 0xFF); 18 outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0); 19 outb(0x1F7, 0x20); // cmd 0x20 - read sectors 20 21 // wait for disk to be ready 22 waitdisk(); 23 24 // read a sector 25 insl(0x1F0, dst, SECTSIZE / 4); 26 }
---恢复内容结束---