第三章 MBR --> loader

第三章 MBR --> loader

本文是对《操作系统真象还原》第三章学习的笔记,欢迎大家一起交流。

a

知识介绍

在上一章的代码部分,我们通过 BIOS 中断进行字符输出,但是离开实模式之后,BIOS 中断就没法用了,因为 BIOS 中断向量表只在实模式下存在,因此我们肯定还会有别的方法来输出字符串,这就是通过显存输出。

能这样的原理是因为显卡的内存已经编排到了 cpu 能够寻址的范围之内,当 cpu 操作这部分“内存”时,实际上是直接在和显卡打交道。显卡拿到了数据处理之后,显示器最终会按照要求显示这些数据。内存中的显存映射的地址范围如下:

image

从起始地址 0xB8000 到 0xBFFFF,这片 32KB 大小的内存区域是用于文本显示。我们往 0xB8000 处输出的字符直接会落到显存中,显存中有了数据,自然显卡就将其搬到显示器屏幕上了,这后续的事情咱们是不需要处理的,咱们只要保证写进显存的数据是正确的就可以。

显示器上每个字符占两字节,低字节是字符 ASCII 码,高字节是用来控制颜色的。高字节低 4 位是字符前景色,也就是字符的颜色(RGB 是红蓝绿三种颜色的调和,I 位表示是否高亮),高字节高 4 位是字符的背景色(RGB 是红蓝绿三种颜色的调和,K 位控制是否闪烁)。所以我们向显卡的对应内存操作时,也应按照如下格式:

image

高八位的颜色组合如下:

image

在写代码之前再补充一个高端内存的小知识,内存只有 0--0xffff,但是可以表示到 0x10ffef,这是因为 0xFFFF*16+0xFFFF=0xFFFF0+0xFFFF

image

代码部分

;主引导程序 
;
;LOADER_BASE_ADDR equ 0xA000 
;LOADER_START_SECTOR equ 0x2
;------------------------------------------------------------
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

   ; 输出背景色绿色,前景色红色,并且跳动的字符串"1 MBR"
   mov byte [gs:0x00],'1'
   mov byte [gs:0x01],0xA4     ; A表示绿色背景闪烁,4表示前景色为红色

   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

   jmp $		       ; 通过死循环使程序悬停在此

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

初始化以及清屏的工作和第二章一样的,这里不再赘述,我们直接看操作显存部分。

13-14 行将 gs 寄存器初始化为 0xb800,方便后面直接向显存写字符

17-50 行就是向显存写字符的过程,以两个字节为一个单位,第八位是字符,高八位是控制颜色和背景色,我们这里统一写入 0xA4​,即 1010,0100​,结合上面颜色组合的表格(注意小端格式)可知,前景色为红色,绿色背景闪烁。

效果如下,我这里把字符串改了一下:

image

b

知识介绍

好了,接下来我们要用 MBR 做点实事了,MBR 只有 510B,能做的事情非常少,所以不能指望它做完所有事情。所以,我们用它把操作系统的 loader 加载到指定位置,然后跳转到 loader 执行,loader 由于大小可以比 MBR 大得多,所以能做的就很多了。

所以,MBR 要加载 loader,就必须要和磁盘打交道。打交道的方式很简单,就是通过 in 与 out 指令与磁盘暴露在外的寄存器交互。下图是 in 与 out 指令的用法:

image

磁盘端口寄存器对应的用途(详细请见书 p127):

image

其中 Status 与 Device 寄存器比较复杂,它们的结构的含义如下:

image

在写硬盘时 Status 就变成了 command 寄存器,这和 408 学的东西也一致,这里主要用到三个命令:

(1)identify:0xEC,即硬盘识别。

(2)read sector:0x20,即读扇区。

(3)write sector:0x30,即写扇区。

接下来我们来实现代码,实现 mbr 从硬盘读取 loader,然后跳转执行 loader。

首先约定一下读取硬盘的过程,好让我们做到心中有数:

(1)先选择通道,往该通道的 sector count 寄存器中写入待操作的扇区数。

(2)往该通道上的三个 LBA 寄存器写入扇区起始地址的低 24 位。

(3)往 device 寄存器中写入 LBA 地址的 24~27 位,并置第 6 位为 1,使其为 LBA 模式,设置第 4 位,选择操作的硬盘(master 硬盘或 slave 硬盘)。

(4)往该通道上的 command 寄存器写入操作命令。

(5)读取该通道上的 status 寄存器,判断硬盘工作是否完成。

(6)如果以上步骤是读硬盘,进入下一个步骤。否则,完工。

(7)将硬盘数据读出。

