第三章 让MBR直接操作硬盘,硬件深似海

本文最后更新于:1 年前

第三章 让MBR直接操作硬盘,硬件深似海

写在前面

7月7日,这一章的概念实在繁多,加上自己也没有汇编的基础,只能硬着头皮攻,加上晚上有学生会主席的竞选,心烦意乱,不过为了完成日更的目标,还是要尽力去做。面对压力时也不要被负能量击溃,我也始终保持着寻找乐趣的心态去学习、阅读。

进入正题

本章一上来讲述了许多晦涩难懂的概念,涉及到了一些汇编的知识,也讲述了一些NASM编译器的使用方法。我在初读这些知识的时候实在无心仔细研究,但是在进行到后面章节的时候发现这些前缀知识是必不可少的。我建议读者在初读的时候如果实在烦躁,可以适当跳过一些内容,等到后面进行编程的时候遇到不会的点再回过头来看,切忌在理论知识的重压下失去信心从而放弃。

然后咱们还是继续主线任务,上一章节我们完成了一个简单MBR编写,并且成功运行在了bochs上,但是需要注意的是这时我们的输出是建立在软件的基础上的,我们最终要实现在显卡上执行任务,首先从修改mbr.S的输出打印部分开始。这里源码我在这里粘出来吧。

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
; mbr.S

; 主引导程序
; --------------------------------------------------

SECTION MBR vstart=0x7c00 ; 把起始地址编译为 0x7c00
mov ax, cs ; cs 代码段寄存器
mov ds, ax ; dx 数据段寄存器
mov es, ax ; es 附加段寄存器
mov ss, ax ; ss 堆栈段寄存器
mov fs, ax ; fs 80386 后添加的寄存器,无全称
mov sp, 0x7c00 ; sp 堆栈指针寄存器
mov ax, 0xb800 ;
mov gs,ax;

; 清屏
; --------------------------------------------------
; 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
mov dx, 0x184f ; 右下角: (80, 25)
; VGA 文本模式种,一行只能容纳 80 个字符,共 25 行
; 下标从 0 开始,所以 0x18=24, 0x4f=79

int 0x10 ; int 0x10
;;;;;;;;输出背景色绿色,前景色红色,并且跳动的字符串;;;;;;;;
mov byte [gs:0x00], '1';
mov byte [gs:0x01], 0xA4;

mov byte [gs:0x02], ' ';
mov byte [gs:0x03], 0xA4;

mov byte [gs:0x04], 'M';
mov byte [gs:0x05], 0xA4;

mov byte [gs:0x06], 'B';
mov byte [gs:0x07], 0xA4;

mov byte [gs:0x08], 'R';
mov byte [gs:0x09], 0xA4;
;;;;;;;;;;;;;;;;;;;;;;打印字符串结束;;;;;;;;;;;;;;;;
jmp $; 程序悬停在此

times 510-($-$$) db 0
db 0x55,0xaa

后面介绍了bochs的调试方法,等后面遇到不懂得知识时回看也不迟。当然,512字节的MBR是没法满足为内核准备好环境的功能的,所以我们需要另一个更大的程序完成这项任务,它就是loader,加载器。所以MBR需要把loader从硬盘加载到内存中,在第二章中有提到两块可用区域,加载到那里面就ok了,我跟作者一样,也把loader加载到0x900这里。

在读取硬盘扇区这一部分。建议先好好阅读一下本节的前置章节,硬盘部分,再看代码就不会再有吃力的地方了。至此,mbr.S的任务也就结束了,接下来我们写一个简单的loader来进行一个结果实现,验证一下思路是否正确就ok了。还是贴出代码:

mbr.S

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
; mbr.S

; 主引导程序
; --------------------------------------------------
%include "boot.inc";
;LOADER_BASE_ADDR equ 0x900 将loader加载到内存0x900
;LOADER_START_SECTOR equ 0x2 loader位于磁盘第2块扇区

SECTION MBR vstart=0x7c00 ; 把起始地址编译为 0x7c00
mov ax, cs ; cs 代码段寄存器
mov ds, ax ; dx 数据段寄存器
mov es, ax ; es 附加段寄存器
mov ss, ax ; ss 堆栈段寄存器
mov fs, ax ; fs 80386 后添加的寄存器,无全称
mov sp, 0x7c00 ; sp 堆栈指针寄存器

mov ax, 0xb800;
mov gs, ax;

; 清屏
; --------------------------------------------------
; 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
mov dx, 0x184f ; 右下角: (80, 25)
; VGA 文本模式种,一行只能容纳 80 个字符,共 25 行
; 下标从 0 开始,所以 0x18=24, 0x4f=79

int 0x10 ; int 0x10


