第2章 编写MBR主引导记录,让我们开始 掌权

第2章 编写MBR主引导记录,让我们开始掌权

2.1 计算机的启动过程

不知道大家对“载入内存”这4个字的理解是怎样的。以下这两点是我曾经的疑问:第一,为什么程序要载入内存。第二,什么是载入内存。

先回答第一个。

CPU的硬件电路被设计成只能运行处于内存中的程序,这是硬件基因的问题,这样做的原因,首先肯定是内存比较快,且容量大。

其次,操作系统可以存储在软盘上,也可以存储在硬盘上,甚至U盘,当然还有很多存储介质都可以。但由于各个硬件特性不同,操作系统要分别考虑每种硬件的特性才行。所以,都在内存中运行程序,操作系统和硬件设计都省事了,这可能也是为了方式的统一吧,否则总不能出现某种存储介质后,操作系统和硬件就要付出额外努力去支持。当然,具体原因只有硬件工程师才知道,咱们在此先打住,继续咱们的内容。

马上回答第二个。

老听说“程序载入内存”,我不知道有多少同学对这个词仅仅是感性认识。

我隐约觉得很多同学都会将“载入内存”和“程序执行”画等号。所谓的载入内存,大概上分两部分。

(1)程序被加载器(软件或硬件)加载到内存某个区域。

(2)CPU的cs:ip寄存器被指向这个程序的起始地址。

操作系统在加载程序时,是需要某个加载器来将用户程序存储到内存中的。其实“加载器”这只是人为起的名字,突显了其功能,并不是多么神秘的东西,本质上它就是一堆函数组成的模块,不要因为未知的东西而感到畏惧。

从按下主机上的power键后,第一个运行的软件是BIOS。于是产生了三个问题。

(1)它是由谁加载的。

(2)它被加载到哪里。

(3)它的cs:ip是谁来更改的。

2.2 软件接力第一棒,BIOS

BIOS全称叫Base Input & Output System,即基本输入输出系统。

人们给任何事物起名字,肯定都不是乱起的,必然是根据该事物的特点,通过总结,精练出一些文字来标识此事物,这个便是对一般事物取名的方法。通过名字,就能够反应出该事物的特性。最符合特性的名字就是昵称和外号了,比如抽油机是用来开采石油的一种机器,因为其工作时,就像“磕头”一样,所以大家给其起了更形象的名字—“磕头机”。

回到BIOS上,输入输出我理解,命名中加上系统二字也明白,可为什么还要用“基本”来修饰呢?不知道您是不是和我一样喜欢咬文嚼字,我们必须得把它搞清楚。

2.2.1 实模式下的1MB内存布局

先来点背景知识,很久很久以前:

Intel 8086有20条地址线,故其可以访问1MB的内存空间,即2的20次方=1048576=1MB,地址范围若按十六进制来表示,是0x00000到0xFFFFF。不知道硬件工程师当时设计的初衷是什么,总之人家有自己的理由,这1MB的内存空间被分成多个部分。

为了让大家先有个印象,免得太抽象不容易理解,先把实模式下1MB内存给大家梳理一下,很辛苦的,各位看官要仔细看哈,所以感兴趣或有强迫症的同学一定要背下来(玩笑),见表2-1。

表2-1  实模式下的内存布局

起始

结  束

大  小

用  途

FFFF0

FFFFF

16B

BIOS入口地址,此地址也属于BIOS代码,同样属于顶部的640KB字节。只是为了强调其入口地址才单独贴出来。此处16字节的内容是跳转指令jmp f000:e05b

F0000

FFFEF

64KB-16B

系统BIOS范围是F0000~FFFFF共640KB,为说明入口地址,将最上面的16字节从此处去掉了,所以此处终止地址是0XFFFEF

C8000

EFFFF

160KB

映射硬件适配器的ROM或内存映射式I/O

C0000

C7FFF

32KB

显示适配器BIOS

B8000

BFFFF

32KB

用于文本模式显示适配器

B0000

B7FFF

32KB

用于黑白显示适配器

A0000

AFFFF

64KB

用于彩色显示适配器

9FC00

9FFFF

1KB

EBDA(Extended BIOS Data Area)扩展BIOS数据区

7E00

9FBFF

622080B约608KB

可用区域

7C00

7DFF

512B

MBR被BIOS加载到此处,共512字节

500

7BFF

30464B约30KB

可用区域

400

4FF

256B

BIOS Data Area(BIOS数据区)

000

3FF

1KB

Interrupt Vector Table(中断向量表)

先从低地址看,地址0~0x9FFFF处是DRAM(Dynamic Random Access Memory),即动态随机访问内存,我们所装的物理内存就是DRAM,如DDR、DDR2等。又要开始咬文嚼字了,动态是什么意思?动态指此种存储介质由于本身电气元件的性质,需要定期地刷新。内存中的每一位都是由电容和晶体管来组成的,您想,单条内存现在都到4GB,内存条的体积大小您也清楚,那么小的面积得集成多少电容才能够拼凑出4GB的内存容量,不包括相关电路元件,也得是4GB×8个电容了。如此小的电容,其缺点也是明显的,漏电很快,所以漏电了就要及时把电补充上去,这样数据才不至于丢失。这个补充电的过程就称为刷新。其实不仅是电容需要刷新,就连电信号也是一样的,不知道您注意了没有,我们平时使用的网线,也是需要在每隔一定长度距离时接个中继放大器,这个就是来放大电信号的,因为物理链路一长,信号衰减就特别严重,只好通过这种“打气”的方式来保持稳定了。终于把动态这一词搞定了,不过我们最终要搞定的词是BIOS中的“基本”,所以咱们还得接着看。

