Linux教程

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

本文主要是介绍动手编写操作系统(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想要输出的信息,显卡提供了显存这一接口。我们只要操作显存,就可以让显卡驱动屏幕,输出对应信息。在实模式下,显卡相关内存分布如下:

起始结束大小用途
0xC00000xC7FFF32KB显示适配器BIOS
0xB80000xBFFFF32KB用于显示适配器文本模式
0xB00000xB7FFF32KB用于黑白显示适配器
0xA00000xAFFFF64KB用于彩色显示适配器
  • 除了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)读操作时功能写操作时功能
0x1f0DataData
0x1f1ErrorFeatures
0x1f2Sector CountSector Count
0x1f3LBA lowLBA low
0x1f4LBA MidLBA mid
0x1f5LBA HighLBA high
0x1f6Devicedevice
0x1f7StatusCommand
0x3f6Alternate StatusDevice 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分别输出了预期的内容,本节的内容结束啦~

Result

这篇关于动手编写操作系统(3):系统引导过程——BIOS与MBR(下)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!