动手编写操作系统(3):系统引导过程——BIOS与MBR(下)

  上一节,我们已经初步认识了系统开机引导过程,并编写了一个简单的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 接口的功能

  1. 设置数据缓冲,解决CPU与外设速率不匹配问题
  2. 设置信号电平转换电路,将CPU的TTL信号与外设的电平(模拟/数字信号)进行转换
  3. 数据格式转换:提供A/D、D/A转换、串并行转换与接口带宽转换等
  4. 设置时序控制电路,以同步CPU与外部设备
  5. 提供地址译码,使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
  • 其中,最后一个端口0x3f6Control 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寄存器必须最后置位,写入后即开始运行
  • 约定使用顺序如下:
    1. 选择通道(Channel),像通道的Sector Count寄存器中写入待操作的扇区数
    2. 指定LBA地址低24位(3个LBA寄存器)
    3. 向Device寄存器中写入LBA高4位,将第6位LBA使能位置位,设置第4位操作的硬盘
    4. 向Command寄存器写入命令
    5. 读取Status,查看是否完成
    6. 如果是读取命令,则读出数据,否则完成

常用数据传送方式

  1. 无条件传送:数据源一定准备随时读取,如寄存器,内存等
  2. 查询传送:也称为PIO (Programming I/O Model),程序IO,读取前需要检测状态,数据源设备在一定状态下才能读取,如硬盘等
  3. 中断传送:也称为中断驱动IO,弥补了查询传送需要不断查询的缺陷,数据源准备好数据,触发中断通知主机读取,效率较高
  4. 直接存储器读取(DMA):将数据源数据输出到内存,CPU直接向内存读取。效率更高(不需要中断),但需要硬件支持(DMA控制器)
  5. 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=513 count=1 seek=1 conv=notrunc
bochs -q

测试结果

  接下来通过一键启动脚本,我们让虚拟机自主运行,MBR和Loader分别输出了预期的内容,本节的内容结束啦~

动手编写操作系统(3):系统引导过程——BIOS与MBR(下)

上一篇:后盾人:JS课程第九章(闭包)


下一篇:Python学习小练习(一)