见表2-1,内存地址0~0x9FFFF的空间范围是640KB,这片地址对应到了DRAM,也就是插在主板上的内存条。有没有人开始小声嘀咕了:为什么是对应到了DRAM,难道不是直接访问到我的物理内存DRAM吗?难道我的内存条不是全部的内存?还可以访问到别处吗?如果您有这样的疑问,我除了回答是啊是啊之外,还是很欣慰的,终于有人和我之前想的一样了。

一会再解释这个,否则咱们离“基本”越来越远了。表2-1,看顶部的0xF0000~0xFFFFF,这64KB的内存是ROM。这里面存的就是BIOS的代码。BIOS的主要工作是检测、初始化硬件,怎么初始化的?硬件自己提供了一些初始化的功能调用,BIOS直接调用就好了。BIOS还做了一件伟大的事情,建立了中断向量表,这样就可以通过“int中断号”来实现相关的硬件调用,当然BIOS建立的这些功能就是对硬件的IO操作,也就是输入输出,但由于就64KB大小的空间,不可能把所有硬件的IO操作实现得面面俱到,而且也没必要实现那么多,毕竟是在实模式之下,对硬件支持得再丰富也白搭,精彩的世界是在进入保护模式以后才开始,所以挑一些重要的、保证计算机能运行的那些硬件的基本IO操作,就行了。这就是BIOS称为基本输入输出系统的原因。

现在开始解释另一个问题,在CPU眼里,为什么我们插在主板上的物理内存不是它眼里“全部的内存”。

地址总线宽度决定了可以访问的内存空间大小,如16位机的地址总线为20位,其地址范围是1MB,32位地址总线宽度是32位,其地址范围是4GB。但以上的地址范围是指地址总线可以触及到的边界,是指计算机在寻址上可以到达的疆域。可是人家并没有说要寻哪里,就拿16位机来说,并没有说这1MB的寻址范围必须得是物理内存(内存条),难道人家20位的地址总线就认得这一亩三分地?完全不是。

归根结底的原因是这样的:在计算机中,并不是只有咱们插在主板上的内存条需要通过地址总线访问,还有一些外设同样是需要通过地址总线来访问的,这类设备还很多呢。若把全部的地址总线都指向物理内存,那其他设备该如何访问呢?由于这个原因,只好在地址总线上提前预留出来一些地址空间给这些外设用,这片连续的地址给显存,这片连续的地址给硬盘控制器等。留够了以后,地址总线上其余的可用地址再指向DRAM,也就是指插在主板上的内存条、我们眼中的物理内存。示意如图2-1所示。

Doc2.files\image001.png

▲图2-1 地址映射

物理内存多大都没用,主要是看地线总线的宽度。还要看地址总线的设计,是不是全部用于访问DRAM。所以说,地址总线是决定我们访问哪里、访问什么,以及访问范围的关键。我们平时用的机器一般是32位,上面的内存条并不是全部都用到了,按理说内存条大小超过4GB就没意义了,超过了地址总线的势力就是浪费。不过通过前面的介绍,即使内存条大小没有超过地址总线的范围,也不会全都能被访问到,毕竟要预留一些地址用来访问其他外设,所以最终还得看地址总线把地址指向哪块内存了。这就是安装了4GB内存,电脑中只显示3.8GB左右的原因。

总之,表示地址的那串数字是地址总线的输入,相当于其参数,和内存条没关系。CPU能够访问一个地址,这是地址总线给做的映射,相当于给该地址分配了一个存储单元,而该存储单元要么落在某个rom中,要么落到了某个外设的内存中,要么落到了物理内存条上。可以想像成,CPU给地址总线提交一个数字,在地址总线看来,这串数字就是地址。地址分配电路根据此地址的范围,决定在哪个存储介质中分配一个存储单元,最后将此地址与此存储单元对应起来。当然事实上未必是这样,我刚才说了,可以想像成这样。我们学习新的知识,很多时候都是建立在原有的知识上,用原有的知识帮助学习新的知识,就像第一次听说电动车的时候,我们潜意识里是用车和蓄电池的概念在联想电动车的形象。如果要学的是一种全新的知识,并且无从用旧的知识来辅助学习时,试图靠想像力是非常有效的。对于知识的掌握,这并没有什么标准,每个人对知识的理解都是不同的,即使两个人都考了满分,其思考过程也是不同的。所以,对于一个新知识的掌握,本质上是给了一个能够说服自己的理由,能够自圆其说,这就够了。

2.2.2 BIOS是如何苏醒的

BIOS其实一直睡在某个地方,直到被唤醒……

前面热火朝天地说了BIOS的功能和内存布局,似乎还没说到正题上,BIOS是如何启动的呢?因为BIOS是计算机上第一个运行的软件,所以它不可能自己加载自己,由此可以知道,它是由硬件加载的。那这个硬件是谁呢?其实前面已经提到过了,相当于是只读存储器ROM,因为它一直就睡在那里不动。

大家知道,只读存储器中的内容是不可擦除的,也就是它不像动态随机访问存储器DRAM那样,掉电后,里面的数据就会丢失。这种存储介质是用来存储一成不变的数据的,当数据写进去后,便与日月同辉,庭前坐看花开花落,不朽于天地万物之间,哈哈,有点夸张了。

BIOS代码所做的工作也是一成不变的,而且在正常情况下,其本身是不需要修改的,平时听说的那些主板坏了要刷BIOS的情况属于例外。于是BIOS顺理成章地便被写进此ROM。ROM也是块内存,内存就需要被访问。此ROM被映射在低端1MB内存的顶部,即地址0xF0000~0xFFFFF处,可以参考表2-1顶部的BIOS部分。只要访问此处的地址便是访问了BIOS,这个映射是由硬件完成的。

