本文是从零开始写个操作系统吧的系列文章之一。

在上篇文章中我们讲到了一些基础知识,然后通过BIOS中断成功的在屏幕上打印出了一个字符。不过我之前也提到了,把kernel从磁盘读到内存中也是bootsect的任务之一,所以这一次我们就来讲一讲如何通过BIOS中断把磁盘中的数据读取内存之中。

1. 磁盘的构造

关于磁盘构造这块我不太想多说了,毕竟网上都能查到。我们需要重点观点关注就是磁盘的三个属性:磁头(Heads)、柱面(Cylinder)、扇区(Sector),也就是一般称为CHS的东西。只要使用这三个参数,我们就能定位到一个指定的扇区,接下来我就演示一下如何使用汇编语言操纵磁盘来从磁盘中读取数据到内存中。

2. 通过BIOS中断读取磁盘扇区中的数据到内存中

以下为代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
[org 0x7c00]

; 打印启动信息
mov bx, BOOT_MESSAGE
call print_string

; 开始读取
call disk_load

; 打印成功信息
mov bx, BOOT_SUCCESS
call print_string

; 为了验证磁盘中的数据是否已经被成功读取到内存中了,我们把内存中对应位置的数据读取出来进行查看
mov dx, [0x9000] ; 打印 0x9000 处的字节,也就是第二个扇区的第一个字节
call print_hex
mov dx, [0x9000 + 512] ; 打印 0x9512 处的字节,也就是第三个扇区的第一个字节
call print_hex
jmp $

disk_load:

mov bx, 0x9000 ; 把磁盘指定扇区中的数据加载内存中的 0x0000(ES):0x9000(BX) 处
mov ah, 0x02 ; BIOS 读取扇区的方法
mov al, 2 ; 读取 2 个扇区
mov ch, 0x00 ; CHS 中的 cylinder 为 0
mov dh, 0x00 ; CHS 中的 head 为 0
mov cl, 0x02 ; 从第 2 个扇区开始读(即接在 bootsect 后面的扇区)
int 0x13 ; 使用 BIOS 13 号中断开始从磁盘读数据到内存
jc disk_error ; 中断调用时会设置 carry flag,如果未设置,则发生了错误
mov bl, 2
cmp bl, al ; BIOS 在读取时会把真正读取到的扇区数赋给 al
jne disk_error ; 如果 al 不为 2,则说明读取发生了错误
ret

disk_error:

mov bx, DISK_ERROR_MSG
call print_string
jmp $

; 为了使代码保持整洁,我们把打印函数都放到 print.asm 文件中,这里的 include 和 C 语言中 include 的功能一样。
%include "print.asm"

; 用于打印的字符串,0x0a 和 0x0d 分别是换行和 Enter,你可以去掉其
; 中一个来看一下打印出来的效果,这样可以方便你对这两个字符的理解
BOOT_MESSAGE:
db 'Karen is booting...', 0x0a, 0x0d, 0
BOOT_SUCCESS:
db 'The kernel has been loaded.', 0x0a, 0x0d, 0

DISK_ERROR_MSG db "Disk read error!", 0

times 510-($-$$) db 0
dw 0xaa55

; dw 是两个字节,256 * 2 个字节 = 512 个字节,也就是说在结尾我们再加上两个 512 个字节的内容,现在汇编后的文件大小
; 是 1.5kb,占用磁盘的前三个扇区。也就是说现在我们除了存有 bootsect 的第一个扇区之外还有存有数据的第二和第三个扇区了。
times 256 dw 0x1234
times 256 dw 0xface

下面是 print.asm 文件中关于打印操作的打印代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
; 打印字符串
print_string:
mov ah, 0x0e
mov cl, [bx]
cmp cl, 0
jne bx_add
ret
bx_add:
mov al, [bx]
int 0x10
add bx, 1
jmp print_string

; 打印两个个字节的 16 进制(不过顺序是反的,这个我没能解决)
print_hex:
cmp dx, 0 ; 需要被打印的字节存储在 dx 中
je end ; 如果 dx 为 0,说明已经打印完毕,方法返回

mov cx, dx
shr dx, 4 ; dx 逻辑右移 4 位
and cl, 0xf ; cl = dx 的低八位,这里再通过与操作得到 cl 的低四位,也就是 dx 的低四位
cmp cl, 10
jl less_ten ; 如果此时 cl < 10,执行方法 less_then
cmp cl, 10
jge great_ten ; 如果此时 cl >= 10,执行方法 great_then
end:
ret

less_ten:

add cl, 48 ; 数字的 ASCII 码是从 48 开始的,数字 + 48 = 数字对应的 ASCII 码
mov al, cl
mov ah, 0x0e
int 0x10
jmp print_hex ; 继续打印下一个字符
great_ten:
add cl, 55 ; 大写字母的 ASCII 码是从 55 开始的,数字 + 55 = 字母对应的 ASCII 码
mov al, cl
mov ah, 0x0e
int 0x10
jmp print_hex

汇编(汇编的时候只需要使用第一个文件即可,print.asm 文件会自动被nasm找到并且引用进来)并且使用虚拟机运行这个文件,运行结果应该如下所示:

在这篇文章里我们主要了解到了BIOS 13号中断的使用以及如何打印一个字节的16进制,磁盘的读取实际上是一段比较机械的代码,主要是设置从什么地方开始读取磁盘、磁盘需要读取的扇区数以及这些数据要被读取到内存中的什么位置等等,这些都设置好之后执行一下中断就可以完成相关的操作了。