OS学习笔记01-接管裸机控制权

实验任务

设计一个引导扇区程序,程序功能是:
在屏幕上显示运动内容,并在触碰到边框时反弹,可以加入变色等其它功能。
将这个程序的机器码放进放进虚拟软盘的首扇区,并用此软盘引导无操作系统的虚拟机,直到成功。

基础原理

主引导记录(MBR)工作原理:

计算机启动后,由BIOS检查硬件并根据指定的顺序,检查引导设备(本次实验中的虚拟软盘)的第一扇区
(即前512B且以0x55AA结尾的内容,也就是我们的主引导记录),加载至内存地址0x7C00,并开始运行;其它内存地址如下所示:

内存分配方式

显示原理:

显存首地址为0xB800,以两个字节来控制相应屏幕位置的显示内容。其中低字节为显示字符的ascii码,高字节为显示颜色,显示颜色如下表:

显示模式
其中,颜色默认为0x07,即黑底白字;

实验工具和环境说明

  • 编辑器:Notepad++,支持语法高亮;
  • 汇编器:NASM,与MASM有部分区别,后文详述;
  • 机器码编辑器:WinHex,用于对COM进行修改使其符合MBR的格式
  • 虚拟机:VMware,用于生成裸机

程序流程及源码

显示个人信息跳动规迹,并变色;

流程图

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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
;显示学号姓名缩写
DRt equ 1 ;D-Down,U-Up,R-right,L-Left
URt equ 2 ;
ULt equ 3 ;
DLt equ 4 ;
delay equ 50000 ;计时器延迟计数,用于控制画框的速度
ddelay equ 580 ;延迟580*50000次
Hm equ 25 ;最大高度
Lm equ 80 ;最大宽度
len equ 14 ;字符串长度

BOOTSEG equ 0x7c00 ;段基址0x7c00
DISPLAYSEG equ 0xb800 ;显存及地址0xb800
org BOOTSEG ;告诉汇编器要在0x7c00执行

section .text
_start:

;初始化数据段,使其指向0X7C00处,即Boot代码被加载的地方
mov ax, cs
mov ds, ax

;将文本显示内存段基址 放在ES中,供后面显示字符使用
mov ax, DISPLAYSEG
mov es, ax

loop1:
dec word[count] ; 递减计数变量
jnz loop1 ; >0:跳转;
mov word[count],delay
dec word[dcount] ; 递减计数变量
jnz loop1
mov word[count],delay
mov word[dcount],ddelay
;mov al, 20H ;空格覆盖
;mov ah, 0FH
;mov [es:bx], ax
dec byte[color]
cmp byte[color], 01H
jz reset
back:
mov al, DRt ;↘
cmp al, byte[dir]
jz DRF
mov al, DLt ;↙
cmp al, byte[dir]
jz DLF
mov al, URt ;↗
cmp al, byte[dir]
jz URF
mov al, ULt ;↖
cmp al, byte[dir]
jz ULF
jmp $

DRF:
inc word[x]
inc word[y] ;向右下前进一格
mov ax, Hm
mov bx, word[x] ;判断x是否越界
cmp ax, bx
jz dr2ur
mov ax, Lm-len
mov bx, word[y] ;判断y是否越界
cmp ax, bx
jz dr2dl
jmp display
dr2ur: ;回弹——从右下改为右上
mov word[x], Hm-2
mov ax, Lm-len ;判断是否为角落
mov bx, word[y]
cmp ax, bx
jz drA
mov byte[dir], URt

jmp display
dr2dl:
mov word[y], Lm-len-2
mov byte[dir], DLt

jmp display
drA: ;角落时原路返回
mov word[y],Lm-len-2
mov byte[dir], ULt

jmp display