BIOS本身是个程序,程序要执行,就要有个入口地址才行,此入口地址便是0xFFFF0。

最重要的一点来了,知道了BIOS在哪里后,CPU如何去执行它,即CPU中的cs:ip值是如何组合成0xFFFF0的。

如果大家不了解内存的分段访问机制,可以参考第0章,里面有讲解CPU为什么分段方式内存。说正事,CPU访问内存是用段地址+偏移地址来实现的,由于在实模式之下,段地址需要乘以16后才能与偏移地址相加,求出的和便是物理地址,CPU便拿此地址直接用了。这个“段基址:段内偏移地址”的组合是0xffff:0吗?或者是0xF000:0xFFF0?或者是更奇葩一点的组合:0xFEEE:0x1110?或者您想出的组合比我的还奇葩,好啦,不折磨大家了,还是说正事要紧。既然作为第一个运行的程序都没开始执行,自然就没办法用软件搞定这件事了,还是得靠硬件支持才行。

在开机的一瞬间,也就是接电的一瞬间,CPU的cs:ip寄存器被强制初始化为0xF000:0xFFF0。由于开机的时候处于实模式,再重复一遍加深印象,在实模式下的段基址要乘以16,也就是左移4位,于是0xF000:0xFFF0的等效地址将是0xFFFF0。上面说过了,此地址便是BIOS的入口地址。

当我给出这个地址后,不知道大家意识到什么没有。BIOS是在实模式下运行的,而实模式只能访问1MB空间(20位地址线,2的20次方是1MB)。而地址0xFFFF0距1MB只有16个字节了(见表2-1除标题外的第一行),这么小的空间够干吗?BIOS又要检测硬件,做各种初始化工作,还要建立中断向量表……16字节的机器指令肯定干不了这么多事。也许有的同学会问,超过寄存器宽度会怎么样呢?比如0xFFFF0+16,这样就溢出了,由于实模式下的寄存器宽度是16位,0xFFFF0+16已经超过了其最大值0xFFFFF。溢出的部分就会回卷到0,又会重新开始,即0xFFFF0+16等于0,0xFFFF0+17等于1。

既然此处只有16字节的空间了,这只能说明BIOS真正的代码不在这,那此处的代码只能是个跳转指令才能解释得通了。好,既然心里有了推断,那咱们就来证明这个推断正确与否。

图2-2是我在bochs中抓的图,下面给大家分析一下这图中的信息都代表什么。

图片 2

▲图2-2 bochs开机界面

首先得承认,这张图有点超前了,这是在有了MBR后才能抓到的,否则会提示boot failed: not a bootable disk,而我们还没有MBR,还没有写主引导记录。先不管这张图是怎么来的啦,反正大家立即就能够在自己的虚拟机里看到这张图了。大家先注意框框中的内容。一共有3个,最上面左边第1个标有cs:ip的那个框,cs寄存器的值是0xf000,ip寄存器的值是0xfff0,也就是段基址0xf000,段内偏移地址0xfff0,这个组合出来的地址便是0xffff0,这是处理器下一条待执行指令的地址。这与上面所说的BIOS入口地址是吻合的。另外,因为cs和ip寄存器中存储的是下一条要执行的指令,目前还没有执行,也就是说,当前还没有执行BIOS,这是机器刚开机的那一刻。这一刻还是值得庆祝的,因为即使是计算机行业的同学都很少看到这一刻,何况我们让这一刻停了下来,成为永恒。

按理说,既然让CPU去执行0xFFFF0处的内容(目前还不知道其是指令,还是数据),此内容应该是指令才行,否则这地址处的内容若是数据,而不是指令,CPU硬是把它当成指令来译码的话,一定会弄巧成拙铸成大错。现在咱们又有了新的推断,物理地址0xFFFF0处应该是指令,继续探索。

继续看第二个框框,里面有条指令jmp far f000:e05b,这是条跳转指令,也就是证明了在内存物理地址0xFFFF0处的内容是一条跳转指令,我们的判断是正确的。那CPU的执行流是跳到哪里了呢?段基址0xf000左移4位+0xe05b,即跳向了0xfe05b处,这是BIOS代码真正开始的地方。

第三个框框cs:f000,其意义是cs寄存器的值是f000,与我们刚刚所说的加电时强制将cs置为f000是吻合的,正确。

接下来BIOS便马不停蹄地检测内存、显卡等外设信息,当检测通过,并初始化好硬件后,开始在内存中0x000~0x3FF处建立数据结构,中断向量表IVT并填写中断例程。

好了,终于到了接力的时刻,这是这场接力赛的第一棒,它将交给谁呢?咱们下回再说。

2.2.3 为什么是0x7c00

计算机执行到这份上,BIOS也即将完成自己的历史使命了,完成之后,它又将睡去。想到这里,心中不免一丝忧伤,甚至有些许挽留它的想法。可是,这就是它的命,它生来被设计成这样,在它短暂的一生中已经为后人创造了足够的精彩。何况,在下一次开机时,BIOS还会重复这段轮回,它并没有消失。好了,让伤感停止,让梦想前行。

先说重点,BIOS最后一项工作校验启动盘中位于0盘0道1扇区的内容。

