x86汇编笔记-第 5章 编写主引导扇区代码

5.1 欢迎来到主引导扇区

  • 处理器加电或者复位之后,如果硬盘是首选的启动设备,那么,ROM-BIOS 将试图读取硬盘的 0 面 0 道 1 扇区。传统上,这就是主引导扇区Main Boot Sector,MBR)。
  • 读取的主引导扇区数据有 512 字节,ROM-BIOS 程序将它加载到逻辑地址 0x0000:0x7c00处,也就是物理地址 0x07c00 处,然后判断它是否有效。
  • 一个有效的主引导扇区,其最后两字节应当是 0x55 和 0xAA。ROM-BIOS 程序首先检测这两个标志,如果主引导扇区有效,则以一个段间转移指令 jmp 0x0000:0x7c00 跳到那里继续执行。

5.2 注释

  • 注释必须以分号“;”开始。
  • 在源程序编译阶段,编译器将忽略所有注释。因此,在编译之后,这些和生成机器代码无关的内容都统统消失了。

5.3 在屏幕上显示文字

5.3.1 显卡和显存

  • 为了显示文字,通常需要两种硬件,一是显示器,二是显卡。
    1. 显卡的职责是为显示器提供内容,并控制显示器的显示模式和状态;
    2. 显示器的职责是将那些内容以视觉可见的方式呈现在屏幕上。

  • 显卡未必一定是独立的插卡。为了节省使用者的成本,有的显卡直接做在主板上,这样的显卡也有个名字,叫 集成显卡

  • 显卡都有自己的存储器,因为它位于显卡上,故称显示存储器(Video RAM:VRAM),简称 显存,要显示的内容都预先写入显存。和其他半导体存储器一样,显存并没有什么特殊的地方,也是一个按字节访问的存储器件。

最简单的显示界面——黑白界面

  • 对于显示器来说,显示黑白界面最为简单,只需控制屏幕每个像素点亮或者不亮,如果把不亮当成比特“0”,亮看成比特“1”,那就好办了;
  • 比如如图 5-1 所示,显存的第 1 个字节对应着屏幕左上角连续的 8 个像素;第 2 个字节对应着屏幕上后续的 8 个像素,后面的依次类推。图5-1

黑白界面上显示出字符的形状

  • 难点: 操作显存里的比特,使得屏幕上能显示出字符的形状,是非常麻烦、非常繁重的工作,因为你必须计算该字符所对应的比特位于显存里的什么位置。
  • 解决办法: 像一个二进制数既可以是一个普通的数,也可以代表一条处理器指令一样。将每个字符也表示成一个数。
  • 比如,数字 0x4C 就代表字符“L”,这个数被称为是字符“L”的 ASCII 代码

    ASCII((American Standard Code for Information Interchange): 美国信息交换标准代码)切忌写成 ascll(ASCLL)

  • 如图 5-2 所示,可以将字符的代码存放到显存里,第 1 个代码对应着屏幕左上角第 1 个字符,第 2 个代码对应着屏幕左上角第 2 个字符,后面的依次类推。图5-2

文本模式

  • 用代码来控制屏幕上的像素,使它们或明或暗以构成字符的轮廓,这是字符发生器和控制电路的事情。

  • 传统上,像上述这种专门用于显示字符的工作方式称为文本模式。文本模式和图形模式是显卡的两种基本工作模式,可以用指令访问显卡,设置它的显示模式。在不同的工作模式下,显卡对显存内容的解释是不同的。

  • 问题: 为了给出要显示的字符,处理器需要访问外设显卡的显存,将字符的ASCII码写入进去。这无疑是多了一道程序,花费了更多的时间。

  • 解决办法: 为了解决这个问题,计算机系统的设计者们决定把显存映射到处理器可以直接访问的地址空间里,也就是内存空间里

  • 如图 5-3 所示,我们知道,8086 可以访问 1MB 内存。其中,0x00000~9FFFF 属于常规内存,由内存条提供;0xF0000~0xFFFFF 由主板上的一个芯片提供,即 ROM-BIOS。图5-3

  • 这样一来,中间还有一个 320KB 的空洞,即 0xA0000~0xEFFFF。传统上,这段地址空间由特定的外围设备来提供,其中就包括显卡。这样就解决了处理访问显存的麻烦。

  • 一直以来,0xB8000~0xBFFFF 这段物理地址空间,是留给显卡的,由显卡来提供,用来显示文本。除非显卡出了毛病,否则这段空间总是可以访问的。

