0x00

这学期开始上操作系统原理的课,本人此前学过许多关于操作系统的知识但是却没有完整地串联起来过。于是决定实现一个简单操作系统。

跟着《操作系统真象还原》来进行实现。但是不使用书中教程使用的 bochs,本人使用 qemu 来进行运行调试。有关环境如下

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
❯ qemu-system-i386 --version
QEMU emulator version 8.2.2 (Debian 1:8.2.2+ds-0ubuntu1.13)
Copyright (c) 2003-2023 Fabrice Bellard and the QEMU Project developers

❯ nasm -v
NASM version 2.16.01

❯ gcc --version
gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0
Copyright (C) 2023 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

❯ hostnamectl
Static hostname: LAPTOP-6JKPOVPE
Icon name: computer-container
Chassis: container ☐
Machine ID: c5f41f846562445b86b9e23f700df887
Boot ID: f10cbec29c32494f960b2e915be0d6d6
Virtualization: wsl
Operating System: Ubuntu 24.04.3 LTS
Kernel: Linux 6.6.87.2-microsoft-standard-WSL2
Architecture: x86-64

❯ gdb --version
GNU gdb (Ubuntu 15.0.50.20240403-0ubuntu1) 15.0.50.20240403-git
Copyright (C) 2024 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

另外有关的基础知识就不多做介绍了,网上整理好的博客很多,原书也写的不错。更多的记录本人在实现中遇到的问题和一些 debug 过程。

0x01 MBR

MBR 也就是 Master Boot Record,是使用 BIOS (Basic Input & Output System) 启动操作系统时的引导记录,位于硬盘第一个扇区。 计算机启动时 BIOS 读取并执行其中的引导代码,加载操作系统。可以注意到它和 BIOS 的代码都是写死的,BIOS 写死在主板的 ROM中,MBR写死在硬盘第一个扇区,因为仅仅在启动时执行。

MBR 的职责就是加载操作系统… 吗? 因为 MBR 仅有 512 字节也就是一个磁盘扇区大小,并不足以实现完整的操作系统加载功能,所以就有了 Loader

MBR 来加载硬盘上的 LoaderLoader 来负责完整加载操作系统的功能。

使用汇编来编写如下

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
%include "boot.inc"
; mbr 主引导程序
SECTION MBR vstart=0x7c00
mov ax, cs
mov ds, ax
mov ss, ax
mov es, ax
mov fs, ax
mov sp, 0x7c00
mov ax, 0xb800
mov gs, ax