在此插播一段小告示:在计算机中是习惯以0作为起始索引的,因为人们已经习惯了偏移量的概念,无论是机器眼里和程序员眼里,用“相对”的概念,即偏移量来表示位置显得很直观,所以很多指令中的操作数都是用偏移量表示的。0盘0道1扇区本质上就相当于0盘0道0扇区。为什么称为1呢,因为硬盘扇区的表示法有两种,我们描述0盘0道1扇区用的便是其中的一种:CHS方法,即柱面Cylinder 磁头Header 扇区Sector(另外一种是LBA方式,暂不关心),“0盘”说的是0磁头,因为一张盘是有上下两个盘面的,一个盘面上对应一个磁头,所以用磁头Header来表示盘面。“0道”是指0柱面,柱面Cylinder指的是所有盘面上、编号相同的磁道的集合,形象一点描述就是把很多环叠摞在一起的样子,组合在一起之后是一个立体的管状。“1扇区”才是我们要解释的部分,将磁道等距划分成一段段的小区间,由于磁道是圆形的,确切地说是圆环,这些被划分出来的小区间便是扇形,所以称为扇区。好了,背景交待完了,重点来了,在CHS方式中扇区的编号是从1开始的,不是0,不是0,原谅我说了两次,良苦用心你懂的,所以0盘0道1扇区其实就相当于0盘0道0扇区,它就是磁盘上最开始的那个扇区。而LBA方式中,扇区编号是从0开始的。关于硬盘的知识我会在以后章节专门来讲,这里我若没表达清楚,大家先不要着急,只要知道MBR所在的位置是磁盘上最开始的那个扇区就行了。

继续说,如果此扇区末尾的两个字节分别是魔数0x55和0xaa,BIOS便认为此扇区中确实存在可执行的程序(在此先剧透一下,此程序便是久闻大名的主引导记录MBR),便加载到物理地址0x7c00,随后跳转到此地址,继续执行。

这里有个小细节,BIOS跳转到0x7c00是用jmp 0:0x7c00实现的,这是jmp指令的直接绝对远转移用法,段寄存器cs会被替换,这里的段基址是0,即cs由之前的0xf000变成了0。

如果此扇区的最后2个不是0x55和0xaa,即使里面有可执行代码也无济于事了,BIOS不认,它也许还认为此扇区是没格干净呢。

不过,这就又抛出两个问题。

(1)为什么是0盘0道1扇区的内容?

(2)为什么是物理地址0x7c00,而不是个好记或好看的其他地址?

先回答第1个,我想这个问题不用官方解释了,因为官方确实没什么好说的,不过他们出于尊重客户,还是会像我一样说出类似下面的话。

我就个人观点给大家一个理由,未经核实,仅是自己一面之词,请大家提高警惕,小心谨慎^—^。

在计算机中处处充满了协议、约定,所以,将0盘0道1扇区作为mbr的栖身之地,我完全可以理解为规定。我们反证一下,如果不存在这个“规定”,会发生什么。当然,此扇区最初是给BIOS使用的,咱们设想一下BIOS的工作将变成怎样。

主引导记mbr是段程序,无论位于软盘、硬盘或者其他介质,总该有个地方保存它。Ok,现在不告诉BIOS它存储在哪个位置了。BIOS只好将所有检测到的存储设备上的每一个存储单位都翻一遍,挨个对比,如果发现该存储单位最后的两个字节是0x55和0xaa,就认为它是mbr。这就好比查字典一样,不用偏旁部首和拼音检索的方法,只能一页一页翻了。

几经花开花落,找到mbr的那一刻,BIOS满脸疲惫地说:“你是我找了好久好久的那个人”。mbr抬起经不起岁月等待的脸:“难得你还认得我,我等你等到花儿都谢了”。其实BIOS的心声是:“看我手忙脚乱的样子,你们这是要闹哪样啊。就那么512字节的内容,害我找遍全世界,我们是在跑接力赛啊,下一棒的选手我都不知道在哪里……以后让它站在固定的位置等我!”

由于0盘0道1扇区是磁盘的第一个扇区,mbr选择了离BIOS最近的位置站好了,从此以后再也不担心被BIOS骂了。

计算机中处处有固定写死的东西,还用举个例子吗?不用了吧?因为任何一个魔数都是啊,有请下一个魔数0x7c00登场。

至于0x7c00,很久之前,比我好奇心大的人查遍了Intel开发手册都没找到相关的说明。要想知道事情的来龙去脉,还是要从个人计算机的初始说起,同样是很久很久以前……

1981年8月,IBM公司生产了世界上第一台个人计算机PC 5150,所以它就是现代x86个人计算机兼容机的祖先。说到有关历史的东西,不给来点真相就感觉气场不足,图2-3所示便是IBM PC 5150,有没有感受到计算机文化底蕴呢?

图片 1

▲图2-3 IBM PC 5150

既然Intel开发手册中没有相关说明,那咱们就朝其他方向找答案,换句话说,既然不是CPU的硬性规定,那很可能就是代码中写死的。为了搞清楚0x7c00是哪里来的,咱们先探索下“IBM PC 5150”的BIOS的秘密。请先深深呼吸一大口气,“0x7C00”最早出现在IBM 公司出产的个人电脑PC5150的ROM BIOS的 INT19H中断处理程序中,说了这么多定语,感觉气都喘不上来了。

通电开机之后,BIOS处理程序开始自检,随后,调用BIOS中断0x19h,即 call int 19h。在此中断处理函数中,BIOS要检测这台计算机有多少硬盘或软盘,如果检测到了任何可用的磁盘,BIOS就把它的第一个扇区加载到0x7c00。

现在应该搞清楚了为什么在x86手册里找不到它的说明了,它是属于BIOS中的规范。似乎这下好办了,既然是BIOS中的规范,那肯定是IBM PC 5150 BIOS 开发团队规定的这个数。

