前提知识:
BIOS中断
BIOS 和 DOS 都是存在于实模式下的程序,由它们建立的中断调用都是建立在中断向量表(Interrupt Vector Table,IVT)中的。它们都是通过软中断指令 int 中断号来调用的。中断向量表中第 0H~1FH 项是 BIOS 中断。,0x20~0x27 是 DOS 中断。而 Linux 内核是在进入保护模式后才建立中断例程的。
中断向量表中的中断例程是由 BIOS 建立的,它从物理内存地址 0x0000 处初始化并在中断向量表中添加各种处理例程。
硬件自己的功能调用例程及初始化代码就存放在这 ROM中。根据规范,第 1 个内存单元的内容是 0x55,第 2 个存储单元是 0xAA,第 3 个存储单位是该 rom 中以512 字节为单位的代码长度。从第 4 个存储单元起就是实际代码了
硬盘扇区表示
描述 0 盘 0 道 1 扇区用的便是其中的一种:CHS 方法,即柱面 Cylinder 磁头 Header 扇区 Sector(另外一种是 LBA 方式,暂不关心),“0 盘”说的是 0 磁头,因为一张盘是有上下两个盘面的,一个盘面上对应一个磁头,所以用磁头 Header 来表示盘面。“0 道”是指 0 柱面,柱面 Cylinder指的是所有盘面上、编号相同的磁道的集合,形象一点描述就是把很多环叠摞在一起的样子,组合在一起之后是一个立体的管状。“1 扇区”才是我们要解释的部分,将磁道等距划分成一段段的小区间,由于磁道是圆形的,确切地说是圆环,这些被划分出来的小区间便是扇形,所以称为扇区。在 CHS 方式中扇区的编号是从 1 开始的,不是 0,不是 0,原谅我说了两次,良苦用心你懂的,所以 0 盘 0 道 1 扇区其实就相当于 0 盘 0 道 0 扇区,它就是磁盘上最开始的那个扇区。
而LBA 方式中,扇区编号是从 0 开始的。LBA 的定义,是一种逻辑上为扇区址的方法,全称为逻辑块地址(Logical Block Address)。
SSD固态硬盘控制器管理LBA到物理存储的映射关系,并负责数据的读取和写入。它使用逻辑地址映射到闪存芯片中的特定页或块,而不是基于CHS模式的物理位置。
访问外设有两种方式:
(1)内存映射:通过地址总线将外设自己的内存映射到某个内存区域(并不是映射到主板上插的内存条中)。
(2)端口操作:外设都有自己的控制器,控制器上有寄存器,这些寄存器就是所谓的端口,通过 in/out指令读写端口来访问硬件的内存。
硬盘的随机存取是靠磁头臂不断移动实现的,磁头臂移动到目标位置的时间称为寻道时间,如果存储的数据不连续,这一块那一片的,磁头就得不断调整位置,这是机械式硬盘不可避免的,这便是硬盘的瓶颈所在,所以一般的硬盘都将寻道时间作为重要参数。
(1)先选择通道,往该通道的 sector count 寄存器中写入待操作的扇区数。
(2)往该通道上的三个 LBA 寄存器写入扇区起始地址的低 24 位。
(3)往 device 寄存器中写入 LBA 地址的 24~27 位,并置第 6 位为 1,使其为 LBA 模式,设置第 4位,选择操作的硬盘(master 硬盘或 slave 硬盘)。
(4)往该通道上的 command 寄存器写入操作命令。
(5)读取该通道上的 status 寄存器,判断硬盘工作是否完成。
(6)如果以上步骤是读硬盘,进入下一个步骤。否则,完工。
(7)将硬盘数据读出。
一旦 command 寄存器被写入后,硬盘就开始工作
32位CPU在实模式下使用32位寄存器
32 位处理器可以执行16 位的程序,包括实模式和16 位保护模式。为此,在16 位模式下,处理器把所有指令都看成是16 位的。举个例子,机器指令码0x40 在16 位模式下的含义是
inc ax
。
当处理器在16 位模式下运行时,也可以使用32 位的寄存器,执行32 位的运算。为此,必须使用指令前缀0x66 来临时改变这种默认状态,因为同一个指令码,在16 位模式下和32 位模式下具有不同的解释。
因此,当处理器在16 位模式下运行时,机器指令码对应的指令不再是inc ax,而是66 40
对应的指令也不再是inc ax
,而是inc eax
loader加载地址
我们的 MBR 受限于 512 字节大小的,在那么小的空间中,没法为内核准备好环境,更没法将内核成功加载到内存并运行。loader 在哪里?如何跳过去执行?这就是新款 MBR 的使命,简而言之就是,负责从硬盘上把 loader 加载到内存,并将接力棒交给它。
MBR 从第 2 扇区(设定)中把它读出来。读出来放到哪里呢?
loader 加载到内存后不能被覆盖,尽量把 loader 放在低处,多留出一些空间给内核,作者将 loader 的加载地址选为 0x900。
代码附加详细解释
boot.inc
1 ;-------------loader 和 kernel-----------
2 LOADER_BASE_ADDR equ 0x900 ;宏定义
3 LOADER_START_SECTOR equ 0x2
主引导程序 :
;主引导程序
;------------------------------------------------------------
%include "boot.inc"
SECTION MBR vstart=0x7c00 ;告诉编译器,把我的起始地址编译为 0x7c00,段内偏移0x7c00
mov ax,cs ;BIOS 是通过 jmp 0:0x7c00 跳转到 MBR 的,故cs 此时为 0
mov ds,ax ;初始化
mov es,ax
mov ss,ax ;FS 和 GS 附加段寄存器是在 32 位CPU 中增加的
mov fs,ax ;16位
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
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,out会修改al的值
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步:向0x1f7command端口写入读命令,0x20,硬盘开始工作
mov dx, 0x1f7
mov al,0x20
out dx,al
;第4步:检测硬盘状态
.not_ready:
;同一端口,写时表示写入命令字,读时表示读入硬盘状态
nop ;为了增加延迟,目的是减少打扰硬盘的工作
in al,dx ;将 Status 寄存器的值读入到 al 寄存器
and al,0x88 ;第4位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘忙
cmp al,0x08
jnz .not_ready ;若未准备好,继续等。
;第5步:从0x1f0端口读数据
mov ax, di
mov dx, 256 ;256 超过了 8 位寄存器表示的范围
mul dx
mov cx, ax ; di为要读取的扇区数,一个扇区有512字节,每次读入一个字,
; 共需di*512/2次,所以di*256
mov dx, 0x1f0
.go_on_read:
in ax,dx
mov [bx],ax ;bx 只会访问到 0~FFFFh 的偏移,此处加载的程序不能超过 64KB
add bx,2
loop .go_on_read
ret
times 510-($-$$) db 0
db 0x55,0xaa
loader的测试程序:
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
; 输出背景色绿色,前景色红色,并且跳动的字符串"1 MBR"
mov byte [gs:0x20],'2'
mov byte [gs:0x21],0xA4 ; A表示绿色背景闪烁,4表示前景色为红色
mov byte [gs:0x22],' '
mov byte [gs:0x23],0xA4
mov byte [gs:0x24],'L'
mov byte [gs:0x25],0xA4
mov byte [gs:0x26],'O'
mov byte [gs:0x27],0xA4
mov byte [gs:0x28],'A'
mov byte [gs:0x29],0xA4
mov byte [gs:0x2a],'D'
mov byte [gs:0x2b],0xA4
mov byte [gs:0x2c],'E'
mov byte [gs:0x2d],0xA4
mov byte [gs:0x2e],'R'
mov byte [gs:0x2f],0xA4
jmp $ ; 通过死循环使程序悬停在此
注意我们通过dd命令将loader的二进制文件放在2扇区
测试结果:
用于学习笔记:操作系统真象还原第三章