5.3.2 初始化段寄存器

  • 和访问主内存一样,为了访问显存,也需要使用逻辑地址,也就是采用“段地址:偏移地址”的形式,这是处理器的要求。

  • 考虑到文本模式下显存的起始物理地址是 0xB8000,这块内存可以看成是段地址为 0xB800,偏移地址从 0x0000 延伸到 0xFFFF 的区域,因此我们可以把段地址定为0xB800

  • 访问内存可以使用段寄存器 DS,但这不是强制性的,也可以使用 ES。因为 DS 还有别的用处,所以在这里我们使用 ES 来指向显存所在的段。

  • 这里附上8086通用寄存器的图,以免混淆;
    图2-7

  • 源程序第 6、7 行,首先把立即数 0xB800 传送到 AX,然后再把 AX 的值传送到 ES。这样,附加段寄存器 ES 就指向 0xb800 段(段基地址为 0xB800)。

  • Intel 的处理器不允许将一个立即数传送到段寄存器,它只允许这样的指令:

mov 段寄存器,通用寄存器 
mov 段寄存器,内存单元

5.3.3 显存的访问和 ASCII 代码

  • 一旦将显存映射到处理器的地址空间,那我们就可以使用普通的传送指令(mov)来读写它,这无疑是非常方便的,但需要首先将它作为一个段来看待,并将它的基地址传送到段寄存器。
  • 我们把 0xB800 作为段地址传送到附加段寄存器 ES,以后就用ES 来读写显存。这样,段内偏移为 0 的位置就对应着屏幕左上角的字符。
  • 在计算机中,每个用来显示在屏幕上的字符,都有一个二进制代码。这些代码和普通的二进制数字没有什么不同,唯一的区别在于,发送这些数字的硬件和接收这些数字的硬件把它们解释为字符,而不是指令或者用于计算的数字。

ASCII

  • 1967 年,美国国家标准学会制定了美国信息交换标准代码(American Standard Code for Information Interchange,ASCII),如表 5-1 所示。表5-1
  • 值得注意的是,ASCII 是 7 位代码,只用了一个字节中的低 7 比特,最高位通常置 0。这意味着,ASCII 只包含 128 个字符的编码。
  • 所以,在表中,水平方向给出了代码的高 3 比特,而垂直方向给出了代码的低 4 比特。

字符显示属性

  • 屏幕上的每个字符对应着显存中的两个连续字节,前一个是字符的 ASCII 代码,后面是字符的显示属性,包括字符颜(前景)和底(背景)。
  • 如图 5-4 所示,字符的显示属性(1 字节)分为两部分,低 4 位定义的是前景色,高 4 位定义的是背景色。
    图5-4
  • 色彩主要由红(R)、绿(G)、蓝 (B)决定,这三原色可以配出其他所有颜色。
  • K 是闪烁位,为 0 时不闪烁,为 1 时闪烁;
  • I 是亮度位,为 0 时正常亮度,为 1 时呈高亮;
  • 表 5-2 给出了背景色和前景色的所有可能值。表5-2
  • 字符属性 0x07 可以解释为黑底白字,无闪烁,无加亮。
  • 有时屏幕上全是黑的,即屏幕上显示的全是黑底白字的空白字符,也叫空格字符(Space),ASCII代码是 0x20。因为它是空白,自然就无法在黑底上看到任何痕迹了。

5.3.4 显示字符

  • 为了方便,多数汇编语言编译器允许在指令中直接使用字符的字面值来代替数值形式的 ASCII码,比如:
	mov byte [es:0x00],'L'

这等效于

	mov byte [es:0x00],0x4c
  • 尽管通过查表可以知道字符“L”的 ASCII 代码是 0x4C,但毕竟费事。不过,要在指令中使用字符的字面值,这个字符必须用引号围起来,就像上面一样。在源程序的编译阶段,汇编语言编译器会将它转换成 ASCII 码的形式。

  • 一般情况下,如果没有附加任何指示,段地址默认在段寄存器 DS 中。比如:

	mov byte [0x00],’L’
  • 但是实际上,显存的段地址位于段寄存器 ES 中,我们希望使用 ES 来访问内存。因此,这里使用了段超越前缀“es:”
  • 关键字“byte”用来修饰目的操作数,指出本次传送是以字节的方式进行的。

5.4 显示标号的汇编地址