个人计算机肯定要运行操作系统,在这台计算机上,运行的操作系统是DOS 1.0,不清楚此系统要求的最小内存是16KB,还是32KB,反正PC 5150 BIOS研发工程师就假定其是32KB的,所以此版本BIOS是按最小内存32KB研发的。

MBR不是随便放在哪里都行的,首先不能覆盖已有的数据,其次,不能过早地被其他数据覆盖。不覆盖已有数据,这个好理解。说一下后面这个“其次”。通常,MBR的任务是加载某个程序(这个程序一般是内核加载器,很少有直接加载内核的)到指定位置,并将控制权交给它。所谓的交控制权就是jmp过去而已。之后MBR就没用了,被覆盖也没关系。我说的过早被覆盖,是指不能让mbr破坏自己,比如被加载的程序,如内核加载器,其放置的内存位置若是MBR自己所在的范围,这不就是破坏自己了吗,这就是我所说的“过早”了,怎么也得等MBR执行完才行。

重现一下当时的内存使用情况。

8086CPU要求物理地址0x0~0x3FF存放中断向量表,所以此处不能动了,再选新的地方看看。

按DOS 1.0要求的最小内存32KB来说,MBR希望给人家尽可能多的预留空间,这样也是保全自己的作法,免得过早被覆盖。所以MBR只能放在32KB的末尾。

MBR本身也是程序,是程序就要用到栈,栈也是在内存中的,MBR虽然本身只有512字节,但还要为其所用的栈分配点空间,所以其实际所用的内存空间要大于512字节,估计1KB内存够用了。

结合以上三点,选择32KB中的最后1KB最为合适,那此地址是多少呢?32KB换算为十六进制为0x8000,减去1KB(0x400)的话,等于0x7c00。这就是倍受质疑的0x7c00的由来,这下清楚了。

可见,加载MBR的位置取决于操作系统本身所占内存大小和内存布局。

我想大家现在都心痒痒了吧,说了这么久,CPU中运行的都是BIOS的代码,连自己一句代码都没跑起来呢。事不宜迟,马上写一个MBR,先让它跑起来再说。

2.3 让MBR先飞一会儿

虽说主引导记录mbr是咱们能够掌控的第一个程序,但这并不是让我们为之激动的理由。我们平时所写的程序都要依赖于操作系统,而我们即将实现的这个程序是独立于操作系统的,能够直接在裸机上运行,这才是让我们激动的理由,对咱们来说这无疑是历史性的一刻。还记得当初我的MBR跑起来时,那可真是发自内心的高兴呀。

好了,不再抒情了,说正事要紧。MBR的大小必须是512字节,这是为了保证0x55和0xaa这两个魔数恰好出现在该扇区的最后两个字节处,即第510字节处和第511字节处,这是按起始偏移为0算起的。由于我们的bochs模拟的是x86平台,所以是小端字节序,故其最后两个字节内容是0xaa55,写到一起后似乎有点不认识了,不要怕,拆开就是0x55和0xaa。

2.3.1 神奇好用的$和$$,令人迷惑的section

$和$$是编译器NASM预留的关键字,用来表示当前行和本section的地址,起到了标号的作用,它是NASM提供的,并不是CPU原生支持的,相当于伪指令一样,对CPU来说是假的。

指令本来没有真伪之别,就像酒一样,因为有了假酒,所以才有了真酒之说。伪指令是相对于CPU可识别的指令来说的,它(伪指令)只是编译器定义的,CPU中并不存在这个指令,愣让CPU执行这些伪指令,CPU会抛出“UD(未定义的操作码)”异常。伪指令是编译器为了开发人员写代码方便而提供的一些符号,这些符号在编译时,会由编译器转换成CPU可识别的东西,如指令或地址等。

汇编语言中的标号是程序员“显式地”写在明处的,如:

……
code_start:
  mov ax, 0
……

code_start这个标号被nasm认为是一个地址,此地址便是“mov ax,0”这条指令所在的地址,即其指令机器码存放的内存位置是code_start。code_start只是个标记,CPU并不认识,和伪指令类似,它是假的,CPU不认。所以nasm会用为其安排的地址来替换标号code_start,到了CPU手中,已经被替换为有意义的数字形式的地址了。

$属于“隐式地”藏在本行代码前的标号,也就是编译器给当前行安排的地址,看不到却又无处不在,$在每行都有。或者这种说法并不是很正确,只有“显示地”用了$的地方,nasm编译器才会将此行的地址公布出来。如果上面的例子改为:

……
code_start:
  jmp $
……

这就和jmp code_start是等效的。$和code_start是同一个值。

$$指代本section的起始地址,此地址同样是编译器给安排的。

对于$和$$的意义,我强调过了,是编译器给安排的地址,默认情况下,它们的值是相对于本文件开头的偏移量。至于实际安排的是多少,还要看程序员同学是否在section中添加了vstart。这个关键字可以影响编译器安排地址的行为,如果该section用了vstart=xxxx修饰,$$的值则是此section的虚拟起始地址xxxx。$的值是以xxxx为起始地址的顺延。如果用了vstart关键字,想获得本section在文件中的真实偏移量(真实地址)该怎么做?nasm编译器提供了这个方法。

section.节名.start。

如果没有定义section,nasm默认全部代码同为一个section,起始地址为0。

稍带说一下section。很多东西从名字上就能理解它的功能,毕竟名字不是乱起的。section也称为节、段,故名思义,是程序中的一小块,形象一点地说,就是用section这个关键字在程序中圈出一块地,并向编译器宣称,这块地我要做些规划,至于我用来干什么您就不用操心了,编译时请您合理安排。

