上一篇文章学习了:计算机的启动过程(点击链接查看上一篇文章)
这篇文章学习记录为:编写主引导扇区代码。
参考:《X86汇编语言-从实模式到保护模式》-李忠。纯学习笔记,更详细内容请阅读正版书籍。如有侵权请联系我删除文章。
实际上,从这篇文章开始,我们才开始进入到实模式的学习。之前的五篇文章,都是预备的学习知识。点击下面链接复习相关的预备知识:
1、回顾主引导扇区
在前面的学习中,我们知道在计算机重新启动后,如果硬盘是首选的启动设备,那么处理器就会跳转到硬盘的0面0道1扇区去执行代码。这里成为主引导扇区。
主引导扇区的大小为512字节、ROM-BIOS将它加载到处理器的地址空间的逻辑地址0x0000:0x7c00
处,也就是物理地址0x07c00
处,然后判断它是否有效。
而判断一个主引导扇区是否有效的方法是判断它最后的两个字节是否是0x55
和0xAA
。ROM-BIOS首先检测这两个位置是否正确,如果正确,则以一个段间转移指令jmp 0x0000:0x7c00
处执行代码。
一般来说,主引导扇区的代码,负责计算出操作系统所在的硬盘位置,然后将操作系统的自举代码加载到内存,也用一个jmp指令跳转到那里继续执行,直到操作系统完成启动。
我们本篇文章的主要内容就是,编写一段代码,将它写到主引导扇区,让处理器执行。为了更加明显的显示我们的代码是正确的,我们选择在屏幕上显示一行字符串。
2、在屏幕上显示文字
在编写代码之前,我们首先来了解一下如何在屏幕上显示文字。
请注意,这里可不是使用printf
或者cout
或者System.out.println
的地方。我们这里是在没有操作系统的情况下,想要在显示屏上显示文字。
想要显示文字,就要把想要显示的内容写到显存即可。显存是什么?也是一种存储器,只不过专门存储需要在显示器上显示的内容的。其他详细原理自己百度吧,或者看本文的参考书籍,有详细的解释。
如下图所示,是一个字符在屏幕上显示的简单的原理图:
处理器为了直接访问显存,将显存映射到处理器的寻址空间中。如下图:
我们知道8086可以访问1M的内存空间。其中0x00000-0x9FFFF
属于常规内存,由内存条提供。0xF0000-0xFFFFF
由主板上的ROM-BIOS提供。
中间还剩余的320KB的空洞,即0xA0000-0xEFFFF
,这段空间就由外设来提供,其中就包括显卡的显存部分。
由于历史原因,一直以来0xB8000-0xBFFFF
这段物理地址空间,是留给显卡的。
3、分析主引导扇区代码
这段代码是本文参考书籍的代码,先把代码贴上,不算长,如果看不懂,不要被吓跑了。下面的分析,肯定可以让你明白这个程序的意思。
1 ;代码清单5-1
2 ;文件名:c05_mbr.asm
3 ;文件说明:硬盘主引导扇区代码
4 ;创建日期:2011-3-31 21:15
5
6 mov ax,0xb800 ;指向文本模式的显示缓冲区,显存的段地址,
7 mov es,ax ;一般用DS段寄存器,但是DS有其他用处,这里我们使用ES寄存器
8
9 ;以下是显示字符串"Label offset:"
10 mov byte [es:0x00],'L'
11 mov byte [es:0x01],0x07
12 mov byte [es:0x02],'a'
13 mov byte [es:0x03],0x07
14 mov byte [es:0x04],'b'
15 mov byte [es:0x05],0x07
16 mov byte [es:0x06],'e'
17 mov byte [es:0x07],0x07
18 mov byte [es:0x08],'l'
19 mov byte [es:0x09],0x07
20 mov byte [es:0x0a],' '
21 mov byte [es:0x0b],0x07
22 mov byte [es:0x0c],"o"
23 mov byte [es:0x0d],0x07
24 mov byte [es:0x0e],'f'
25 mov byte [es:0x0f],0x07
26 mov byte [es:0x10],'f'
27 mov byte [es:0x11],0x07
28 mov byte [es:0x12],'s'
29 mov byte [es:0x13],0x07
30 mov byte [es:0x14],'e'
31 mov byte [es:0x15],0x07
32 mov byte [es:0x16],'t'
33 mov byte [es:0x17],0x07
34 mov byte [es:0x18],':'
35 mov byte [es:0x19],0x07
36
37 mov ax,number ;取得标号number的偏移地址
38 mov bx,10
39
40 ;设置数据段的基地址,只是在同一个段,偏移地址是不一样的
41 mov cx,cs
42 mov ds,cx
43
44 ;求个位上的数字
45 mov dx,0
46 div bx
47 mov [0x7c00+number+0x00],dl ;保存个位上的数字
48
49 ;求十位上的数字
50 xor dx,dx
51 div bx
52 mov [0x7c00+number+0x01],dl ;保存十位上的数字
53
54 ;求百位上的数字
55 xor dx,dx
56 div bx
57 mov [0x7c00+number+0x02],dl ;保存百位上的数字
58
59 ;求千位上的数字
60 xor dx,dx
61 div bx
62 mov [0x7c00+number+0x03],dl ;保存千位上的数字
63
64 ;求万位上的数字
65 xor dx,dx
66 div bx
67 mov [0x7c00+number+0x04],dl ;保存万位上的数字
68
69 ;以下用十进制显示标号的偏移地址
70 mov al,[0x7c00+number+0x04]
71 add al,0x30
72 mov [es:0x1a],al ;将al寄存器中的ASCII数字传送到显示缓冲区
73 mov byte [es:0x1b],0x04 ;下一字节存放显示属性,0x04代表:黑底红字,无闪烁,无加亮
74
75 mov al,[0x7c00+number+0x03]
76 add al,0x30
77 mov [es:0x1c],al
78 mov byte [es:0x1d],0x04
79
80 mov al,[0x7c00+number+0x02]
81 add al,0x30
82 mov [es:0x1e],al
83 mov byte [es:0x1f],0x04
84
85 mov al,[0x7c00+number+0x01]
86 add al,0x30
87 mov [es:0x20],al
88 mov byte [es:0x21],0x04
89
90 mov al,[0x7c00+number+0x00]
91 add al,0x30
92 mov [es:0x22],al
93 mov byte [es:0x23],0x04
94
95 mov byte [es:0x24],'D'
96 mov byte [es:0x25],0x07
97
98 infi: jmp near infi ;无限循环,防止处理器再接着取下面的数据,数据当成指令取执行会导致错误或运行不正常
99
100 number db 0,0,0,0,0
101
102 times 203 db 0
103 db 0x55,0xaa
- 首先我们是要在屏幕上显示字符串,所以需要将需要显示的字符串的字符传送到显存中
- 6行7行代码:由第2节的内容知显存位于处理器寻址空间的0xB8000处。所以我们需要设置显存的段地址为:0xb800 ,这里我们使用ES寄存器来表示显存段地址(当然也可以使用DS,但是DS还有其他用处,所以我们就使用ES寄存器)。
- 10行-35行:显示字符串"Label offset:"
那么为什么每将一个字符传送到显存后,后面要继续传动一个0x07
呢?实际上是这样的:
显存中,每一个字符的ASCII码后面跟的是该字符的显示属性。包括字符的颜色和背景色。如下图:
在8086下,80x25文本模式下的颜色表如下:
由以上可知,我们显示的字符属性是0x07,黑底白字,无闪烁,无加亮。也就是黑底白字。
10行-35行依次将字符写入到缓存中,后面依次写入字符的属性。这很好理解!!!
- 37行:取得标号number的汇编地址。本代码不光想在屏幕上显示字符串
Label offset:
,还想将number的汇编地址显示出来。number是一个标号,标号是它所在的地方的汇编地址。什么是汇编地址?
实际上一个程序经过编译后,编译器会给每一条代码一个汇编地址,这个汇编地址实际上是从0开始。
在分段机制中,偏移地址也是从0开始。实际上,这个汇编地址就是与偏移地址是对应的。如下图:
理解了什么是汇编地址与偏移地址的关系后(不理解的看原书第五章),我们就来将number处的汇编地址在屏幕上显示出来。
number就代表那个地址的值。我这里已经提前知道这个地址是:0x012E
也就是十进制302
。
由之前的学习内容知道直接将302传送到显存的话,是不可能在屏幕上显示302的。我们只能将302进行拆分,将每一个数位都拆解出来,一个一个传送给显存。如何拆解?每次除以10…太简单了就不写了。
- 38行:将bx寄存器赋值为10 ,作为下面除以10 的时候的除数。
- 100行:这里为什么突然到100行了?不急,慢慢来,你分析程序也是跳来跳去的分析吧。
我们既然想将number的汇编地址分解为一个个数位,就得找一个地方,将分解后的数字先咱是存起来。你可以想到用寄存器先存起来,但是寄存器,毕竟就那么8个通用的寄存器,而且本段代码也用了好几个了,所以这里无法使用寄存器来暂时存我们的数据。
一个办法就是在内存找到一个地方,来存储。这里,我们的主引导扇区是512字节,我们写的代码很少不到300字节,所以我们选择在主引导扇区的最后先开辟一个空间用于存储number的分解后的数字。
那么第100行,就定义了五字节的数据,赋值为0。当然你也可以赋值为其他值,反正后面是呀被覆盖的。
-
41-42行:我们将DS寄存器指向代码段,就是让数据段寄存器DS与代码段寄存器CS保持一致。因为我们这里将数据与代码都放到一个段里面了,所以数据段与代码段是一个段(正常不能不放到一个段,我们初学,先这么写,后面会分段) 其实用CS来访问数据也可以,但是我们还是习惯用DS来访问数据,所以这里就有这么两句赋值代码。
-
44-67行:求numberi的各个数位的数字,然后存到我们预先开辟好的空间中。
-
70-93行:先将各个数位转化成十进制显示,然后送入到显存,在每一个字符后面写入显示属性
0x04
,代表黑底红字。 -
95-96行:显示字符
D
以代表我们前面显示的number地址是10进制显示的。黑底白字。 -
98行:无限循环,防止处理器再接着取下面的数据,数据当成指令取与执行会导致错误或运行不正常
-
102行:由于主引导扇区是512字节的,我们写的程序并没有达到512字节。所以我们应该将主引导扇区未满的地方填满。我们这里采取了一些特殊手段得知有203字节未填满,搜易我们了连续声明203个字节用于存储0. 至于使用了什么特殊手段,不必要知道,因为后面的学习中会学习使用正常的的手段来得知这个未填满的字节有多少。
-
103行:一个有效的主引导扇区,它的最后必须是
0x55和0xaa
。
4、编译主引导扇区代码并加载运行
在上一篇文章,我们已经安装了VirtualBox 虚拟机软件,并在里面创建了一台名为LEARNASM的虚拟计算机。除此之外,还为它创建了一块虚拟硬盘。
然后我们参考书上4.2.4节的内容,将我们汇编代码编译好的二进制bin文件写到虚拟硬盘的主引导扇区中。启动虚拟机,就会运行我们写的代码,运行结果如下:
今天的程序运行的很顺利。
5、总结
了解汇编的运行机制,对以后深入学习高级语言,很有帮助:比如JVM。
笔记记得不是很全,像汇编的语法以及如何将代码写到虚拟硬盘的主引导扇区这些都没有写。如果又不懂的可以加我联系方式一起交流。
学习探讨加个人:
qq:1126137994
微信:liu1126137994