引言: 持续的努力和坚持才会有所收获

地址和vstart

编译器给程序中各符号(变量名或者函数名等)分配的地址,就是各位符号相对于文件开头的偏移量.
section 中数据的地址依然是相对于整个文件的顺延,仅仅是在逻辑上让开发人员梳理程序之用.

vstart是虚拟起始地址, 为 section 内的数据指定一个起始地址,从这个地址往后编排 data 和 code.

CPU的实模式

CPU的工作原理

CPU大概分为三个单元: 控制单元, 运算单元, 存储单元.大概的工作原理为: 控制单元要取下一条待运行的指令, 该指令的地址在程序计数器PC中,
在x86CPU上,程序计数器就是CS:IP. 取地址,放入指令寄存器IR, 指令译码器译码, 然后操作控制器给运算下令. ip累加指向下一个地址. 重复整个过程.

实模式下的寄存器

寄存器有很多种类, CPU是用 “段基址 : 段内偏移地址” 的方式来访问内存, 段基址就是段寄存器来存储的. CPU通过总线地址访问内存, 而这个地址存在段寄存器里.
一般有 CS, DS, ES, FS, GS, SS 等寄存器. CS: 代码段寄存器, 存放指令的地址, DS: 数据段寄存器, 存放数据, ES: 附加段寄存器, 存放附加数据, FS: 附加段寄存器, 存放附加数据, GS: 附加段寄存器, 存放附加数据, SS: 堆栈段寄存器, 存放栈. CS是可见寄存器, IP是不可见寄存器, ip是自动累加的.

flags寄存器, 展示CPU内部各项设置,指标, 任何一个指令的执行, 其过程的细节, 对计算机造成了哪些影响 都在flags中通过一些标志位反映出来.

通用寄存器, AH, AL, BH, BL, CH, CL, DH, DL, SI, DI, SP, BP, 这些寄存器都是通用寄存器, AX 是由 AH 和 AL 两个 8 位寄存器组成, 后面6个同理.
EAX, 是在 AX 基础上扩展高 16 位. 通用寄存器可以用来保存任何数据, 包括地址. 但是也有一些基本的公共约定, 比如 CX 用来保存循环计数器, BX 用来保存基址指针, DX 用来保存数据指针, 等等. 详情见 书本P76 表3-2.

实模式下的内存分段的来源

实模式的实 体现在: 程序中用到的地址都是真实的物理地址. 为什么用8086的CPU型号讲解, 因为8086是第一个x86的CPU, 后面有 286, 386, 486, 等等. 8086引入了段的概念, 告别了内存硬编码时代. 8086中, 寄存器是 16 位的, 所以需要通过分段的方式来访问 20 位的物理地址.

实模式下的CPU的内存寻址方式

寄存器寻址, 立即数寻址, 内存寻址, 其实都差不多, 作者这里大废口舌照顾没有计组基础的同学理解. 硬件中的栈, 栈的基础地址,放在 SS 中, 栈顶指针放在 BP 中. 栈的增长方向是向下增长的, 计算方式是 SS * 16 + SP/BP. SP一般不改, 由 BP 读取.

对显示器说点什么

IO接口

IO接口是链接CPU和外部设备的逻辑控制部件, IO接口大概有如下的功能:

  1. 设置数据缓冲区
  2. 设置信号电平转换电路
  3. 设置数据格式转换 (模拟量和数字量)
  4. 设置时序控制电路来同步CPU和外部设备
  5. 提供地址译码

同一时刻,CPU只能和一个IO接口通信, 这个分配工作交给南桥芯片,输入输出控制中心(ICH)完成, 不过现在都集成到了CPU中. 南桥负责管理pci, pcie, AGP等低速设备.
IO接口要通过寄存器的方式和CPU通信, 为了区别于CPU内部的寄存器,叫做端口.

显卡概述

显卡给我们提供了可编程接口是,IO端口和显存. 拼凑像素打印的方式很蠢, 于是有一个字节的编码对应一个字符的方法, 这也是ASCII码. 所以接下来,我们来试着输出点东西.
从地址总线起始地址 0xB8000 到 0xBFFFF, 这 32KB 大小的显存用于文本显示, 文本模式又有多种模式,默认是 80*25 . 一般一个字符有两个字节来表示, 第一个是ASCII码,第二个是颜色之类(前景色和背景色)的信息.