DLF:
inc word[x]
dec word[y]
mov ax, Hm
mov bx, word[x]
cmp ax, bx
jz dl2ul
mov ax, -1
mov bx, word[y]
cmp ax, bx
jz dl2dr
jmp display
dl2ul:
mov word[x], Hm-2
mov ax, -1
mov bx, word[y]
cmp ax, bx
jz dlA
mov byte[dir], ULt

jmp display
dl2dr:
mov word[y], 1
mov byte[dir], DRt

jmp display
dlA:
mov word[y], 1
mov byte[dir], URt

jmp display

URF:
dec word[x]
inc word[y]
mov ax, -1
mov bx, word[x]
cmp ax, bx
jz ur2dr
mov ax, Lm-len
mov bx, word[y]
cmp ax, bx
jz ur2ul
jmp display
ur2dr:
mov word[x], 1
mov ax, Lm-len
mov bx, word[y]
cmp ax, bx
jz urA
mov byte[dir], DRt

jmp display
ur2ul:
mov word[y], Lm-len-2
mov byte[dir], ULt

jmp display
urA:
mov word[y], Lm-len-2
mov byte[dir], DLt

jmp display

ULF:
dec word[x]
dec word[y]
mov ax, -1
mov bx, word[x]
cmp ax, bx
jz ul2dl
mov ax, -1
mov bx, word[y]
cmp ax, bx
jz ul2ur
jmp display
ul2dl:
mov word[x], 1
mov ax, -1
mov bx, word[y]
cmp ax, bx
jz ulA
mov byte[dir], DLt

jmp display
ul2ur:
mov word[y], 1
mov byte[dir], URt

jmp display
ulA:
mov word[y], 1
mov byte[dir], DRt

jmp display

display: ;显示模块
mov ax, word[x]
mov bx, Lm
mul bx
add ax, word[y]
mov bx, 2
mul bx
mov bx, ax ;bx = ax = (x*80+y) * 2
mov cx, len
mov si, info
mov ah, [color]
loop_str: ;字符串显示模块
mov al, [si]
mov [es:bx], ax
inc si
inc bx
inc bx
loop loop_str

jmp loop1

reset:
mov byte[color], 0FH
jmp back
end:
jmp $

;section .data

info db " ygz 16337287 ",0
count dw delay
dcount dw ddelay
dir db DRt ; 向右下运动
x dw 2
y dw 0
color db 0FH

times 510-($-$$) db 0 ;填充空格
dw 0xaa55

实验步骤

  1. 在NotePad++中进行编辑程序;
  2. 在生成软盘文件:

    • 打开CMD终端窗口至asm文件所在的文件夹,输入
      nasm -f bin %name%.asm -o %name%.com > amsg.txt%name%为文件名);
    • 或直接输入na %name%.asm快速生成com文件,na.bat内容如下:
      @echo off
      set name=%~n1
      nasm -f bin %name%.asm -o %name%.COM > amsg.txt
      type amsg.txt |find “Error”
      type amsg.txt |find “Warn”
    • 生成com文件,打开com文件将0x1FE和0x1FF改为0x55和0xAA,保存为flp软盘格式:

      flp文件

    • 另:若代码末尾已经加上了
      times 510-($-$$) db 0 ;填充空格
      dw 0xaa55 ;加上55AA,符合MBR特征
      则不必再修改com文件,直接生成为flp文件即可
  3. 创建无操作系统的裸机:

虚拟机配置

  1. 将flp文件装载进虚拟机作为MBR引导启动:

加载虚拟软盘

结果截图

  1. 显示有规迹的‘A’:
    'A'带规迹
  2. 显示动态无规迹的‘A’+固定的个人信息‘ ygz 16337287 ’:
    'A'无规迹
  3. 逐字显示动态变色个人信息,并保留痕迹:
    逐字显示
  4. 整行显示动态变色个人信息:
    整行显示

接下来就是各种踩坑和填坑的经历了

