上一节,我们已经初步认识了系统开机引导过程,并编写了一个简单的MBR引导程序(仅样例,不带分区表)。下面,我们将在实模式下继续认识计算机的IO接口、硬盘操作等知识,并真正实现一个内核加载器。
(本系列所有文章均参考郑刚所著《操作系统真象还原》,真诚感谢前辈的指导。)
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
PCIE
USB
SATA
为了给用户提供视觉上的交互,计算机需要依靠显示器来输出图像、文字信息。而显示器显示的性能、功能要求较高,CPU无法直接驱动显示器,驱动显示器的工作就交给了专门的显示适配器,也就是我们常说的显卡。
为了接收、储存CPU想要输出的信息,显卡提供了显存这一接口。我们只要操作显存,就可以让显卡驱动屏幕,输出对应信息。在实模式下,显卡相关内存分布如下:
起始 | 结束 | 大小 | 用途 |
---|---|---|---|
0xC0000 | 0xC7FFF | 32KB | 显示适配器BIOS |
0xB8000 | 0xBFFFF | 32KB | 用于显示适配器文本模式 |
0xB0000 | 0xB7FFF | 32KB | 用于黑白显示适配器 |
0xA0000 | 0xAFFFF | 64KB | 用于彩色显示适配器 |
下面,我们通过修改之前的打印欢迎信息的程序,来试验一下效果。程序使用一个循环,将数据字符串复制到显存对应位置,并设置颜色属性
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
程序同样打印出了正确内容,并且设置了我们想要设置的颜色。不过,如果我们手动操作显存,就无法使用控制字符(回车、换行等),如果使用,则需要另外处理。
硬盘控制器的主要端口寄存器如下表所示:
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 |
0x3f6
为 Control Block register
,其余均为Command Block register
部分主要寄存器的功能为:
PIO (Programming I/O Model)
,程序IO,读取前需要检测状态,数据源设备在一定状态下才能读取,如硬盘等按照上面的过程,我们就可以操作硬盘啦!下面,我们就将使用这个新技能,将硬盘中的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
端口输出想要读取的扇区数,也就是配置参数
shr
逻辑右移命令,每次将低八位输出,可以构造循环0x1f7
Command寄存器写入读命令0x20
0x88
过滤下面同样用一个简单的输出测试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
同样地,它需要被写入磁盘文件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分别输出了预期的内容,本节的内容结束啦~