为什么说合理安排呢,因为section是伪指令,是nasm提供的,具体解释权还是人家nasm说了算。比如以下代码:

section data
   var dd 0
section code
   jmp start
… ...

编译器一看这两个section,data中定义的是变量,code中是代码,于是把这两个section的内容分别归入最终的数据段和代码段。

有时候nasm并不会完全听您的,如改为下面的例子:

section data_a
  var dd 0
section code
  jmp start
section data_b
  var dd 1
  ……

虽然人为定义了三个section,但nasm发现data_a和data_b这两个section完全能够合并到一起,于是在编译阶段会被“合理”地安排到一起。

在第0章中有说明section和segment的区别。section是伪指令,CPU运行程序是不需要这个东西的,这个只是用来给程序员规划程序用的,有了section,就可以将自己的代码分成一段一段的,当然这只是在逻辑上的段,实际上编译出来的程序还是完整的一体。逻辑上划分成段的好处是方便开发人员梳理代码,方便管理。想像一下,把一大片农田按亩来划分成一个个的小段,一眼望去,是不是显得井然有序呢?单是简短的几行汇编代码是无法体现出这一优势的,就像如果农田本来就不大,还要划分成多个段,那自然是得不偿失的。当代码量上去的时候,会发现如果不在逻辑上将其拆分成几块,对一锅粥似的代码进行维护,代价还是很大的,可能一会儿脑子也像一锅粥了呢。

划分成section后,编译器便根据您的意图,将这些section中的内容安排位置,它被安排到哪里咱们是不需要关心的,咱们也不必管,因为程序内部的关联是通过地址实现的。想想看,无非是section被安排到A位置,其他用到此section中内容的相关指令,其操作数为A地址,若section被安排到B位置,操作数便是B地址,这些都是编译器安排的,它会帮您圆上的。

关于section地址更详细的说明,大家可以参照第3章,这里只是抛砖引玉。

总之,section是给开发人员逻辑上规划代码用的,只起到思路清晰的作用,最终还是在编译阶段由nasm在物理上的规划说了算。

2.3.2 NASM简单用法

在咱们的实际工程中只用到了nasm的一些简单功能,所以不必担心连操作系统的一句代码都没写呢,却先要为学习其他的东西而付出额外的精力。

nasm -f <format><filename> [-o <output>]

以上是nasm的基本用法,对咱们来说,够用了。注意我说的是“基本”,还有好多其他参数呢,不过咱们用不着。甚至,大多数时候连-f都不用呢。

-o 就是指定输出可执行文件的名称。

查看一下nasm的帮助,ok,执行man nasm回车,输出的信息太多了,我们只看-f的说明就行了。

-f format
   Specifies the output file format. 
To see a list of valid output formats, use the -hf option.

瞧,人家说啦,-f是用来指定输出文件的格式。要想知道有多少种有效的输出格式,用-hf选项。那咱们还是用nasm –hf来查看一下吧,见表2-2。

表2-2  nasm编译输出的格式

格  式

描  述

bin

flat-form binary files (e.g. DOS .COM, .SYS),此项为默认

ith

Intel hex

srec

Motorola S-records

aout

Linux a.out object files

aoutb

NetBSD/FreeBSD a.out object files

coff

COFF (i386) object files (e.g. DJGPP for DOS)

elf32

ELF32 (i386) object files (e.g. Linux)

elf64

ELF64 (x86_64) object files (e.g. Linux)

elfx32

ELFX32 (x86_64) object files (e.g. Linux)

as86

Linux as86 (bin86 version 0.3) object files

obj

MS-DOS 16-bit/32-bit OMF object files

win32

Microsoft Win32 (i386) object files

win64

Microsoft Win64 (x86-64) object files

rdf

Relocatable Dynamic Object File Format v2.0

ieee

IEEE-695 (LADsoft variant) object file format

macho32

NeXTstep/OpenStep/Rhapsody/Darwin/MacOS X (i386) object files

macho64

NeXTstep/OpenStep/Rhapsody/Darwin/MacOS X (x86_64) object files

dbg

Trace of all info passed to output stage

elf

ELF (short name for ELF32)

macho

MACHO (short name for MACHO32)

win

WIN (short name for WIN32)

一共列出了21个,不过大部分格式和咱们关系不大,咱们只关注bin和elf格式就好啦。

既然bin是默认输出格式,也就是不用-f bin来明确指定了,所以以后咱们只在输出elf格式时才用-f指定。

bin是指纯二进制。二进制就二进制吧,还有不纯的?就像前面的拿酒举例一样,本来没有真酒之说,由于有了假酒的出现,才有了真的说法。纯二进制就是不掺杂其他的东西,直接给CPU后就能用,也就是可执行文件中什么样,内存中就什么样。我们平时所说的elf或pe格式的二进制可执行文件,那里面有好多和指令无关的东西,里面掺杂了程序的内存布局、位置等信息,这是给操作系统中的程序加载器用的,是属于操作系统规划的范畴了。

2.3.3 请下一位选手MBR同学做准备

有点不好意思了,说了好久,才说到实质性的东西,好了,赶紧说正题。

代码2-1 (c2/a/boot/mbr.S)

 1 ;主引导程序
 2 ;------------------------------------------------------------
 3 SECTION MBR vstart=0x7c00
 4   mov ax,cs
 5   mov ds,ax
 6   mov es,ax
 7   mov ss,ax
 8   mov fs,ax
 9   mov sp,0x7c00
