计算机的启动流程

BIOS

在开机的一瞬间,也就是上电的一瞬间,CPU 的 CS:IP 寄存器被强制初始化为 0xF000:0xFFF0。

CPU复位后PC指针

由于开机时处于实模式,段基址(CS)要左移 4 位,于是 0xF000:0xFFF0 的等效地址是 0xFFFF0。此地址便是 BIOS 的入口地址。

BISO的起始地址

不过,0xffff0 到 0xfffff 只有 16 字节的空间,BIOS 又要检测硬件,做各种初始化工作,还要建立中断向量表,显然 16 字节干不了这么多活。这说明 BIOS 真正的代码不在这,那此处的代码只能是个跳转指令。

jmp far f000:e05b

接下来BIOS便马不停蹄地检测内存、显卡等外设信息,当检测通过,并初始化好硬件后,开始在内存中 0x000~0x3FF 处建立数据结构,中断向量表IVT并填写中断例程。

计算机执行到这份上,BIOS也即将完成自己的历史使命了。

一直到这里,我们程序员都无法干预,从这里开始,程序员才能插手。

BIOS-我们无法干预

MBR

BIOS 检查第一块硬盘的 0 盘 0 道 1 扇区,如果该扇区的最后两个字节是 0x55 和 0xAA,就说明这个扇区是 MBR,可以从这里引导。BIOS 便将这 512 字节从硬盘拷贝到内存的 0x7c00 处,并跳转到 0x7c00,控制权便交给了 MBR。

错误示例:

qemu-system-i386 -daemonize -m 128M -s -S  -drive file=disk.img,index=0,media=disk,format=raw

MBR-错误

qemu 启动 disk.img 启动不起来,我们看下 disk.img 的内容

disk-00

很明显,disk.img 这个磁盘就两个字节,都不够 512 字节,自然扇区也没有以 0x55 0xaa 结尾,所以 BIOS 提示找不到 boot disk。

根本原因是,start.S 代码里就只写了个 jmp .,只有一句跳转指令,编译器也只将这句跳转指令编译成了机器码Eb FE,并没有人去在一个扇区的最后两个字节填充 0x55 0xAA。

start-00

正确示例:

我们在 start.S 中增加两行,即让 510、511 字节处填充 0x55、0xaa。这样生成的 disk.img 就是一个 MBR 了,见下图。

BIOS 识别出了 MBR,便将 MBR 拷贝到内存的 0x7c00 处,然后设置 cs:ip 为 0x0000:0x7c00,即跳转到 0x7c00 处开始运行,控制权由 BIOS 移交给 MBR。

start-01

disk-01

确实把 disk.img 拷贝到了内存 0x7c00 处,见下图

disk2mem

BIOS 不仅可以检测硬件,它还提供中断功能(BIOS 中断),比如中断 0x13,我们只要通过指令触发 0x13 中断,BIOS 就会执行 0x13 对应的中断服务函数,这些都是 BIOS 提供的,我们可以好好利用它。

上述所说的 0x13 功能是读取硬盘。

PS:向 BIOS 传递参数需要使用寄存器,这和 C 语言不同,C 语言使用的是函数参数,使用的是内存。也联想到了系统调用,也是通过寄存器传递参数)

番外篇(改造 MBR)

上电 ==> BIOS ==> MBR ==> loader

mbr_loader

给 MBR 具备读写磁盘到内存的功能,将扇区 2 所存储的 loader 程序加载到 0x900 内存处运行。

;主引导程序 mbr.S
;------------------------------------------------------------
%include "boot.inc"
SECTION MBR vstart=0x7c00         
   mov ax,cs      
   mov ds,ax
   mov es,ax
   mov ss,ax
   mov fs,ax
   mov sp,0x7c00
   mov ax,0xb800
   mov gs,ax

; 清屏
;利用0x06号功能,上卷全部行,则可清屏。
; -----------------------------------------------------------
;INT 0x10   功能号:0x06	   功能描述:上卷窗口
;------------------------------------------------------
;输入:
;AH 功能号= 0x06
;AL = 上卷的行数(如果为0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;无返回值:
   mov     ax, 0600h
   mov     bx, 0700h
   mov     cx, 0                   ; 左上角: (0, 0)
   mov     dx, 184fh		   ; 右下角: (80,25),
				   ; 因为VGA文本模式中,一行只能容纳80个字符,共25行。
				   ; 下标从0开始,所以0x18=24,0x4f=79
   int     10h                     ; int 10h

   ; 输出字符串:MBR
   mov byte [gs:0x00],'1'
   mov byte [gs:0x01],0xA4

   mov byte [gs:0x02],' '
   mov byte [gs:0x03],0xA4

   mov byte [gs:0x04],'M'
   mov byte [gs:0x05],0xA4	   ;A表示绿色背景闪烁,4表示前景色为红色

   mov byte [gs:0x06],'B'
   mov byte [gs:0x07],0xA4

   mov byte [gs:0x08],'R'
   mov byte [gs:0x09],0xA4
	 
   mov eax,LOADER_START_SECTOR	 ; 起始扇区lba地址
   mov bx,LOADER_BASE_ADDR       ; 写入的地址
   mov cx,1			 ; 待读入的扇区数
   call rd_disk_m_16		 ; 以下读取程序的起始部分(一个扇区)
 
   jmp LOADER_BASE_ADDR
       
;-------------------------------------------------------------------------------
;功能:读取硬盘n个扇区
rd_disk_m_16:	   
;-------------------------------------------------------------------------------
				       ; eax=LBA扇区号
				       ; ebx=将数据写入的内存地址
				       ; ecx=读入的扇区数
      mov esi,eax	  ;备份eax
      mov di,cx		  ;备份cx
;读写硬盘:
;第1步:设置要读取的扇区数
      mov dx,0x1f2
      mov al,cl
      out dx,al            ;读取的扇区数

      mov eax,esi	   ;恢复ax

;第2步:将LBA地址存入0x1f3 ~ 0x1f6

      ;LBA地址7~0位写入端口0x1f3
      mov dx,0x1f3                       
      out dx,al                          

      ;LBA地址15~8位写入端口0x1f4
      mov cl,8
      shr eax,cl
      mov dx,0x1f4
      out dx,al

      ;LBA地址23~16位写入端口0x1f5
      shr eax,cl
      mov dx,0x1f5
      out dx,al

      shr eax,cl
      and al,0x0f	   ;lba第24~27位
      or al,0xe0	   ; 设置7~4位为1110,表示lba模式
      mov dx,0x1f6
      out dx,al

;第3步:向0x1f7端口写入读命令,0x20 
      mov dx,0x1f7
      mov al,0x20                        
      out dx,al

;第4步:检测硬盘状态
  .not_ready:
      ;同一端口,写时表示写入命令字,读时表示读入硬盘状态
      nop
      in al,dx
      and al,0x88	   ;第4位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘忙
      cmp al,0x08
      jnz .not_ready	   ;若未准备好,继续等。

;第5步:从0x1f0端口读数据
      mov ax, di
      mov dx, 256
      mul dx
      mov cx, ax	   ; di为要读取的扇区数,一个扇区有512字节,每次读入一个字,
			   ; 共需di*512/2次,所以di*256
      mov dx, 0x1f0
  .go_on_read:
      in ax,dx
      mov [bx],ax
      add bx,2		  
      loop .go_on_read
      ret

   times 510-($-$$) db 0
   db 0x55,0xaa

mbr_loader2

参考

李述铜:

设计自己的Linux:从0写x86操作系统 课程资料

如何用6000行代码编写多进程的x86 Linux操作系统

《操作系统真象还原》

VSCode与CMake搭配使用之基本配置 —— vscode 可以自动创建 cmake 工程