以下部分是实验代码:

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
;主引导程序
;-----------------------------------------------------
SECTION MBR vstart=0x7c00 ;告诉编译器,起始地址是0x7c00
mov ax,cs ;因为BIOS执行完毕后cs:ip为0x0:0x7c00,所以用cs初始化各寄存器(此时cs=0)
mov ds,ax ;ds、es、ss、fs不能给立即数初始化,需要用ax寄存器初始化
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00 ;初始化堆栈指针,因为目前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 ;上卷行数:全部 功能号:06
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


; 输出背景色绿色, 前景色红色, 并且跳动的字符串"1 MBR"

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 调试方法

bochs为我们提供了丰富的调试功能, 我们直接进入bochs查看它提供的功能. 输入help命令查看帮助信息.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Please choose one: [6] 6
00000000000i[ ] installing x module as the Bochs GUI
00000000000i[ ] using log file bochs.out
Next at t=0
(0) [0x0000fffffff0] f000:fff0 (unk. ctxt): jmpf 0xf000:e05b ; ea5be000f0
<bochs:1> help
h|help - show list of debugger commands
h|help command - show short command description
-*- Debugger control -*-
help, q|quit|exit, set, instrument, show, trace, trace-reg,
trace-mem, u|disasm, ldsym, slist
-*- Execution control -*-
c|cont|continue, s|step, p|n|next, modebp, vmexitbp
-*- Breakpoint management -*-
vb|vbreak, lb|lbreak, pb|pbreak|b|break, sb, sba, blist,
bpe, bpd, d|del|delete, watch, unwatch
-*- CPU and memory contents -*-
x, xp, setpmem, writemem, crc, info,
r|reg|regs|registers, fp|fpu, mmx, sse, sreg, dreg, creg,
page, set, ptime, print-stack, ?|calc
-*- Working with bochs param tree -*-
show "param", restore

大体上,bochs 的调试命令分为 “Debugger Control” , “Execution control”, “Breakpoint management”, “CPU and memory contents” 四大类. 需要用的时候,翻书查询即可.

硬盘控制

硬盘中的扇区在物理上是用"柱面-磁头-扇区"来定位的, 简称为CHS, 但是我们要事先算出扇区是在哪个盘面,哪个柱面, 于是有了逻辑块地址(LBA) ,我们使用的控制方式是LBA28 (支持 2^28 = 268435456 扇区) 每个扇区是512字节, 最大支持支持 128GB. 现在用的是 LBA48.

接下来我们来讲解 LBA寄存器, LBAlow LBAmid LBAhigh 三个寄存器, LBAlow 存放扇区的低 8 位, LBAmid 存放扇区的中间 8 位, LBAhigh 存放扇区的高 8 位.
剩下 4 位放在 device寄存器低四位里面, 还有一个status寄存器比较重要,这些具体的参数位置复习 p128即可.

我们约定一个操作硬盘的顺序:

  1. 先选择通道,往该通道的sector count写入待操作扇区数
  2. 往该通道上三个LBA寄存器写入扇区起始地址的低24位
  3. 往device寄存器中写入LBA地址的24~27位,并置第6位为1,使其为LBA模式,设置第4位选择主盘或从盘
  4. 往该通道上command寄存器中写入操作命令
  5. 读取该通道上的status寄存器,判断硬盘工作是否完成
  6. 如果以上步骤是读硬盘进入下一个步骤,否则完工。
  7. 将硬盘数据读出

完成上述步骤后,我们可以开始传送数据,数据传送方式有一下几种:

  1. 无条件传送方式
  2. 查询传送方式
  3. 中断传送方式
  4. 直接存储器传送方式
  5. I/O 处理机传送方式

其中我们的操作系统只会实现第二种和第三种查询方式, 因为4,5是需要而外的硬件支持的. 硬盘和内存直接交互.

让 MBR 使用硬盘