关于NASM:

  1. NASM的标识符区分大小写;
  2. NASM中,memory操作数直接是内容地址,并且不保存数据类型,导致以下两个区别:
    1. 在读取数据内容时,要加方括号[ ] 指明是地址的内容,若没有方括号则是首地址的值;
    2. 在操作数据内容时且另一个操作数是常数(非ax、ah这种能够知道数据大小的寄存器),要加上数据类型如byte、word等;
    3. 不需要ptr指示字;
  3. 提供segment时,NASM要在方括号内提供segment,如:MOV [es:bx], ax
  4. 段定义方式,NASM定义数据段section .data,定义代码段section .text
  5. equ类似C语言中的define,用于定义整数常量;(注意,不能定义浮点数,且最长为8bytes)
  6. times来重复定义数据或指令
  7. $为当前nasm编译后当前指令位置,$$为该段的初始位置。一开始在没弄懂$$的情况下照猫画虎胡乱使用,
    导致生成的文件大于512B且0x55AA位置错误(虽然没有影响),后来去掉了section .data后即可(即让后面的数据处于与代码的同一段,以便于计算总体机器码的大小)。
  8. 其它的NASM需要注意的语法待后面的实验探索。

    关于加载至0x7c00:

    BIOS检查了引导设备的第一个扇区(以0x55AA结尾的512B)即主引导记录(MBR),加载至0x7c00,因此默认偏移为0x7c00。
    至于为什么时0x7c00,是由于一开始的8088CPU内存为32KB,即0~0x7FFF,保留512B给MBR,512B保留给MBR产生的数据,
    为了让出足够的空间给操作系统,因此从后开始算,0x7FFF-1024 = 0x7c00;

    关于org的作用及思考:

    根据上一条,我们的代码将会被强制加载到0x7c00,因此若没有org,后面的memory操作数会默认从0开始算。

例如在相对位置3做了如下定义:
msg db ‘@’

  • 在没有org的情况下,[msg]会在0x03读取一位,而真正的数据‘@’被加载到了0x7c03,在读取[msg]的时候就会读到乱码。
  • 在有org 0x7c00的情况下,[msg]会直接去0x7c03读取’@’。

以上可以在生成机器码后,通过反汇编看出。总的来说,org就是告诉汇编器,该代码必然会被加载到某一偏移值上
,因此要告诉汇编器后面的相对地址要加上偏移值。

这也就能够解释一个现象:明明在数据段中初始化了数据,却无法读取,只能在代码开头重新mov一遍才能正常输出,
但是重复mov时的目的地址为0x7c00前面的某个位置,造成别的内存的篡改,产生不可预计的后果。

关于算法的错误:

  • 角落判断:在一开始的设计中,仅分别判断了x和y的位置,若字符运行至角
    落,仅仅将x反弹,而y会继续运动,而且运动方向也会错误,导致一次显示错误的同时也造成了y的越界,
    且会越跑越远,最终让字符完全移出屏幕,如下图可见在后期字符的反弹条件已经错误了

角落错误

修改方法:在判断x越界后的反弹程序中,判断y是否同时越界,若是,则将路径原路返回

  • 在部分不需要写回的相等判断中,用了sub,其实用cmp即可;

不足及优化设想:

在整体信息输出弹跳时,本想着每次弹跳后才变色的,视觉效果会好很多,但是在弹跳时加了判断后(即十二种弹跳),会超出512B限制,导致部分指令无法装载。
有两种优化设想:

  1. 将DRt等数据不局限于一个特征数,而是按规律如上下分别为1、0来进行编写,在弹跳时通过运算来更改路径,合并重复逻辑;
  2. 在MBR中进行引导而非操作,真正的操作放在操作系统的位置执行。

这两种方法都有待进一步的实践。

入门摸索,难免有纰漏,欢迎各位大佬批评指正

参考资料

  1. “Hello world” 引导程序
  2. NASM与MASM语法区别
  3. 主引导记录内存地址是0x7c00
  4. 关于org 0x7c00的原因