;;;;;下面代码是新增功能;;;;;;;;;;;;;;;;;;;;
mov eax, LOADER_START_SECTOR; 磁盘中loader的LBA地址
mov bx, LOADER_BASE_ADDR; loader加入内存的起始地址
mov cx, 1; 待读入内存的扇区数
call rd_disk_m_16;

jmp LOADER_BASE_ADDR;
;--------------------------------------------------------------
;功能:读取硬盘n个扇区
rd_disk_m_16:
;----------------------------------------------------------
mov esi, eax; 备份eax al在in/out指令会被使用
mov di, cx; 备份cx cl会在接下来代码中频繁使用
;读写硬盘:
;第一步:设置要读取的扇区数 1
mov dx, 0x1f2; 配置的硬盘是ata0-master 是Primary通道 主盘
;sector count寄存器是0x1f2端口
mov al, cl;
out dx, al; 从内存把扇区数1输出到端口号0x1f2;
mov eax, esi; 恢复eax

;第二步 将LBA地址存入0x1f3~0x1f6
;LBA地址7~0位写入端口0x1f3
mov dx, 0x1f3;
out dx, al;

;LBA地址15~8位写入端口0x1f4
mov cl, 8;
shr eax, cl;
mov dx, 0x1f4;
out dx, al;

;LBA地址23~16位写入端口0x1f5
shr eax, cl;
mov dx, 0x1f5;
out dx, al;

;LBA地址24~27位写入端口0x1f6
shr eax, cl;
and al, 0x0f; 与运算al中低四位为LBA地址24~27位
or al, 0xe0; 或运算拼出0x1f6的高四位1110 第6位为1表示LBA
mov dx, 0x1f6;
out dx, al;

;第三步 向0x1f7端口写入读命令,0x20
mov dx, 0x1f7;
mov al, 0x20;
out dx, al; 命令 写入端口0x1f7后,硬盘立即开始工作,将数据放入硬
; 盘控制器的缓冲区

;第四步 检测硬盘状态,判断loader是否已经读入0x1f0端口中
.not_ready:
nop;
in al, dx; 将端口0x1f7的status写入al;
and al, 0x88; 获得status的第3位和第7位;
cmp al, 0x08; 与第3位相减作比较 会影响ZF CF PF
jnz .not_ready; ZF不等于0就跳,相当于循环等缓冲区中的数据准备好为止

;第5步 将0x1f0端口的数据搬向内存
;5.1 计算搬运次数
mov ax, di;
mov dx, 256; 0x1f0端口是16比特
mul dx; di*512字节/2字节=搬运次数 16位乘法乘积32位
;高16位在dx,低16位在ax;
mov cx, ax; dx=1 乘积高16位是0,故把低16位移入cx
;5.2 循环搬运至内存
mov dx, 0x1f0;
.go_on_read: ;我们的loader只有一个扇区512字节
in ax, dx; ;bx的寻址范围位64KB 65536字节
mov [bx], ax; 该循环不能加载大于64KB的程序于内存
add bx, 2;
loop .go_on_read; cx不等于0 就回到循环处继续搬
ret ;搬运完loader 回到LOADER_BASE_ADDR;


times 510-($-$$) db 0 ; 填充文件末尾的魔数 0xaa55 和当前位置之间的空间
; 保证编译后生成的文件大小为 512 字节(硬盘一个扇区的大小)
db 0x55, 0xaa

loader.S

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
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR

mov byte [gs:0x00], '2';
mov byte [gs:0x01], 0xA4;

mov byte [gs:0x02], ' ';
mov byte [gs:0x03], 0xA4;

mov byte [gs:0x04], 'L';
mov byte [gs:0x05], 0xA4;

mov byte [gs:0x06], 'O';
mov byte [gs:0x07], 0xA4;

mov byte [gs:0x08], 'A';
mov byte [gs:0x09], 0xA4;

mov byte [gs:0x0a], 'D';
mov byte [gs:0x0b], 0xA4;

mov byte [gs:0x0c], 'E';
mov byte [gs:0x0d], 0xA4;

mov byte [gs:0x0e], 'R';
mov byte [gs:0x0f], 0xA4;

jmp $;

boot.inc

1
2
3
;----------------------loader AND kernel-----------------------------
LOADER_BASE_ADDR equ 0x900 ;loader.s加载到内存地址0x900
LOADER_START_SECTOR equ 0x2 ;loader.s刻入硬盘0盘0道2扇区(LBA)

写在后面

建议读者在阅读的时候不要偏离主线,作者花费了大量的篇幅去补充理论知识,但我们还是要记得自己的主线任务是os的实现,所以要秉持着理论服侍实践的想法去阅读。我虽阅读时间还很短,但能感觉到自己是真的学到并运用了一些知识,后面就要进入保护模式了,但我的夏令营集训也开始了,希望flag能完成.