10
11 ; 清屏利用0x06号功能,上卷全部行,则可清屏。
12 ; -----------------------------------------------------------
13 ;INT 0x10  功能号:0x06  功能描述:上卷窗口
14 ;------------------------------------------------------
15 ;输入:
16 ;AH 功能号= 0x06
17 ;AL = 上卷的行数(如果为0,表示全部)
18 ;BH = 上卷行属性
19 ;(CL,CH) = 窗口左上角的(X,Y)位置
20 ;(DL,DH) = 窗口右下角的(X,Y)位置
21 ;无返回值:
22   mov   ax, 0x600
23   mov   bx, 0x700
24   mov   cx, 0        ; 左上角: (0, 0)
25   mov   dx, 0x184f      ; 右下角: (80,25),
26                   ; VGA文本模式中,一行只能容纳80个字符,共25行。
27                  ; 下标从0开始,所以0x18=24,0x4f=79
28   int   0x10        ; int 0x10
29
30 ;;;;;;;;;  下面这三行代码获取光标位置  ;;;;;;;;;
31 ;.get_cursor获取当前光标位置,在光标位置处打印字符。
32   mov ah, 3           ; 输入: 3号子功能是获取光标位置,需要存入ah寄存器
33   mov bh, 0          ; bh寄存器存储的是待获取光标的页号
34
35   int 0x10           ; 输出: ch=光标开始行,cl=光标结束行
36                   ; dh=光标所在行号,dl=光标所在列号
37
38 ;;;;;;;;;  获取光标位置结束  ;;;;;;;;;;;;;;;;
39
40 ;;;;;;;;;   打印字符串  ;;;;;;;;;;;
41   ;还是用10h中断,不过这次调用13号子功能打印字符串
42   mov ax, message
43   mov bp, ax          ; es:bp 为串首地址,es此时同cs一致,
44                 ; 开头时已经为sreg初始化
45
46   ; 光标位置要用到dx寄存器中内容,cx中的光标位置可忽略
47   mov cx, 5          ; cx 为串长度,不包括结束符0的字符个数
48   mov ax, 0x1301      ;子功能号13显示字符及属性,要存入ah寄存器,
49                  ; al设置写字符方式 ah=01: 显示字符串,光标跟随移动
50   mov bx, 0x2         ; bh存储要显示的页号,此处是第0页,
51                 ; bl中是字符属性,属性黑底绿字(bl = 02h)
52   int 0x10           ; 执行BIOS 0x10 号中断
53 ;;;;;;;;;   打字字符串结束  ;;;;;;;;;;;;;;;
54
55   jmp $            ; 使程序悬停在此
56
57   message db "1 MBR"
58   times 510-($-$$) db 0
59   db 0x55,0xaa

简短说一下代码功能,在屏幕上打印字符串“1 MBR”,背景色为黑色,前景色为绿色。

由于还没有给大家讲解显卡的使用方法,故本段代码中关于“打印显示”的操作都利用BIOS给我们建立好的例程就好了,这里第0x10号中断便是负责有关打印的例程。

0x10中断是最为强大的BIOS中断了,调用的方法是把功能号送入ah寄存器,其他参数按照BIOS中断手册的要求放在适当的寄存器中,随后执行int 0x10即可。我们不用太细致琢磨BIOS功能调用了,大家可以参数代码中的注释了解下即可,毕竟咱们这里用BIOS中断只是临时的,以后也用不到了。

第3行的“vstart=0x7c00”表示本程序在编译时,告诉编译器,把我的起始地址编译为0x7c00。

第4~8行是用cs寄存器的值去初始化其他寄存器。由于BIOS是通过jmp 0:0x7c00跳转到MBR的,故cs此时为0。对于ds、es、fs、gs这类sreg,CPU中不能直接给它们赋值,没有从立即数到段寄存器的电路实现,只有通过其他寄存器来中转,这里我们用的是通用寄存器ax来中转。例如mov ds:0x7c00,这样就错了。

第9行是初始化栈指针,在CPU上运行的程序得遵从CPU的规则,mbr也是程序,是程序就要用到栈。目前0x7c00以下暂时是安全的区域,就把它当作栈来用。

第11~28行是清屏。因为在BIOS工作中,会有一些输出,如检测硬件的结果信息。为了让大家看清楚我们在MBR中的输出字符串,故先把BIOS的输出清掉,这里演示的是BIOS中断int 0x10的用法。

第30~35行是做打印前的工作,先获取光标位置,目的是避免打印字符混乱,覆盖别人的输出。其实这是防君子不防小人的做法,万一别人不在光标处打印,自己打印的内容同样也会被别人覆盖。不管别人了,咱们做好自己的就行,老老实实地只在光标处打印。不知道这是否能提醒大家,字符打印的位置,不一定要在光标处,字符的位置只和显存中的地址有关,和光标是没关系的,这只是人为地加个约束,毕竟光标在视觉上告诉了我们当前字符写到哪里了,完全是为了好看,不要以为光标就是新打印字符的位置。更多细节,以后讲显卡时会提到。

这里还用到了页的概念,您看第33行,往bh寄存器中写入了0,这是告诉BIOS例程,我要获取第0页当前的光标。什么是页呢?

显示器有很多种模式,如图形模式、文本模式等,在文本模式中,又可以工作于80*25和40*25等显示方式,默认情况下,所有个人计算机上的显卡在加电后都将自己置为80*25这种显示方式。80*25是指一屏可以显示25行、每行80列的字符,也就是2000个字符。但由于一个字符要用两字节来表示,低字符是字符的ASCII编码,高字节是字符属性,故显示一屏字符需要用4000字节(实际上,分配给一屏的容量是4KB),这一屏就称为一页,0页是默认页。

第38~52行是往光标处打印字符。说一下第48行的mov ax,0x1301,13对应的是ah寄存器,这是调用0x13号子功能。01对应的是al寄存器,表示的是写字符方式,其低2位才有意义,各位功能描述如下。