读出之后写入内存,然后跳转执行即可,但是在此之前我们还需要解决两个问题,loader 写到硬盘的哪里,loader 写入内存的哪里。

loader 写到硬盘的哪里

由于 MBR 是占据了硬盘的第 0 扇区(以逻辑 LBA 方式,扇区从 0 开始编号,若是以物理 CHS 方式,扇区则从 1 开始编号),第 1 扇区是空闲的,可以用,但离得太近总感觉不如隔开一点心里踏实,所以把 loader 放到第 2 扇区。MBR 从第 2 扇区中把它读出来。

loader 写入内存的哪里

读出来放到哪里呢?原则上是找个空闲地方就行了,在实模式下的内存布局表格中查看下,只要在“用途”列中注明“可用区域”的地方都可以用。0x500~0x7BFF 和 0x7E00~9FBFF 这两段内存区域都可以。

image

首先,loader 中要定义一些数据结构(如 GDT 全局描述符表,不懂没关系,以后会说),这些数据结构将来的内核还是要用的,所以 loader 加载到内存后不能被覆盖。

其次,随着咱们不断添加功能,内核必然越来越大,其所在的内存地址也会向越来越高的地方发展,难免会超过可用区域的上限,咱们尽量把 loader 放在低处,多留出一些空间给内核。

所以,作者将 loader 的加载地址选为 0x900。为什么不是 0x500,这个多省空间?还是预留出一定空间吧,彼此隔开远一点心里才踏实,不差这点空间了,哈哈,完全是作者的偏好,大家随意啦。

代码部分

ok,下面上代码

MBR

;主引导程序 
;------------------------------------------------------------
%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

第一行 %include "boot.inc"​,我们在 include/boot.inc ​定义了起始扇区 lba 地址以及写入内存的地址

;-------------	 loader和kernel   ----------
LOADER_BASE_ADDR equ 0x900 ;写入内存的地址
LOADER_START_SECTOR equ 0x2 ;起始扇区lba地址

4--52 行和之前实验一样的部分我们先跳过不看,重点看读硬盘的过程,在 call 读取硬盘之前,先准备参数

image

然后再函数的开始,再次对这些重要的参数备份

image

然后就是按照我们上面的约定,一步一步从磁盘中读出数据,首先是先选择通道,往该通道的 sector count 寄存器中写入待操作的扇区数。

image

第二步,写 LBA 地址以及 device 寄存器,这里按照上面说的写即可,其中我们获取 LBA 各个部分的地址是通过右移 eax 获得的

image

第三步,往该通道上的 command 寄存器写入操作命令。

image

;第 4 步:检测硬盘状态,直到磁盘准备好了再继续,这里的 nop 相当于一个时间很短的 sleep

image

第五步:读取该通道上的 status 寄存器,判断硬盘工作是否完成。每次只能读两个字节,所以一个扇区要读 256 次,256*读取的扇区数即读取的总次数,读取之后写到内存中,不断循环即可,读完直接返回。

image

调用完函数之后直接 jmp 到 loader 即可

image

Loader

我们这里先写一个简单的 loader 验证跳转即可,在屏幕上显示 2 Loader​,代码很简单,没什么要说的

%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR

; 输出背景色绿色,前景色红色,并且跳动的字符串"1 MBR"
mov byte [gs:0x00],'2'
mov byte [gs:0x01],0xA4     ; A表示绿色背景闪烁,4表示前景色为红色

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

mov byte [gs:0x04],'L'
mov byte [gs:0x05],0xA4   

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

mov byte [gs:0x08],'A'
mov byte [gs:0x09],0xA4

mov byte [gs:0x0a],'D'
mov byte [gs:0x0b],0xA4

mov byte [gs:0x0c],'E'
mov byte [gs:0x0d],0xA4

mov byte [gs:0x0e],'R'
mov byte [gs:0x0f],0xA4

jmp $		       ; 通过死循环使程序悬停在此

但是我们这里的编译命令有变化,要加一个 -I ​参数用于指定头文件位置,在用 dd 命令写入硬盘时也要加上 seek 命令用来写到 2 号扇区(0开始计算的情况下)

nasm -I include/ -o mbr.bin mbr.s
dd if=./mbr.bin of=../../../hd60M.img bs=512 count=1 conv=notrunc

nasm -I include/ -o loader.bin loader.s
dd if=./loader.bin of=../../../hd60M.img bs=512 count=1 seek=2 conv=notrunc

最后效果如下:

image

posted @ 2025-01-04 12:36  fdx_xdf  阅读(9)  评论(0编辑  收藏  举报