上一节,我们已经初步认识了系统开机引导过程,并编写了一个简单的MBR引导程序(仅样例,不带分区表)。下面,我们将在实模式下继续认识计算机的IO接口、硬盘操作等知识,并真正实现一个内核加载器。
(本系列所有文章均参考郑刚所著《操作系统真象还原》,真诚感谢前辈的指导。)
I/O 接口
CPU通过I/O接口与外部设备进行通信。I/O接口作为一个“层”,CPU与硬件的交互提供兼容服务,是连接CPU与外部设备的逻辑控制部件。I/O接口具有硬件和软件部分,硬件部分负责底层的硬件连接以及为软件提供缓冲等硬件基础,高级的硬件接口允许同时与多个设备进行连接;软件部分通过程序实现数据的交换。通过IO接口控制编程,我们可以控制接口的功能,这通常由端口读写指令in/out
实现。
in ax, dx ; ax: read data, dx: port
out dx, al ; dx: port, al: write data
IO 接口的功能
- 设置数据缓冲,解决CPU与外设速率不匹配问题
- 设置信号电平转换电路,将CPU的TTL信号与外设的电平(模拟/数字信号)进行转换
- 数据格式转换:提供A/D、D/A转换、串并行转换与接口带宽转换等
- 设置时序控制电路,以同步CPU与外部设备
- 提供地址译码,使CPU能够选中某个端口,时期访问数据总线
总线
- 为提供多个设备之间的连接,我们使用总线连接这些设备,成为这些设备的公共通道
- 总线上, 同时只有一对设备进行通信,其他设备处于高阻态
- 如果总线空闲,一个设备需要发送信息,则驱动总线发送,其他设备收到符合的地址,则开始通信
- 计算机内部存在一条高速总线,连接CPU与南桥(输出输出控制中心),称为内部总线(系统总线)
- 南桥负责连接外部设备,内部集成多个IO接口
- 为了连接可拓展设备,IO接口设置了总线通信标准,符合一类总线标准的设备可连接在一套总线上
- 常见的总线标准
PCIE
USB
SATA
- 常见的总线标准
- 串行总线与并行总线
- 并行总线通过链路一次收发多b数据,但需要同步各线时序
- 并行总线一次收发1b数据,频率较高
- 串行总线逐渐取代并行总线
显示适配器
为了给用户提供视觉上的交互,计算机需要依靠显示器来输出图像、文字信息。而显示器显示的性能、功能要求较高,CPU无法直接驱动显示器,驱动显示器的工作就交给了专门的显示适配器,也就是我们常说的显卡。
为了接收、储存CPU想要输出的信息,显卡提供了显存这一接口。我们只要操作显存,就可以让显卡驱动屏幕,输出对应信息。在实模式下,显卡相关内存分布如下:
起始 | 结束 | 大小 | 用途 |
---|---|---|---|
0xC0000 | 0xC7FFF | 32KB | 显示适配器BIOS |
0xB8000 | 0xBFFFF | 32KB | 用于显示适配器文本模式 |
0xB0000 | 0xB7FFF | 32KB | 用于黑白显示适配器 |
0xA0000 | 0xAFFFF | 64KB | 用于彩色显示适配器 |
- 除了BIOS空间,其他的位置均指向显存。只要修改寄存器的值,就会影响屏幕的输出。
下面,我们通过修改之前的打印欢迎信息的程序,来试验一下效果。程序使用一个循环,将数据字符串复制到显存对应位置,并设置颜色属性
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 ; auxiliary section regisster
mov gs, ax
;mov ecx, 0 ; initialize ecx
; Clear Screen use 0x06 sub-func in interrupt 0x10(Video Service)
; function description: scroll up the window
; INPUTS:
; AH function_index = 0x06
; AL = rows to scroll up, 0: clear
; BH = scroll up color property
; (CL, CH) = Top Left Corner of window
; (DL, DH) = Lower Right Corner of window
; No return
mov ax, 0x0600 ; also can mov ah, 6; mov ax, 0
mov bx, 0x0700 ; BH: Light Gray on Black
mov cx, 0 ; Top Left (0, 0)
mov dx, 0x184f ; Lower Right: (80, 25) ((79, 14))
; in VGA Text Module, only 80c in one line, max 25 lines
int 0x10
; print string with gpu
; mov si, message
mov cx, 0
loop:
mov si, message
add si, cx
mov al, byte [si]
mov bx, cx
add bx, bx
mov byte [gs:bx], al
add bx, 1
mov byte [gs:bx], 0x0a ; color: light green
add cx, 1
cmp cl, byte [len]
jb loop
; end
jmp $
; data
message db "Hello, world. This is a MBR."
len db $ - message
; times 510-($-$$) db 0
db 510 - ($-$$) dup (0)
db 0x55, 0xaa
程序同样打印出了正确内容,并且设置了我们想要设置的颜色。不过,如果我们手动操作显存,就无法使用控制字符(回车、换行等),如果使用,则需要另外处理。
硬盘
- 硬盘中的物理扇区使用CHS (Cylinder Head Sector)定位方法进行定位的
- Cylinder: 柱面,确定半径的所有盘片的同一圆柱面,读取同一柱面数据不需要移动磁头
- Head: 磁头,每个盘面需要有一个磁头用于读写数据
- Sector: 扇区,分割开的扇形区域
- 为了方便定位,将所有磁盘中扇区从0开始编号,分配一个逻辑地址LBA (Logical Block Address),称为逻辑块地址
- 早期的LBA28采用28位寻址,在扇区大小为512B时,最大支持空间128GB;后期的LBA48支持128PB
端口寄存器
硬盘控制器的主要端口寄存器如下表所示:
IO端口号(Primary) | 读操作时功能 | 写操作时功能 |
---|---|---|
0x1f0 | Data | Data |
0x1f1 | Error | Features |
0x1f2 | Sector Count | Sector Count |
0x1f3 | LBA low | LBA low |
0x1f4 | LBA Mid | LBA mid |
0x1f5 | LBA High | LBA high |
0x1f6 | Device | device |
0x1f7 | Status | Command |
0x3f6 | Alternate Status | Device Control |
- 以上为Primary(主盘)端口号,PATA从盘端口号 -0x80
- 其中,最后一个端口
0x3f6
为Control Block register
,其余均为Command Block register
- 部分寄存器具有多重功能,一个功能占几位
- 仅Data寄存器为16b位宽,其余均为8位
部分主要寄存器的功能为:
- Data:传输数据
- Error:在硬盘读错误时存放错误信息,此时Sector Count存放尚未读取的扇区数
- Feature:存放部分命令需要的额外参数
- Sector Count:存放需要读取/写入的扇区数。完成一个扇区的读写后,会将此值-1
- LBA: 存储LBA寻址地址,其余4位在Device寄存器中
- Device:杂项寄存器,低4b存放LBA的24-27位,第4位指定通道上的主/从盘,0为主;,第6位设置是否启用LBA
- Status
- 0:Error位,为1时表示出错
- 3:Data Request 位,为1时表示数据准备完成,可以供主机读取
- 6:DRDY,硬盘就绪,硬盘诊断中表示硬盘检测正常
- 7:BSY,硬盘正忙
硬盘操作命令
主要命令
- Identify: 0xEC, 硬盘识别
- Read Sector: 0x20, 读扇区
- Write Sector: 0x30, 写扇区
命令顺序
- 没有完整顺序,只有command寄存器必须最后置位,写入后即开始运行
- 约定使用顺序如下:
- 选择通道(Channel),像通道的Sector Count寄存器中写入待操作的扇区数
- 指定LBA地址低24位(3个LBA寄存器)
- 向Device寄存器中写入LBA高4位,将第6位LBA使能位置位,设置第4位操作的硬盘
- 向Command寄存器写入命令
- 读取Status,查看是否完成
- 如果是读取命令,则读出数据,否则完成
常用数据传送方式
- 无条件传送:数据源一定准备随时读取,如寄存器,内存等
- 查询传送:也称为
PIO (Programming I/O Model)
,程序IO,读取前需要检测状态,数据源设备在一定状态下才能读取,如硬盘等 - 中断传送:也称为中断驱动IO,弥补了查询传送需要不断查询的缺陷,数据源准备好数据,触发中断通知主机读取,效率较高
- 直接存储器读取(DMA):将数据源数据输出到内存,CPU直接向内存读取。效率更高(不需要中断),但需要硬件支持(DMA控制器)
- I/O处理机传送:通过单独的IO处理设备处理传输
使用硬盘
读取Loader
按照上面的过程,我们就可以操作硬盘啦!下面,我们就将使用这个新技能,将硬盘中的Loader读取到内存里,并跳转执行。下面直接贴代码:
%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 ; auxiliary section regisster
mov gs, ax
mov ecx, 0 ; initialize ecx
; Clear Screen use 0x06 sub-func in interrupt 0x10(Video Service)
; function description: scroll up the window
; INPUTS:
; AH function_index = 0x06
; AL = rows to scroll up, 0: clear
; BH = scroll up color property
; (CL, CH) = Top Left Corner of window
; (DL, DH) = Lower Right Corner of window
; No return
mov ax, 0x0600 ; also can mov ah, 6; mov ax, 0
mov bx, 0x0700 ; BH: Light Gray on Black
mov cx, 0 ; Top Left (0, 0)
mov dx, 0x184f ; Lower Right: (80, 25) ((79, 14))
; in VGA Text Module, only 80c in one line, max 25 lines
int 0x10
mov bx, 0
mov si, message
mov cx, [len_message]
call my_print
mov eax, LOADER_START_SECTOR ; Start Sector of Loader
mov bx, LOADER_BASE_ADDR ; Loader base address
mov cx, 1 ; Sector count
call rd_disk_m_16
; jmp $ ; for debug
jmp LOADER_BASE_ADDR
my_print:
; print string with gpu
; param: bx: offset on the screen
; param si: string address
; param cx: length
; mov cx, dx
loc_0x37:
mov al, byte [si]
mov byte [gs:bx], al
add bx, 1
mov byte [gs:bx], 0x0a ; color
add si, 1
add bx, 1
loop loc_0x37
retn
; my_print endp
rd_disk_m_16:
; param eax = LBA Sector number
; bx = destination address
; cx = sector count
mov esi, eax ; backup eax
mov dx, 0x1f2 ; sector count
mov al, cl
out dx, al
mov eax, esi
mov dx, 0x1f3 ; set LBA address low 24b
loc_72:
out dx, al
add dx, 1
shr eax, 8
cmp dx, 0x1f5
jbe loc_72
and al, 0x0f ; set LBA address high 4b
or al, 0xe0 ; set device mode
out dx, al
add dx, 1
mov al, 0x20 ; read command
out dx, al
.not_ready:
nop
nop
in al, dx
and al, 0x88 ; 4: ready; 7: busy
cmp al, 0x08
jnz .not_ready
and cx, 0xf
shl cx, 8 ; words to read, *512 / 2
mov dx, 0x1f0 ; data port
.go_on_read:
in ax, dx
mov [bx], ax
add bx, 2
loop .go_on_read
retn
; data
message db "Hello, world. This is a MBR."
len_message db $ - message
; times 510-($-$$) db 0
db 510 - ($-$$) dup (0)
db 0x55, 0xaa
下面是主要结构以及改动的说明:
- 增加
%include
预处理指令,将boot.inc
中设置的两个参数宏定义引用进文件中 - 对
my_print
进行改进,使之成为函数,采用寄存器传递参数 - 增加
rd_disk_m_16
读取硬盘函数,并调用此函数读取硬盘中loader,载入内存
我们再来详细看一下rd_disk_m_16
函数:
- 向
0x1f2
端口输出想要读取的扇区数,也就是配置参数- 直接通过配置好的寄存器参数输出即可
- 向三个LBA地址端口写入需要读取的扇区号
- 这里使用
shr
逻辑右移命令,每次将低八位输出,可以构造循环
- 这里使用
- 向Device寄存器写入LBA高四位以及其他配置信息
- 还需要置位LBA使能位,和另外两个恒为1的位
- 向
0x1f7
Command寄存器写入读命令0x20
- 检测硬盘状态,直到硬盘就绪
- 通过读取 Status 状态寄存器信息,可以得知硬盘状态是否就绪
- 我们只关心第4位Ready和第7位Busy,使用mask
0x88
过滤
- 循环从Data寄存器中读出数据
- 读取一个Sector共512B,由于Data寄存器长度为16b,故每次读出Word (2B)
- 循环数为扇区数左移8位
测试Loader
下面同样用一个简单的输出测试Loader是否被正确加载
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
mov bx, 0x100
mov si ,message
mov cx, [len]
call my_print
jmp $
my_print: ; vesion 3.0
; print string with gpu
; param: bx: (bh, bl)=(row, col) offset on the screen
; param si: string address
; param cx: length
; mov cx, dx
mov ax, 0xa0
mul bh
add ax, bl
mov bx, ax
loc_0x37:
mov al, byte [si]
mov byte [gs:bx], al
add bx, 1
mov byte [gs:bx], 0x0a ; color
add si, 1
add bx, 1
loop loc_0x37
retn
; my_print endp
; data
message db "Loader ready."
len dw $ - message
- 使用更新版的my_print函数,可以指定输出的起始坐标
同样地,它需要被写入磁盘文件disk.img
中,我们将它放在第二个扇区里。下面是一键启动的配置文件:
#!/bin/zsh
nasm -o mbr.bin mbr-gpu.S
nasm -o loader.bin loader.S
dd if=mbr.bin of=disk.img bs=512 count=1 conv=notrunc
dd if=loader.bin of=disk.img bs=512 count=1 seek=1 conv=notrunc
bochs -q
测试结果
接下来通过一键启动脚本,我们让虚拟机自主运行,MBR和Loader分别输出了预期的内容,本节的内容结束啦~