Write the main boot sector code
📄 返回目录页
本次代码:
;代码清单5-1
;文件名:c05_mbr.asm
;文件说明:硬盘主引导扇区代码
;创建日期:2011-3-31 21:15
mov ax,0xb800 ;指向文本模式的显示缓冲区
mov es,ax
;以下显示字符串"Label offset:"
mov byte [es:0x00],'L'
mov byte [es:0x01],0x07
mov byte [es:0x02],'a'
mov byte [es:0x03],0x07
mov byte [es:0x04],'b'
mov byte [es:0x05],0x07
mov byte [es:0x06],'e'
mov byte [es:0x07],0x07
mov byte [es:0x08],'l'
mov byte [es:0x09],0x07
mov byte [es:0x0a],' '
mov byte [es:0x0b],0x07
mov byte [es:0x0c],"o"
mov byte [es:0x0d],0x07
mov byte [es:0x0e],'f'
mov byte [es:0x0f],0x07
mov byte [es:0x10],'f'
mov byte [es:0x11],0x07
mov byte [es:0x12],'s'
mov byte [es:0x13],0x07
mov byte [es:0x14],'e'
mov byte [es:0x15],0x07
mov byte [es:0x16],'t'
mov byte [es:0x17],0x07
mov byte [es:0x18],':'
mov byte [es:0x19],0x07
mov ax,number ;取得标号number的偏移地址
mov bx,10
;设置数据段的基地址
mov cx,cs
mov ds,cx
;求个位上的数字
mov dx,0
div bx
mov [0x7c00+number+0x00],dl ;保存个位上的数字
;求十位上的数字
xor dx,dx
div bx
mov [0x7c00+number+0x01],dl ;保存十位上的数字
;求百位上的数字
xor dx,dx
div bx
mov [0x7c00+number+0x02],dl ;保存百位上的数字
;求千位上的数字
xor dx,dx
div bx
mov [0x7c00+number+0x03],dl ;保存千位上的数字
;求万位上的数字
xor dx,dx
div bx
mov [0x7c00+number+0x04],dl ;保存万位上的数字
;以下用十进制显示标号的偏移地址
mov al,[0x7c00+number+0x04]
add al,0x30
mov [es:0x1a],al
mov byte [es:0x1b],0x04
mov al,[0x7c00+number+0x03]
add al,0x30
mov [es:0x1c],al
mov byte [es:0x1d],0x04
mov al,[0x7c00+number+0x02]
add al,0x30
mov [es:0x1e],al
mov byte [es:0x1f],0x04
mov al,[0x7c00+number+0x01]
add al,0x30
mov [es:0x20],al
mov byte [es:0x21],0x04
mov al,[0x7c00+number+0x00]
add al,0x30
mov [es:0x22],al
mov byte [es:0x23],0x04
mov byte [es:0x24],'D'
mov byte [es:0x25],0x07
infi: jmp near infi ;无限循环
number db 0,0,0,0,0
times 203 db 0
db 0x55,0xaa
主引导扇区(Main Boot Sector,MBR)。硬盘的0面0头1扇区
一个有效的主引导扇区,其最后2字节应当是0x55和0xAA。ROM-BIOS程序首先检测这两个标志,如果主引导扇区有效,则以一个段间转移指令jmp 0x0000:0x7c00跳到那里继续执行。
一般来说,主引导扇区是由操作系统负责的。正常情况下,一段精心编写的主引导扇区代码将检测用来启动计算机的操作系统,并计算出它所在的硬盘位置。然后,它把操作系统的自举代码加载到内存,也用jmp指令跳转到那里继续执行,直到操作系统完全启动。
计算机是怎么在屏幕上显示文字的?
显卡和显存
显示需要显示器和显卡,显卡给显示器提供内容,同时控制显示器的显示模式和状态,
显示器则将内容显示在屏幕上。
显卡控制显示器的最小单位是像素,一个像素对应着屏幕上的一个点。显卡都有自己的存储器,因为它位于显卡上,故称显示存储器(Video RAM, VRAM),简称显存
显示器显示黑白图像只需要控制每个像素是亮,还是不亮。不亮当成比特“0”,亮看成比特“1”。