(1)al=0,显示字符串,并且光标返回起始位置。

(2)al=1,显示字符串,并且光标跟随到新位置。

(3)al=2,显示字符串及其属性,并且光标返回起始位置。

(4)al=3,显示字符串及其属性,光标跟随到新位置。

第55行执行了个死循环,$是本行指令的地址,这属于伪指令,是汇编器在编译期间分配的地址。在最终编译出来的程序中,$会被替换为指令实际所在行的地址。jmp是个近跳转,$是jmp自己的地址,于是跳到自己所在的地址再执行自己,又是跳到自己所在的地址再继续执行跳转,这样便实现了死循环。可见CPU可乖了,它只会埋头做事,并不会觉得有什么不妥,靠谱,值得依赖。

第57行是定义打印的字符串。

第58行的$$是指本section的起始地址,上面说过了$是本行所在的地址,故$-$$是本行到本section的偏移量。由于MBR的最后两个字节是固定的内容,分别是0x55和0xaa,要预留出这2个字节,故本扇区内前512-2=510字节要填满,那到底要用多少字节才能填满此扇区呢。用510字节减去上面通过$-$$得到的偏移量,其结果便是本扇区内的剩余量,也就是要填充的字节数。由此可见第50行的“times 510-($-$$) db 0”是在用0将本扇区剩余空间填充。

代码说完了,可还有两件大事要做,1是编译,2是如何将编译后的文件存储到0盘0道1扇区中成为MBR,以供BIOS大神加载之用。

前面介绍了nasm的用法,咱们马上来编译汇编代码。

nasm -o mbr.bin mbr.S回车,您看,这样就编译成功了,我连-f都没有指定吧。按理说此文件大小是512字节,咱们用ls命令验证一下:ls -lb mbr.bin回车,以下是ls的输出。

-rw-rw-r--. 1 work work 512 7月 26 21:10 mbr.bin

用过Linux的同学对这个输出还是很熟悉的,若头一次用Linux的同学也不要慌张,这里面好多的信息并不重要,只要看看中间部分就好了,512,果然是512字节,这下心里踏实了,下一步是考虑如何将此文件写入0盘0道1扇区。

这里再给大家介绍另一个Linux命令:dd。dd是用于磁盘操作的命令,功能太强大了,有如穿甲弹一样,可以深入磁盘的任何一个扇区,无坚不摧。所以,它也可以删除Linux操作系统自己的文件,是把双刃剑。

还是先看帮助文件,man dd回车,为了节约大家的时间,我只把咱们今后用到的几个选项摘了出来,还是那句话,够用就行了,需要时再学。

if=FILE
read from FILE instead of stdin

此项是指定要读取的文件。

of=FILE
write to FILE instead of stdout

此项是指定把数据输出到哪个文件。

bs=BYTES
read and write BYTES bytes at a time (also see ibs=,obs=)

此项指定块的大小,dd是以块为单位来进行IO操作的,得告诉人家块是多大字节。此项是统计配置了输入块大小ibs和输出块大小obs。这两个可以单独配置。

count=BLOCKS
copy only BLOCKS input blocks

此项是指定拷贝的块数。

seek=BLOCKS
skip BLOCKS obs-sized blocks at start of output

此项是指定当我们把块输出到文件时想要跳过多少个块。

conv=CONVS
convert the file as per the comma separated symbol list

此项是指定如何转换文件。

append append mode (makes sense only for output; conv=notrunc suggested)

这句话建议在追加数据时,conv最好用notrunc方式,也就是不打断文件。

齐了,dd的介绍就到这了,赶紧试验一下这个神奇的工具吧。

dd if=/your_path/mbr.bin of=/your_path/bochs/hd60M.img bs=512 count=1 conv=notrunc

各位看官,请将上面命令行中的your_path替换为您自己的实际路径。

输入文件是刚刚编译出来的mbr.bin,输出是我们虚拟出来的硬盘hd60M.img,块大小指定为512字节,只操作1块,即总共1*512=512字节。由于想写入第0块,所以没用seek指定跳过的块数。

执行上面的命令后,会有如下输出。

记录了1+0 的读入

记录了1+0 的写出

512字节(512 B)已复制,0.313312 秒,1.6 KB/秒

这就说明命令执行成功了,mbr.bin已经写进hd60M.img的第0块了。借鉴美国宇航员阿姆斯特朗的一句话:虽然这只是简单的一小步,但却是实现我们自己系统的一大步。记得当初我可是非常激动呢。

启动bochs测试一下,我习惯到bochs安装目录下启动它,bin/bochs –f bochsrc.disk回车,接着会显示如图2-4所示的界面。

图片 1

▲图2-4 bochs启动

默认是[6],开始模拟啦。回车。

由于咱们编译的是可调试的版本,所以会停下来,bochs等待咱们键入下一步的命令,如图2-5所示。

大家看到,这一下弹出了两个界面,前面的那个是bochs所模拟的机器,可以认为它就是台电脑了,不仅仅是电脑的显示器。后面的界面是bochs的控制台,咱们控制bochs运行就要在这里输入命令。现在激活后面的bochs控制台,输入字符c后,回车。bochs所模拟的机器就开始运行了。这里键入的c是continue,调试方法同gdb类似,详细的bochs操作方法咱们会在下一章中介绍。

图片 2

▲图2-5 bochs运行

MBR运行起来后,就会出现下面的效果,如图2-6所示。

图片 1

▲图2-6 bochs运行mbr

下一章正式开讲细节部分。

目录

相关技术

推荐用户