; 清屏利用 0x06 号功能,上卷全部行,则可清屏。
; -----------------------------------------------------------
;INT 0x10 功能号:0x06 功能描述:上卷窗口
;------------------------------------------------------
;输入:
;AH 功能号= 0x06
;AL = 上卷的行数(如果为 0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;无返回值:
mov ax, 0x600
mov bx, 0x700
mov cx, 0 ; 左上角: (0, 0)
mov dx, 0x184f ; 右下角: (80,25),
; VGA 文本模式中,一行只能容纳 80 个字符,共 25 行。
; 下标从 0 开始,所以 0x18=24,0x4f=79
int 0x10 ; int 0x10

; 输出一段自定义的提示词
mov byte [gs:0x00],'M'
mov byte [gs:0x01],0x0F ; 0 表示黑色背景,F 表示前景色为白色

mov byte [gs:0x02],'B'
mov byte [gs:0x03],0x0F

mov byte [gs:0x04],'R'
mov byte [gs:0x05],0x0F

; 准备从硬盘读入loader
mov eax, LOADER_START_SECTOR ;起始扇区LBA地址
mov bx, LOADER_BASE_ADDR ;写入的内存地址
mov cx, 4 ;待读入的扇区数 这里读入4个扇区 (loader比较大)
call rd_disk_m_16 ;以下读取程序的起始部分(一个扇区)
jmp LOADER_BASE_ADDR

; | 端口 | 作用
; | 0x1F0 | 数据端口
; | 0x1F1 | 错误
; | 0x1F2 | 扇区数
; | 0x1F3 | LBA低8位
; | 0x1F4 | LBA中8位
; | 0x1F5 | LBA高8位
; | 0x1F6 | 设备+LBA高4位
; | 0x1F7 | 命令 / 状态


rd_disk_m_16:
; ======== 保存参数
mov esi, eax
mov di, cx
; ======== 设置读取的扇区数
mov dx, 0x1f2 ; 0x1f2 是硬盘控制器的扇区计数寄存器端口
mov al, cl
out dx, al
; 恢复eax
mov eax, esi
; ========= 将LBA地址写入端口
; 低8位
mov dx, 0x1f3
out dx, al
; 中8位
mov dx, 0x1f4
shr eax, 8
out dx, al
; 次高8位
mov dx, 0x1f5
shr eax, 8
out dx, al
; 最高4位和设备编码(各4位)
mov dx, 0x1f6
shr eax, 8
and al, 0x0f
or al, 0xe0 ; 1110 表示LBA模式
out dx, al
; ========== 向0x1f7端口写入读命令 0x20
mov dx, 0x1f7
mov al, 0x20
out dx, al
; ========== 检查硬盘状态
; | 位 | 名称 | 含义 | 常用判断
; | 7 | BSY (Busy) | 硬盘忙 | `1=忙,不能操作`
; | 6 | DRDY (Ready) | 设备就绪 | `1=可以接受命令`
; | 5 | DF (Device Fault) | 设备错误 | `1=硬件故障`
; | 4 | DSC (Seek Complete) | 寻道完成 | 很少用
; | 3 | DRQ (Data Request) | 数据请求 | `1=可以读/写数据`
; | 2 | CORR (Corrected) | 数据纠错 | 一般忽略
; | 1 | IDX (Index) | 索引 | 基本不用
; | 0 | ERR (Error) | 错误发生 | `1=命令失败`
.disk_not_ready:
nop
mov dx, 0x1f7
in al, dx
; 检查硬盘是否可读写以及是否处于忙状态
and al, 0x88; 10001000
cmp al, 0x08; 00001000
jnz .disk_not_ready ; 若硬盘就绪且可以读写则往下执行否则持续等待
; ========== 从 0x1f0 端口读数据
mov ax, di ; di 是保存的扇区数
mov dx, 0x100
mul dx
mov cx, ax ; 一次读入一个字,需读 扇区数*512/2 次
mov dx, 0x1f0
.continue_read:
in ax, dx
mov [bx], ax
add bx, 2
loop .continue_read ; loop 次数由 cx 控制
ret

times 510-($-$$) db 0 ;填充到510字节
db 0x55, 0xaa ; magic number 用于检测是否为可启动扇区

;dd if=./mbr.bin of=./hd60M.img bs=512 count=1 conv=notrunc

实现从指定的硬盘扇区读取数据到内存。也就是读取 Loader,然后跳转过去执行。

0x02 Loader

Loader 功能就多了,加载 GDT 表,进入保护模式,加载 kernel,加载文件系统等等。LoaderMBR 和操作系统内核之间的桥梁。

GDT 表就是全局段描述符表,就是一张记录了记录内存中各个的基地址、大小、类型(代码/数据)、特权级(0~3)、是否可读写等属性等等细节的表。

保护模式下,CPU 通过段选择子(Segment Selector,可以简单想象为索引)在 GDT 中查找对应段描述符,获取段的基地址和访问权限。与实模式(段基址固定为段寄存器值左移4位)不同,保护模式下段的信息全部由 GDT 管理,实现更灵活、安全的内存访问.

那么实模式和保护模式又是什么?

在计算机启动时,只支持 20 位地址线,仅有1MB内存,cpu16位状态运行(寄存器默认16位但可以使用32位寄存器)且代码没有权限分级,内存也没有保护,处于裸奔状态,还好刚启动这个实模式状态仅仅从 BIOSMBR 再到Loader就差不都结束了,因为 Loader 会进行一些操作,然后切换到保护模式。

保护模式下, cpu 进入32位状态,支持权限分级、内存保护等等,有 4GB 大内存,支持虚拟内存、分页机制等等。

目前 Loader 实现到进入保护模式部分。如下

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
59
60
61
62
63
64
65
66
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR
; ============ 从 mbr 接管,跳转到 loader_start
jmp loader_start

; ============== 构建gdt及其内部描述符
; 63 56 55 52 51 48 47 45 44 43 40 39 32
; +------------------+-----+-----+-----+-----+--------------+------------+
; | Base 31:24 | G | D/B | L | AVL | Limit 19:16 | AccessByte | 高四字节含义
; +------------------+-----+-----+-----+-----+--------------+------------+
; | Base 23:0 | Limit 15:0 | 低四字节含义
; +--------------------------------+-----------------------------------+
; 31 16 15 0
GDT_BASE: dd 0x00000000 ;低4字节
dd 0x00000000 ;高4字节,无效描述符,防止选择子未初始化
CODE_DESC: dd 0x0000FFFF ;dd是伪指令,表示define double-word
dd DESC_CODE_HIGH4 ;代码段描述符
DATA_STACK_DESC: dd 0x0000FFFF
dd DESC_DATA_HIGH4 ;栈段描述符也是数据段描述符, 为了方便共用一个段
VIDEO_DESC: dd 0x80000007;limit=(0xbffff-0xb8000)/4k=0x7,这里的0xb8000~0xbffff是实模式下文本模式显存适配器的内存地址,因此段界限即为上述方程
dd DESC_VIDEO_HIGH4 ;此时dpl为0,此乃显存段描述符

GDT_SIZE equ $ - GDT_BASE ;计算当前GDT已经填充的大小
GDT_LIMIT equ GDT_SIZE - 1 ;
times 60 dq 0 ;此处预留60个描述符空位,这里跟上面一致,表示define quad-word ,也就是定义60个以0填充的段描述符,这里的times是循环次数

; ======== 定义选择子,段选择子 = 索引(index) + 表类型(TI) + 特权级(RPL)
SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0 ;相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0
SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0 ;同上类似
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0 ;同上类似

; ========= 定义gdt的指针,前2字节是gdt的界限,后4字节是gdt的起始地址 ---------
gdt_ptr dw GDT_LIMIT ;define word
dd GDT_BASE

loader_start:
; ========= 准备进入保护模式
; ===== 打开A20
in al, 0x92 ; 0x92 是 A20 地址线的控制端口
or al, 0000_0010B
out 0x92, al
; ====== 加载gdt
lgdt [gdt_ptr]
; ======= 将CR0的PE位置1
mov eax, cr0
or eax, 0x00000001
mov cr0, eax
; ====== 已经进入保护模式
jmp dword SELECTOR_CODE:p_mode_start ; 刷新流水线 使用dword(32位)防止保护模式下执行这条指令导致地址编码错误

[bits 32]
p_mode_start:
mov ax, SELECTOR_DATA ; 初始化数据段和栈段基址寄存器
mov ds, ax
mov es, ax
mov ss, ax
mov esp, LOADER_STACK_TOP ; 初始化栈顶
mov ax, SELECTOR_VIDEO ; 初始化显存基址寄存器
mov gs, ax

mov byte [gs:0x10], 'p' ; 打印提示表明程序顺利执行到这了

jmp $

; dd if=./loader.bin of=./hd60M.img bs=512 count=4 seek=2 conv=notrunc

至于头文件 boot.inc 中定义的内容就不放了,大部分是 GDT 相关的定义。

看起来完美,加载了 GDT,开启了高位地址线,将 cr0寄存器的模式控制位修改好了。使用 qemu 运行一下看看。

0x03 debug 过程

看起来进入保护模式失败了。仅输出了 MBR 的提示符。

调试看看

1
2
3
4
5
6
7
8
9
10
11
12
13
pwndbg> b *0x900
Breakpoint 1 at 0x900
pwndbg> c
Continuing.

Breakpoint 1, 0x00000900 in ?? ()
Permission error when attempting to parse page tables with gdb-pt-dump.
Either change the kernel-vmmap setting, re-run GDB as root, or disable `ptrace_scope` (`echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope`)
Exception occurred: context: 'NoneType' object has no attribute 'markers' (<class 'AttributeError'>)
For more info invoke `set exception-verbose on` and rerun the command
or debug it by yourself with `set exception-debugger on`
pwndbg> x/i 0x900
=> 0x900: jmp 0xb0b

可以看到 Loader 最初的jmp loader_start确实执行了,然而我们查看跳转到的地方:

1
2
3
4
5
6
7
8
pwndbg> x/50xb 0xb0b
0xb0b: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0xb13: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0xb1b: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0xb23: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0xb2b: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0xb33: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0xb3b: 0x00 0x00

全都是0。说明读取出了问题!然而把 Loader 修改为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
; 输出一段自定义的提示词
mov byte [gs:0x00],'L'
mov byte [gs:0x01],0x0F ; 0 表示黑色背景,F 表示前景色为白色

mov byte [gs:0x02],'o'
mov byte [gs:0x03],0x0F

mov byte [gs:0x04],'a'
mov byte [gs:0x05],0x0F

mov byte [gs:0x06],'d'
mov byte [gs:0x07],0x0F

mov byte [gs:0x08],'e'
mov byte [gs:0x09],0x0F

mov byte [gs:0x0a],'r'
mov byte [gs:0x0b],0x0F

jmp $
; dd if=./loader.bin of=./hd60M.img bs=512 count=1 seek=2 conv=notrunc

然后再运行,发现:

正常啊。那么在 Loader 前加上times 510-($-$$) db 0 ;填充到510字节。然后再运行,竟然又运行不了了!

那么问题就定位到扇区读取上了。因为超过 512 字节的部分会在另一个扇区,很可能 MBR 读取下一个扇区出了问题。

那么在硬盘读取部分打上断点。循环读取手动观察。发现一个很有意思的现象:

手动一次次继续执行读取,Loader后续正常读取了!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pwndbg> x/i 0x900
=> 0x900: jmp 0xb0b
pwndbg> x/20i 0xb09
0xb09: in al,0x92
0xb0b: or al,0x2
0xb0d: out 0x92,al
0xb0f: lgdtd [esi]
0xb12: add ecx,DWORD PTR [ebx]
0xb14: mov eax,cr0
0xb17: or ax,0x1
0xb1b: mov cr0,eax
0xb1e: jmp 0x0:0xb26
0xb24: or BYTE PTR [eax],al
0xb26: mov ax,0x10
0xb2a: mov ds,eax
0xb2c: mov es,eax
0xb2e: mov ss,eax
0xb30: mov esp,0x900
0xb35: mov ax,0x18
0xb39: mov gs,eax
0xb3b: mov BYTE PTR gs:0x10,0x70
0xb43: jmp 0xb43

然而如果不在循环读取处打断点直接让他快速读取完结果就全是0。这两者行为差别是:速度

手动打断点一直继续执行的读取速度比 qemu 执行速度慢太多太多了。那么读取太快竟然导致了异常,那么很可能是硬盘后续扇区还没准备好数据!

注意到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.disk_not_ready:
nop
mov dx, 0x1f7
in al, dx
; 检查硬盘是否可读写以及是否处于忙状态
and al, 0x88; 10001000
cmp al, 0x08; 00001000
jnz .disk_not_ready ; 若硬盘就绪且可以读写则往下执行否则持续等待
; ========== 从 0x1f0 端口读数据
mov ax, di ; di 是保存的扇区数
mov dx, 0x100
mul dx
mov cx, ax ; 一次读入一个字,需读 扇区数*512/2 次
mov dx, 0x1f0
.continue_read:
in ax, dx
mov [bx], ax
add bx, 2
loop .continue_read ; loop 次数由 cx 控制
ret

这里读取时候仅仅检查了一次硬盘状态,后续跨扇区读取时候没有再次检查。很可能就是这个问题了。把这一部分改成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    mov cx, di          ; 外层循环:扇区数
.read_sector:
push cx ; 保存外层循环索引 cx
.disk_not_ready:
nop
mov dx, 0x1f7
in al, dx
and al, 0x88 ; 10001000
cmp al, 0x08 ; 00001000 硬盘不忙且可以读写数据
jnz .disk_not_ready ; 等待当前扇区就绪
mov cx, 0x100 ; 内层:读256个字 = 512字节, 读一个扇区
mov dx, 0x1f0
.continue_read:
in ax, dx
mov [bx], ax
add bx, 2
loop .continue_read
pop cx
loop .read_sector
ret

然后调试发现一直卡在硬盘检查(此时Loader 的后续代码顺利读取了,说明就是此前跨扇区读取数据没准备好),查看eax寄存器值是 0x41,硬盘的错误位是 1!那么就是读取多扇区时候出现了错误!这是一个新的 bug。

检查返现我们读取 4 个扇区,Loader只占用两个扇区,此时是在读取第三个时候出现的错误,那么很可能就是我们制作虚拟硬盘镜像的时候出错了。重新制作一个硬盘镜像写入 MBRLoader。再执行:

太棒了成功了

然而此时窗口一直在闪烁,似乎一直在循环执行。调试发现进入保护模式成功后莫名奇妙又跳回了 MBR。搜索半天又询问 claude,得出答案是进入保护模式后 BIOS 的中断向量表就失效了,这时候如果发生硬件中断 (这里很可能是时钟中断),就会跳到奇怪的地方。

那么我们在进入保护模式前屏蔽 cpu 对中断的响应即可(加上 cli 指令),反正也用不上了,后续需要自己在内核实现中断。这时问题就彻底解决了。

最后完整代码如下

0x04 最终实现

mbr

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
%include "boot.inc"
; mbr 主引导程序
SECTION MBR vstart=0x7c00
mov ax, cs
mov ds, ax
mov ss, ax
mov es, ax
mov fs, ax
mov sp, 0x7c00
mov ax, 0xb800
mov gs, ax

; 清屏利用 0x06 号功能,上卷全部行,则可清屏。
; -----------------------------------------------------------
;INT 0x10 功能号:0x06 功能描述:上卷窗口
;------------------------------------------------------
;输入:
;AH 功能号= 0x06
;AL = 上卷的行数(如果为 0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;无返回值:
mov ax, 0x600
mov bx, 0x700
mov cx, 0 ; 左上角: (0, 0)
mov dx, 0x184f ; 右下角: (80,25),
; VGA 文本模式中,一行只能容纳 80 个字符,共 25 行。
; 下标从 0 开始,所以 0x18=24,0x4f=79
int 0x10 ; int 0x10

; 输出一段自定义的提示词
mov byte [gs:0x00],'M'
mov byte [gs:0x01],0x0F ; 0 表示黑色背景,F 表示前景色为白色

mov byte [gs:0x02],'B'
mov byte [gs:0x03],0x0F

mov byte [gs:0x04],'R'
mov byte [gs:0x05],0x0F

; 准备从硬盘读入loader
mov eax, LOADER_START_SECTOR ;起始扇区LBA地址
mov bx, LOADER_BASE_ADDR ;写入的内存地址
mov cx, 4 ;待读入的扇区数 这里读入4个扇区 (loader比较大)
call rd_disk_m_16 ;以下读取程序的起始部分(一个扇区)
jmp LOADER_BASE_ADDR

; | 端口 | 作用
; | 0x1F0 | 数据端口
; | 0x1F1 | 错误
; | 0x1F2 | 扇区数
; | 0x1F3 | LBA低8位
; | 0x1F4 | LBA中8位
; | 0x1F5 | LBA高8位
; | 0x1F6 | 设备+LBA高4位
; | 0x1F7 | 命令 / 状态


rd_disk_m_16:
; ======== 保存参数
mov esi, eax
mov di, cx
; ======== 设置读取的扇区数
mov dx, 0x1f2 ; 0x1f2 是硬盘控制器的扇区计数寄存器端口
mov al, cl
out dx, al
; 恢复eax
mov eax, esi
; ========= 将LBA地址写入端口
; 低8位
mov dx, 0x1f3
out dx, al
; 中8位
mov dx, 0x1f4
shr eax, 8
out dx, al
; 次高8位
mov dx, 0x1f5
shr eax, 8
out dx, al
; 最高4位和设备编码(各4位)
mov dx, 0x1f6
shr eax, 8
and al, 0x0f
or al, 0xe0 ; 1110 表示LBA模式
out dx, al
; ========== 向0x1f7端口写入读命令 0x20
mov dx, 0x1f7
mov al, 0x20
out dx, al
; ========== 检查硬盘状态
; | 位 | 名称 | 含义 | 常用判断
; | 7 | BSY (Busy) | 硬盘忙 | `1=忙,不能操作`
; | 6 | DRDY (Ready) | 设备就绪 | `1=可以接受命令`
; | 5 | DF (Device Fault) | 设备错误 | `1=硬件故障`
; | 4 | DSC (Seek Complete) | 寻道完成 | 很少用
; | 3 | DRQ (Data Request) | 数据请求 | `1=可以读/写数据`
; | 2 | CORR (Corrected) | 数据纠错 | 一般忽略
; | 1 | IDX (Index) | 索引 | 基本不用
; | 0 | ERR (Error) | 错误发生 | `1=命令失败`
mov cx, di ; 外层循环:扇区数
.read_sector:
push cx ; 保存外层循环索引 cx
.disk_not_ready:
nop
mov dx, 0x1f7
in al, dx
and al, 0x88 ; 10001000
cmp al, 0x08 ; 00001000 硬盘不忙且可以读写数据
jnz .disk_not_ready ; 等待当前扇区就绪
mov cx, 0x100 ; 内层:读256个字 = 512字节, 读一个扇区
mov dx, 0x1f0
.continue_read:
in ax, dx
mov [bx], ax
add bx, 2
loop .continue_read
pop cx
loop .read_sector
ret

times 510-($-$$) db 0 ;填充到510字节
db 0x55, 0xaa ; magic number 用于检测是否为可启动扇区

;dd if=./mbr.bin of=./hd60M.img bs=512 count=1 conv=notrunc

Loader

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
59
60
61
62
63
64
65
66
67
68
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR
; ============ 从 mbr 接管,跳转到 loader_start
jmp loader_start

; ============== 构建gdt及其内部描述符
; 63 56 55 52 51 48 47 45 44 43 40 39 32
; +------------------+-----+-----+-----+-----+--------------+------------+
; | Base 31:24 | G | D/B | L | AVL | Limit 19:16 | AccessByte | 高四字节含义
; +------------------+-----+-----+-----+-----+--------------+------------+
; | Base 23:0 | Limit 15:0 | 低四字节含义
; +--------------------------------+-----------------------------------+
; 31 16 15 0
GDT_BASE: dd 0x00000000 ;低4字节
dd 0x00000000 ;高4字节,无效描述符,防止选择子未初始化
CODE_DESC: dd 0x0000FFFF ;dd是伪指令,表示define double-word
dd DESC_CODE_HIGH4 ;代码段描述符
DATA_STACK_DESC: dd 0x0000FFFF
dd DESC_DATA_HIGH4 ;栈段描述符也是数据段描述符, 为了方便共用一个段
VIDEO_DESC: dd 0x80000007;limit=(0xbffff-0xb8000)/4k=0x7,这里的0xb8000~0xbffff是实模式下文本模式显存适配器的内存地址,因此段界限即为上述方程
dd DESC_VIDEO_HIGH4 ;此时dpl为0,此乃显存段描述符

GDT_SIZE equ $ - GDT_BASE ;计算当前GDT已经填充的大小
GDT_LIMIT equ GDT_SIZE - 1 ;
times 60 dq 0 ;此处预留60个描述符空位,这里跟上面一致,表示define quad-word ,也就是定义60个以0填充的段描述符,这里的times是循环次数

; ======== 定义选择子,段选择子 = 索引(index) + 表类型(TI) + 特权级(RPL)
SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0 ;相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0
SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0 ;同上类似
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0 ;同上类似

; ========= 定义gdt的指针,前2字节是gdt的界限,后4字节是gdt的起始地址 ---------
gdt_ptr dw GDT_LIMIT ;define word
dd GDT_BASE

loader_start:
; ========= 准备进入保护模式
cli ; 关闭中断, 因为进入保护模式后 BIOS 的中断向量表不再有效, 如果发生时钟中断则有可能导致异常现象
; ===== 打开A20
in al, 0x92 ; 0x92 是 A20 地址线的控制端口
or al, 0000_0010B
out 0x92, al
; ====== 加载gdt
lgdt [gdt_ptr]
; ======= 将CR0的PE位置1
mov eax, cr0
or eax, 0x00000001
mov cr0, eax
; ====== 已经进入保护模式
jmp dword SELECTOR_CODE:p_mode_start ; 刷新流水线 使用dword(32位)防止保护模式下执行这条指令导致地址编码错误

[bits 32]
p_mode_start:
mov ax, SELECTOR_DATA ; 初始化数据段和栈段基址寄存器
mov ds, ax
mov es, ax
mov ss, ax
mov esp, LOADER_STACK_TOP ; 初始化栈顶
mov ax, SELECTOR_VIDEO ; 初始化显存基址寄存器
mov gs, ax

mov byte [gs:0x10], 'p' ; 打印提示表明程序顺利执行到这了
mov byte [gs:0x11], 0x0F

jmp $

; dd if=./loader.bin of=./hd60M.img bs=512 count=4 seek=2 conv=notrunc