显存的第1字节对应着屏幕左上角连续的8个像素;第2字节对应着屏幕上后续的8个像素,后面的依次类推。
显卡的工作是周期性地从显存中提取这些比特,并把它们按顺序显示在屏幕上。
黑白两个颜色只需要1比特,而24比特可以对应16777216种颜色。
上述显示器的是图形模式;
显示文字的当然是文本模式,对于显示器来说图像和文字都是二进制数
显存可以存放字符的代码,第1个代码对应着屏幕左上角第1个字符,剩下的工作是如何用代码来控制屏幕上的像素,使它们或明或暗以构成字符的轮廓,这是字符发生器和控制电路的事情。

为了方便访问显存,显存被映射在了内存空间里,8086可访问的内存空间中除了内存和ROM-BIOS,还有320KB,即0xA0000~0xEFFFF,这段地址空间由特定的外围设备来提供,其中就包括显卡。
文本模式下显存到内存的映射:

一直以来,0xB8000~0xBFFFF这段物理地址空间,是留给显卡的,由显卡来提供,用来显示文本。
初始化段寄存器
文本模式显存是从0xB8000开始的,操作这一段物理地址时,段地址为0xB800,所以要将DS Segment-register的值改为0xB800,但也不一定要用DS也可以用ES(附加段寄存器)。
更改段寄存器的值:
mov ax,0xB800
mov es,ax
INTEL处理器不允许将一个立即数传送到段寄存器,它只允许这样的指令:
mov 段寄存器,通用寄存器
mov 段寄存器,内存单元
显存的访问和ASCII代码
给段寄存器指向0xB800的值后,现在的逻辑地址可以正常地对应屏幕左上角的字符。
字符也是一个二进制代码,和正常计算的数字没有区别,只是看不同的硬件有不同的解释。所以1967年,美国国家标准学会制定了美国信息交换标准代码(American Standard Code for InformationInterchange, ASCII)

ASCII表中有相当一部分代码是不可打印和显示的,它们用于控制通信过程。比如,LF是换行;CR是回车;DEL和BS分别是删除和退格,在我们平时用的键盘上也是有的;BEL是振铃(使远方的终端响铃,以引起注意);SOH是文头;EOT是文尾;ACK是确认。
屏幕上的每个字符对应着显存中连续2字节,前一个是字符的ASCII代码,后面是字符的显示属性,包括字符颜色(前景色)和底色(背景色)。
字符代码及字符属性:

字符的显示属性(1字节)分为两部分,低4位定义的是前景色,高4位定义的是背景色。
色彩主要由R、G、B这3位决定。K是闪烁位,为0时不闪烁,为1时闪烁;I是亮度位,为0时正常亮度,为1时呈高亮。
80×25文本模式下的颜色表:

在屏幕上显示文字
显示字符
从源程序的第10行开始,到第35行,目的是显示一串字符“Label offset:”
在第10行中:
mov byte [es:0x00],'L'
’L‘的ASCII码是0x4C,也就可以这样说:
mov byte [es:0x00],0x4C
关键字“byte”用来修饰目的操作数,指出本次传送是以字节的方式进行的。
[es:0x00] 是内存地址,0x00是偏移地址。如果是不指定寄存器es来访问内存会默认用段寄存器ds,也就是这样写[0x00]。
在16位的处理器上,一次可以操作的数据宽度是8位,也可以是16位,所以这里0x4C和0x00
可以是0x4C,0x00,也可以是0x004C,0x0000。我们这里用了“byte”修饰符,所以是按照字节的方式进行的,如果不使用编译器不知道你的意图只可以报错。操作字单元可以用“word”进行修饰。
下面的指令就不需要任何修饰:
mov [0x00],al ; 按字节操作
mov ax,[0x02] ; 按字操作
显示标号的汇编地址
标号
在源程序的编译阶段,编译器会把代码整体上作为一个独立的段来处理,并从0开始计算和跟踪每条指令的地址。因为该地址是在编译期间计算的,故称为汇编地址。汇编地址是在源程序编译期间,编译器为每条指令确定的汇编位置(Assembly Position),指示该指令相对于程序或者段起始处的距离,以字节计。当编译后的程序装入物理内存后,它又是该指令在内存段内的偏移地址。
汇编程序经过编译后除了.bin文件,还会有一个.lst的列表文件
本文上面的代码编译后列表文件的内容:
1 ;代码清单5-1
2 ;文件名:c05_mbr.asm
3 ;文件说明:硬盘主引导扇区代码
4 ;创建日期:2011-3-31 21:15
5
6 00000000 B800B8 mov ax,0xb800 ;指向文本模式的显示缓冲区
7 00000003 8EC0 mov es,ax
8
9 ;以下显示字符串"Label offset:"
10 00000005 26C60600004C mov byte [es:0x00],'L'
11 0000000B 26C606010007 mov byte [es:0x01],0x07
12 00000011 26C606020061 mov byte [es:0x02],'a'
13 00000017 26C606030007 mov byte [es:0x03],0x07
14 0000001D 26C606040062 mov byte [es:0x04],'b'
15 00000023 26C606050007 mov byte [es:0x05],0x07
16 00000029 26C606060065 mov byte [es:0x06],'e'
17 0000002F 26C606070007 mov byte [es:0x07],0x07
18 00000035 26C60608006C mov byte [es:0x08],'l'
19 0000003B 26C606090007 mov byte [es:0x09],0x07
20 00000041 26C6060A0020 mov byte [es:0x0a],' '
21 00000047 26C6060B0007 mov byte [es:0x0b],0x07
22 0000004D 26C6060C006F mov byte [es:0x0c],"o"
23 00000053 26C6060D0007 mov byte [es:0x0d],0x07
24 00000059 26C6060E0066 mov byte [es:0x0e],'f'
25 0000005F 26C6060F0007 mov byte [es:0x0f],0x07
26 00000065 26C606100066 mov byte [es:0x10],'f'
27 0000006B 26C606110007 mov byte [es:0x11],0x07
28 00000071 26C606120073 mov byte [es:0x12],'s'
29 00000077 26C606130007 mov byte [es:0x13],0x07
30 0000007D 26C606140065 mov byte [es:0x14],'e'
31 00000083 26C606150007 mov byte [es:0x15],0x07
32 00000089 26C606160074 mov byte [es:0x16],'t'
33 0000008F 26C606170007 mov byte [es:0x17],0x07
34 00000095 26C60618003A mov byte [es:0x18],':'
35 0000009B 26C606190007 mov byte [es:0x19],0x07
36
37 000000A1 B8[2E01] mov ax,number ;取得标号number的偏移地址
38 000000A4 BB0A00 mov bx,10
39
40 ;设置数据段的基地址
41 000000A7 8CC9 mov cx,cs
42 000000A9 8ED9 mov ds,cx
43
44 ;求个位上的数字
45 000000AB BA0000 mov dx,0
46 000000AE F7F3 div bx
47 000000B0 8816[2E7D] mov [0x7c00+number+0x00],dl ;保存个位上的数字
48
49 ;求十位上的数字
50 000000B4 31D2 xor dx,dx
51 000000B6 F7F3 div bx
52 000000B8 8816[2F7D] mov [0x7c00+number+0x01],dl ;保存十位上的数字
53
54 ;求百位上的数字
55 000000BC 31D2 xor dx,dx
56 000000BE F7F3 div bx
57 000000C0 8816[307D] mov [0x7c00+number+0x02],dl ;保存百位上的数字
58
59 ;求千位上的数字
60 000000C4 31D2 xor dx,dx
61 000000C6 F7F3 div bx
62 000000C8 8816[317D] mov [0x7c00+number+0x03],dl ;保存千位上的数字
63
64 ;求万位上的数字
65 000000CC 31D2 xor dx,dx
66 000000CE F7F3 div bx
67 000000D0 8816[327D] mov [0x7c00+number+0x04],dl ;保存万位上的数字
68
69 ;以下用十进制显示标号的偏移地址
70 000000D4 A0[327D] mov al,[0x7c00+number+0x04]
71 000000D7 0430 add al,0x30
72 000000D9 26A21A00 mov [es:0x1a],al
73 000000DD 26C6061B0004 mov byte [es:0x1b],0x04
74
75 000000E3 A0[317D] mov al,[0x7c00+number+0x03]
76 000000E6 0430 add al,0x30
77 000000E8 26A21C00 mov [es:0x1c],al
78 000000EC 26C6061D0004 mov byte [es:0x1d],0x04
79
80 000000F2 A0[307D] mov al,[0x7c00+number+0x02]
81 000000F5 0430 add al,0x30
82 000000F7 26A21E00 mov [es:0x1e],al
83 000000FB 26C6061F0004 mov byte [es:0x1f],0x04
84
85 00000101 A0[2F7D] mov al,[0x7c00+number+0x01]
86 00000104 0430 add al,0x30
87 00000106 26A22000 mov [es:0x20],al
88 0000010A 26C606210004 mov byte [es:0x21],0x04
89
90 00000110 A0[2E7D] mov al,[0x7c00+number+0x00]
91 00000113 0430 add al,0x30
92 00000115 26A22200 mov [es:0x22],al
93 00000119 26C606230004 mov byte [es:0x23],0x04
94
95 0000011F 26C606240044 mov byte [es:0x24],'D'
96 00000125 26C606250007 mov byte [es:0x25],0x07
97
98 0000012B E9FDFF infi: jmp near infi ;无限循环
99
100 0000012E 0000000000 number db 0,0,0,0,0
101
102 00000133 00<rep CBh> times 203 db 0
103 000001FE 55AA db 0x55,0xaa
第一条指令mov ax,0xb800的汇编地址是0x00000000,对应的机器代码为B8 00 B8;
在代码的第98行中“infi” 是一个标号,也就是说它代表着汇编地址0000012B。
那个冒号是无所谓的。
在NASM汇编语言里,每条指令的前面都可以拥有一个标号,以代表和指示该指令的汇编地址。
如何显示十进制数字
在程序的第37行中,number是一个标号,是100行的0000012E。也就是这条代码相当于:
mov ax,0x012E
其中字操作数是10进制的302,当然直接把ax的内容给到显示缓冲区是无法显示字符“302”的。
字符“0”的ASCII代码是0x30,字符“1”的ASCII代码是0x31,字符“9”的ASCII代码是0x39。这就是说,把每次相除得到的余数加上0x30,在屏幕上显示就没问题了。
所以把302这个数拆开来一个一个加上0x30,再显示在屏幕上就好了。
在程序中声明并初始化数据
可以用处理器提供的除法指令来分解一个数的各个数位,但是每次除法操作后得到的数位需要临时保存起来以备后用。最好的办法是在内存中专门留出一些空间来保存这些数位。
在第100行的number中代表了后面的代码:
number db 0,0,0,0,0
;用于声明并初始化这些数据,而标号number则代表了这些数据的起始汇编地址。
;就是这些初始化的数是从0000012E开始的。
要放在程序中的数据是用DB指令来声明(Declare)的,DB的意思是声明字节(Declare Byte),所以,跟在它后面的操作数都占一字节的长度(位置)。
如果要声明超过一个以上的数据,各个操作数之间必须以逗号隔开。
声明的数据可以是任何值,只要不超过伪指令所指示的大小。
除此之外,DW(Declare Word)用于声明字数据,DD(Declare DoubleWord)用于声明双字(两个字)数据,DQ(Declare Quad Word)用于声明四字数据。DB、DW、DD和DQ并不是处理器指令,它只是编译器提供的汇编指令,所以称作伪指令(Pseudo Instruction)。
伪指令是汇编指令的一种,它没有对应的机器指令,所以它不是机器指令的助记符,仅仅在编译阶段由编译器执行,编译成功后,伪指令就消失了。所以在程序执行时,伪指令是得不到处理器光顾的。实际上,程序执行时,伪指令已不存在。
像这样一段代码:
00000000 diva dw 0x08f9
00000002 divb db 0x08
00000003 mov ax, [diva]
00000006 div byte [divb]
在编译阶段,编译器在生成这两条指令的机器码之前,会先将它们转换成以下的形式:
mov ax, [0x0000]
div byte [0x0002]
处理器可以访问任何内存位置,但不一定每个位置都是空闲的。伪指令DB用来保留只供自己访问的内存位置。
分解数的各个数位
要分解一个数的各个数位,需要做除法。8086处理器提供了除法指令div,它可以做两种类型的除法。
#第一种类型是用16位的二进制数除以8位的二进制数。
比如:
div cl
div byte [0x0023]
被除数是放在ax寄存器里的,这是必要的。指令执行后,商在寄存器al中,余数在寄存器ah中。
第一条指令是将ax与cl的内容相除,而除数放在cl中。
第二条指令是去逻辑地址 [ds:0x0023]形成的物理地址中取出一个字节的内容当除数。
我们通常是不知道数据的具体位置的,所以关于地址的部分大部分多是以标号的形式表示的。
#第二种类型是用32位的二进制数除以16位的二进制数。
当然16位的处理器是不可能直接提供32位的数的,所以要将32位的数拆开来,把高16位放在dx寄存器,把低16位放在ax寄存器里。
除数还是和第一种一样,两个数相除后商在ax中,余数在dx中。
相除之后,我们要从寄存器里取出余数,将余数保存在数据段。我们在100行中初始化了5个字节数据,这里的余数就传送到那里。
就像代码的第47行:
mov [0x7c00+number+0x00],dl ;保存个位上的数字
这里将dl的内容传送到了内存地址[0x7c00+number+0x00]中,也就是[0x7D2E]。
我们知道程序是从一个段开始运行的,所以number标号是相对于程序开始处的汇编地址,
这个number在[]中用也是一个普通的值。而number代表的值与cs的内容相加应该我们的目标地址。但这里的代码比较特殊,这是主引导程序,是从0x0000:0x7c00开始的,代码段和数据段都从0x00000开始,而程序从0x7c000开始。所以,number要与0x7c00相加才可以得到我们存放数据的起始位置。0x00就很简单了,第一个数据。