终于又到了实验部分, 我们需要一个程序来完成为操作系统内核准备环境并加载. 所以我们mbr的使命就是负责从硬盘上把 loader加载到内存.
我们把 loader放到第二扇区, 接着我们需要查看实模式下的内存布局有哪些内存可以用. 0x500-0x7BFF,0x7E00-9FBFF,这两段内存都可以.

然后我们要确定以下loader的大小, loader首先要定义一些数据结构 (如 GDT 全局描述符表) 等.所以尽量放在低处. 作者选择了 0x900.

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
127
128
;主引导程序
;-----------------------------------------------------
%include "boot.inc" ;包含boot.inc文件,里面有一些常用的函数
SECTION MBR vstart=0x7c00 ;告诉编译器,起始地址是0x7c00
mov ax,cs ;因为BIOS执行完毕后cs:ip为0x0:0x7c00,所以用cs初始化各寄存器(此时cs=0)
mov ds,ax ;ds、es、ss、fs不能给立即数初始化,需要用ax寄存器初始化
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00 ;初始化堆栈指针,因为目前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 ;上卷行数:全部 功能号:06
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


; 输出背景色绿色, 前景色红色, 并且跳动的字符串"1 MBR"

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

;--------------------------------------------------------------------------


mov eax ,LOADER_START_SECTOR ;起始扇区lba地址
mov bx ,LOADER_BASE_ADDR ;写入的地址
mov cx ,1 ;待读入的扇区数
call rd_disk_m_16 ;以下读取程序的起始部分

jmp LOADER_BASE_ADDR ;跳转到Loader

;--------------------------------------------------------------------------
;功能:读取eax=LBA扇区号
rd_disk_m_16:
mov esi ,eax ;备份eax
mov di ,cx ;备份cx

;读写硬盘
;1---设置要读取的扇区数
mov dx ,0x1f2 ;设置端口号,dx用来存储端口号的
mov al ,cl
out dx ,al ;读取的扇区数

mov eax ,esi ;恢复eax


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

;LBA 15~8位写入端口0x1f4
mov cl ,8
shr eax ,cl ;逻辑右移8位,将eax的最低8位移掉,让最低8位al的值变成接下来8位
mov dx ,0x1f4
out dx ,al

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

shr eax ,cl
and al ,0x0f ;设置lba 24~27位
or al ,0xe0 ;设置7~4位是1110表示LBA模式
mov dx ,0x1f6
out dx ,al

;3---向0x1f7端口写入读命令0x20
mov dx ,0x1f7
mov al ,0x20
out dx ,al

;4---检测硬盘状态
.not_ready:
;同写入命令端口,读取时标示硬盘状态,写入时是命令
nop
in al ,dx
and al ,0x88 ;第三位为1表示已经准备好了,第7位为1表示硬盘忙
cmp al ,0x08
jnz .not_ready

;5---0x1f0端口读取数据
mov ax ,di ;要读取的扇区数
mov dx ,256 ;一个扇区512字节,一次读取2字节,需要读取256次
mul dx ;结果放在ax里
mov cx ,ax ;要读取的次数

mov dx ,0x1f0
.go_on_read:
in ax, dx
mov [bx], ax ;bx是要读取到的内存地址
add bx, 0x02
loop .go_on_read ;循环cx次
ret

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

大概功能就是先显示 1 mbr 的字符, 然后跳转到读取硬盘的函数,跳转这个函数前需要保存一些参数到寄存器(见注释), 接下来就是按照前文讲解的端口规则写入参数, 通过循环不断的读出硬盘的内容放到内存(栈中)
因为栈的起始地址已经被固定为 0x900 所以读取完后直接跳转到这里运行加载好的loader. 我们编译mbr,并加载到硬盘中, 然后把loader加载到序号为2的扇区.

nasm -I include/ -o mbr.bin mbr.s

dd if=./boot/mbr3.bin of=hd60M.img bs=512 count=1 conv=notru

下面是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
%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 $

nasm -I include/ -o loader.bin loader.S

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

实验效果: 从 1 mbr -> 2 loader
mbr

loader