一、背景和目的
在(一)中的boot程序通过使用BIOS
提供的0x10
中断实现了清屏和打印字符。但:
- BIOS最终要交接出去,CPU的工作方式要从实模式切换到保护模式,而保护模式下是没有中断向量表的;
- CPU是高速设备,外部IO一般都是低速设备。若通过增加CPU等待时间来向下兼容IO是得不偿失的;
- CPU使用TTL电平,而不同外设的电气属性不同,无法和CPU很好兼容;
- CPU内部的数据是并行的传输方式,而IO的数据串并行方式都有存在;
基于以上原因,在CPU和外设之间加了一层IO接口层
。显卡、网卡、声卡、硬盘控制器等都属于IO接口。CPU可通过主板上的硬件接口和各IO控制器内部的寄存器(端口
)直接相连。通过对这些控制器端口的读写操作,进而达到驱动外设工作,进行数据交互的目的。
本文的写作目的是将CPU与显卡、硬盘控制器这两种IO接口之间进行交互,进而实现在显示器中显示信息和读写IDE硬盘数据的过程进行描述和实现。
二、理论说明
1 显卡
- 无论是液晶显示器还是CRT显示器,它们都是由显卡进行驱动和控制显示;而无论是哪种显卡,它都提供了两种可编程接口:
IO端口
和显存
。 - 显存——是显卡内部的一块内存。显卡的工作就是不断地读取显存中地内容并将其发送到显示器。
- 实模式下的CPU的寻址能力是1MB,显存的用于文本模式显示的部分被映射到了
0xB800~0xBFFFF
这32KB的空间。也就说,我们对这块地址进行读写操作,便可以将数据读出或写入显存,进而在显示器中进行显示。 - 显示器上的每个字符占两个字节:
低字节是字符的ASCII码
,高字节为前景色和背景色设置
。
2 机械硬盘
- 机械硬盘IDE——盘片存储数据,磁头读取数据;一方面盘片自转,另一方面磁头的摆动,这两种动作的配合实现了磁头读取盘片任意位置的数据;
- 为便于管理,将整个盘面划分为多个同心的圆环,这个同心圆环就被成为
磁道
;而一个磁道又被划分为若干个小扇形,这个扇形就被成为扇区
(扇区是硬盘存储的最基本单位,每个扇区的大小为512字节) - 扇区的定位方式有两种:
- CHS模式——即通过柱面-磁头-扇区的方式定位;
- LBA模式——规定扇区从0开始依次递增编号,而不必考虑实际的物理结构;(boot程序放在0扇区,loader程序放在2扇区及之后)
- 硬盘控制器集成在了硬盘内部(这也是IDE的由来-Integrated Driver Electronics)。硬盘控制器的主要端口寄存器如下表所示:
IO端口的primary通道 | 端口用途 |
---|---|
0x1F0 | 数据的读取或写入 |
0x1F1 | 写操作时,用于写命令的参数;读操作时,存储读取失败状态的信息 |
0x1F2 | 要读取或写入的扇区个数 |
0x1F3 | LBA的低8位 0~7bit |
0x1F4 | LBA的中低8位 8~15bit |
0x1F5 | LBA的中高8位 16~24bit |
0x1F6 | 低4位:LBA的高4位 25~27bit; 第4位:0–主盘,1–从盘; 第6位:1–LBA模式 0–CHS模式; 第5、7位:固定为1 |
0x1F7 | 写操作时,写0xEC–硬盘识别,写0x20–读扇区, 写0x30–写扇区;读操作时,第3位:1–硬盘已准备好数据,第7位:1–硬盘正忙 |
三、boot程序实现写显示器和读IDE硬盘扇区
MBR只有512字节,这么小的空间无法为内核准备好环境,因此boot程序的作用是要将位于2扇区及之后的几个扇区中的loader程序
读入到内存,完成初始化环境和加载内核的任务。
因为此时还未实现loader程序,但还要验证正确性。因此我们将0扇区的boot程序
读入到内存的0x900
区域,通过bochs的GUI调试功能,验证读硬盘操作是否成功。
- boot汇编代码的编写
org 0x7c00
;-------------------------------------
;主函数
;-------------------------------------
main: mov ax, 3
int 0x10
mov ax, cs
mov ss, ax
mov ds, ax
mov ax, 0xb800
mov es, ax
mov sp, 0x7c00
mov si, info
mov di, 0
call print
xchg bx, bx ;bochs的魔数断点
mov ecx, 0x0 ;要读取的LBA扇区地址(源地址)
mov edi, 0x900 ;读取到内存的地址(目标地址)
mov bx, 1 ;读取的扇区个数
call rd_disk
xchg bx, bx
jmp $
;--------------------------------------------------
;print子函数——通过将数据写入显存的方式,在显示器中显示字符
;--------------------------------------------------
print:
.next:
mov al, [ss:si]
cmp al, 0 ;将1byte数据写入到al
jz .done
mov ah, 0x0f ;字符的显示属性设置:黑底白字
mov [es:di], ax
inc si
add di, 2
loop .next
.done:
ret
;--------------------------------------------------
;读取硬盘数据子函数——读取某位置的n个扇区数据
;--------------------------------------------------
rd_disk:
;第一步:确定要读取的扇区个数
mov dx, 0x1f2
mov ax, bx
out dx, al
;第二步:确定要读取扇区的LBA首地址
mov dx, 0x1f3
mov al, cl
out dx, al
mov dx, 0x1f4
shr ecx, 8
mov al, cl
out dx, al
mov dx, 0x1f5
shr ecx, 8
mov al, cl
out dx, al
;第三步:确定LBA的高4位地址和配置相关模式
mov dx, 0x1f6
shr ecx, 8
and cl, 0x0f ;低四位有效
mov al, 0b1110_0000 ;LBA模式--主盘--LBA的24~27位地址
or al, cl
out dx, al
;第四步:设置读硬盘命令
mov dx, 0x1f7
mov al, 0x20
out dx, al
;第五步:确定硬盘的当前状态(是否忙碌)
.busy:
mov dx, 0x1f7
in al, dx
nop
nop
nop ;短暂延时
and al, 0b1000_1000
cmp al, 0b0000_1000
jnz .busy
;第六步:若硬盘空闲,则读取n个扇区数据
mov ax, bx
mov dx, 256
mul dx ; ax = bx * dx
mov cx, ax
mov dx, 0x1f0
.read_s:
in ax, dx
mov [ds:edi], ax
add edi, 2
loop .read_s
ret
info: db "HELLO WORLD! I am gnix!", 0
times 510-($-$$) db 0
dw 0xaa55
- 在bochs的配置文件
bochsrc
中开启魔数断点功能
- 我们在print函数之后和rd_disk函数之后添加断点。可预见的结果是:
- 当执行到第一次断点时,0x7c00为起始地址的512字节存放着boot程序代码,同时bochs显示器中显示字符;
- 当执行到第二次断点时,0x900为起始地址的512字节也存放boot程序(我们在代码中设置的是将0扇区,即boot程序加载到此处)。
上图为第一次断点的执行结果,与猜想一相符。
上图为第二次断点的执行结果1
,显示的初始地址为0x9002
,其之后的512字节内存有显示字符3
,说明boot代码已成功从硬盘的0扇区加载到内存。