5.4.1 标号

  • 在编译阶段,每条指令都被计算并赋予了一个汇编地址,就像它们已经被加载到内存中的某个段里一样。实际上,如图 5-5 所示,当编译好的程序加载到物理内存后,它在段内的偏移地址和它在编译阶段的汇编地址是相等的。图5-5
  • 编译后的程序是整体加载到内存中某个段的,交叉箭头用于指示它们之间的映射关系。之所以箭头是交叉的,是因为源程序的编译是从上往下的,而内存地址的增长是从下往上的(从低地址往高地址方向增长)。

号标的应用

  • 在 NASM 汇编语言里,每条指令的前面都可以拥有一个标号,以代表和指示该指令的汇编地址。
  • 标号可以由字母、数字、“”、“$”、“#”、“@”、“~”、“.”、“?”组成,但必须以字母、“.”、“”和“?”中的任意一个打头。
  • 标号并不是必需的,只有在我们需要引用某条指令的汇编地址时,才使用标号。
  • 比如可以使用号标和jmp来实现循环
		 infi: 
				jmp near infi

5.4.3 在程序中声明并初始化数据

  • 要放在程序中的数据是用 DB 指令来声明(Declare)的,DB 的意思是声明字节(Declare Byte),所以,跟在它后面的操作数都占一个字节的长度(位置)。
  • 注意,如果要声明超过一个以上的数据,各个操作数之间必须以逗号隔开。
  • 除此之外,DW(Declare Word)用于声明字数据,DD(Declare Double Word)用于声明双字(两个字)数据,DQ(Declare Quad Word)用于声明四字数据。
  • DB、DW、DD 和 DQ 并不是处理器指令,它只是编译器提供的汇编指令,所以称做伪指令(pseudo Instruction)。
  • 伪指令是汇编指令的一种,它没有对应的机器指令,所以它不是机器指令的助记符,仅仅在编译阶段由编译器执行,编译成功后,伪指令就消失了,所以在程序执行时,伪指令是得不到处理器光顾的,实际上,程序执行时,伪指令已不存在。
		dividnd dw 0x3f0
		divisor db 0x3f
		
		……
		
		mov ax,[dividnd]
		div byte [divisor]
  • 首先,声明了标号 dividnd 并初始化了一个字 0x3f0 作为被除数;
  • 然后,又声明了标号 divisor 并初始化一个字节 0x3f 作为除数。
  • 在后面的 mov 和 div 指中,是用标号 dividnd 和 divisor 来代替被除数和除数的汇编地址
  • 在编译阶段,编译器用具体的数值取代括号中的标号 dividnd 和 divisor。

现在,假设 dividnd(被除数) 和 divisor (除数)所代表的汇编地址分别是 0xf000 和 0xf002,那么,在编译阶段,编译器在生成这两条指令的机器码之前,会先将它们转换成以下的形式:

		mov ax,[0xf000]
		div byte [0xf002]

完成并编译主引导扇区代码

5.6.1 主引导扇区有效标志

  • 主引导扇区在系统启动过程中扮演着承上启下的角色,但并非是唯一的选择。如果硬盘的主引导扇区不可用,系统还有其他选择,比如可以从光盘和 U 盘启动。
  • ,计算机的设计者们决定,一个有效的主引导扇区,其最后两个字节的数据必须是 0x55 和 0xAA。否则,这个扇区里保存的就不是一些有意而为的数据。
  • 定义这两个字节很简单,伪指令 db 和 dw 就可以实现。源程序第 103 行就是 db 版本的实现,但没有标号。
  • 标号的作用是提供当前位置的汇编(偏移)地址供其他指令引用,如果没有任何指令引用这个地址,标号可以省略。这是两个单独的字节,所以 0x55 在前,0xAA 在后,即使编译之后也是这个顺序。
    如果采用 dw 版本,应该这样写:
		dw 0xaa55
  • 因为,在 Intel 处理器上,将一个字写入内存时,是采用低端字节序的,低字节 0x55 置入低地址端(在前),高字节 0xAA 在高地址端(在后)。

代码清单5-1

1		;代码清单5-1
2		;文件名:c05_mbr.asm
3		;文件说明:硬盘主引导扇区代码
4		;创建日期:2011-3-31 21:15
5
6		mov ax,0xb800 ;指向文本模式的显示缓冲区
7		mov es,ax
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
73		mov byte [es:0x1b],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

资料参考

  • 《x86 汇编语言:从实模式到保护模式》(编著:李忠 王晓波 余杰)
发布了29 篇原创文章 · 获赞 42 · 访问量 8503

猜你喜欢

转载自blog.csdn.net/qq_43068326/article/details/104570866