xor指令在数字逻辑里是异或(eXclusive OR)的意思,或者叫互斥或、互斥的或运算。
在数字逻辑里,如果0代表假,1代表真 所以:
0 xor 0 = 0
0 xor 1 = 1
1 xor 0 = 1
1 xor 1 = 0
xor指令的目的操作数可以是通用寄存器和内存单元,源操作数可以是通用寄存器、内存单元和立即数(不允许两个操作数同时为内存单元)。
一般地,xor指令的两个操作数应当具有相同的数据宽度。
总之我们知道了两个操作数相同为假
执行xor dx dx后会给dx寄存器清零。
清零后,我们就可以计算后面的数位了,ax的值也就又成为了被除数,因为是32为除16位,ax和dx是联合的,所以才有了前面的操作。
无限循环
一直到程序的96行这个程序的任务已经结束了,处理器会继续向下取指令执行,执行到非指令的区域就不好了。鉴于我们任务结束了,也没活了,同时避免问题,在98行整了个无限循环:
infi: jmp near infi
jmp是转移指令,用于使处理器脱离当前的执行序列,转移到指定的地方执行,关键字near表示目标位置依然在当前代码段内。
这一段是jmp的一种用法叫做相对近转移。jmp后面的是标号计算这种;
在编译阶段,编译器会将标号处的汇编地址减去下一条指令的地址(也就是当前ip寄存器的值),得到操作数。而这个操作数再和ip寄存器的值相加,以near取值的低16位,就得到了目标指令的汇编地址,也就是当前的位置。
完成并编译主引导扇区代码
运行主引导扇区的代码系统启动,但主引导扇区里的程序是错误的或者什么都没有,无效的,其结果是陷入宕机状态。所以一个有效的主引导扇区,其最后2字节的数据必须是0x55和0xAA。否则,这个扇区里保存的就不是一些有意而为的数据,系统就会尝试以光盘和U盘启动。
定义这两个字节就用db伪指令就可以做到:
db 0x55,0xaa
但是这两个字节要在512字节中的最后,而我们无法确定我们这里代码的长度,现在的指令在内存的哪里也不知道。
#我们当然有非常好的办法解决,但还不宜在这里说明。
我们知道,在前面的内容和结尾的0xAA55之间,有203字节的空洞。因此,源程序的第102行,用于声明203个为0的数值来填补。
所以用了times伪指令,可用于重复它后面的指令若干次。
102 00000133 00<rep CBh> times 203 db 0
db 0重复了203次。
然后再将编译好的裸二进制文件写入到虚拟硬盘里,在虚拟机执行。
最后的结果:
