MBR到Loader的过渡 —— 在汇编层面读取硬盘数据

之前的文章有提到,PC(一般为32位)的启动过程为 BIOS -> MBR -> loader -> kernel。

BIOS在休眠后的最后任务是从硬盘中的第0柱面第0磁头第1扇区中读取MBR,其实就是逻辑0扇区。因为扇区是从1开始计数的。
BIOS所做的内部原理对我来说是从来不去涉及的,所以也就不讨论这个。

从MBR开始才是我们程序猿所能任意写的内容,而MBR同BIOS一样,也需要对下一阶段loader进行过渡。
而一个特别麻烦的地方就是,没有任何库与函数供我们调用……因此我们只能在汇编层面从硬盘读取数据。

老实说,我也不清楚应该从哪开始讲,后续内容都建立在以下基础知识之上:

  • CPU通过端口访问设备
  • 硬盘中的块表示有CHS和LBA方式
  • 通道的概念
  • 一个PATA线可以挂两个硬盘,一个主盘,一个从盘
  • 以前一般主板只支持4个PATA硬盘,所以只需两个通道,这里也只提两个通道
  • 硬盘的IO接口称为硬盘控制器

硬盘控制器的主要端口寄存器

Primary 通道Secondary 通道读操作 写操作
0x1F00x170DataData
0x1F10x171ErrorFeatures
0x1F20x170Sector countSector count
0x1F30x170LBA lowLBA low
0x1F40x170LBA midLBA mid
0x1F50x170LBA highLBA high
0x1F60x170DeviceDevice
0x1F70x170StatusCommand
0x3F60x170Alternate statusDevice Control
注: 1. 以上寄存器中除了data寄存器,其他寄存器都是8位的,data寄存器为16位,这也很容易理解么,总所周知,当前PC的速度卡点就在IO上。 2. 这里的LBA方式使用的是LBA28,即28位描述一个扇区,最大支持128G。另外一种是48位的。 3. 某一些寄存器在读和写的时候用途不同,这是为了提高利用率。 4. 简单介绍一下这些寄存器 – Data: 管理数据 – Error : 读取数据时出错会在这里有记录 – Feature: 存放调用参数 – Sector count: 待读写的扇区数量 – Device: 低4位用于LBA,第4位用于指定主从盘,第6位用于设置LBA/CHS。其他两个MBS位设置为1。 – Status: 存放状态信息 – Command: 存放命令,主要有硬盘识别 ( 0xEC ) 、读扇区 ( 0x20 )、写扇区 ( 0x30 )。

常用读写硬盘的操作方法

  1. 先选择通道,往该通道的 sector count 寄存器中写入待操作扇区数
  2. 往该通道的3个LBA寄存器中写入扇区起始地址
  3. 往Device寄存器中写入低4位( 0 ~ 3 )数据,第6位置1,表示选择LBA,第4位选择主从盘,其他两位置1.
  4. 往通道上的command 寄存器中写入命令
  5. 读取status寄存器,判断是否完成
  6. 读硬盘或者完工

最后是取数据,这个在操作系统课上有详说,什么DMA啊,通道啊,中断啊,什么的。这里不详说。

最后附上一个较为完整的 MBR ,取自郑钢的《操作系统真象还原》
书上的注释乱码了,只能我自己写一点

<code class="language-null">%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

   mov     ax, 0600h
   mov     bx, 0700h
   mov     cx, 0
   mov     dx, 184fh

   int     10h

   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

   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 ; loader起始扇区
   mov bx,LOADER_BASE_ADDR        ; 载入内存的起始位置
   mov cx,1                                          ; 读取一个扇区
   call rd_disk_m_16

   jmp LOADER_BASE_ADDR

rd_disk_m_16:
      ; 备份现场
      mov esi,eax
      mov di,cx

      ; 设置读取扇区
      mov dx,0x1f2
      mov al,cl
      out dx,al

      mov eax,esi

      ; 存入LBA
      mov dx,0x1f3
      out dx,al

      mov cl,8
      shr eax,cl
      mov dx,0x1f4
      out dx,al

      shr eax,cl
      mov dx,0x1f5
      out dx,al

      shr eax,cl
      and al,0x0f
      or al,0xe0
      mov dx,0x1f6
      out dx,al

      ; 向command 寄存器中写入读命令  0x20
      mov dx,0x1f7
      mov al,0x20
      out dx,al

     ; 检测硬盘状态
  .not_ready:
      nop
      in al,dx
      and al,0x88 
      cmp al,0x08
      jnz .not_ready

     ; 读数据
      mov ax, di
      mov dx, 256
      mul dx
      mov cx, ax
      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