继续开发
; hello-os ; TAB=4 ORG 0x7c00 ; 指明程序的装载地址 ; 以下的记述用于标准FAT12格式的软盘 JMP entry DB 0x90 DB "HELLOIPL" ; 启动区的名称 DW 512 ; 每个扇区(sector)的大小 DB 1 ; 簇(cluster)的大小 DW 1 ; FAT的起始位置 DB 2 ; FAT的个数 DW 224 ; 根目录的大小 DW 2880 ; 该磁盘的大小 DB 0xf0 ; 磁盘的种类 DW 9 ; FAT的长度 DW 18 ; 1个磁道(track)有几个扇区 DW 2 ; 磁头数 DD 0 ; 不适用分区 DD 2880 ; 重写一次磁盘大小 DB 0,0,0x29 ; DD 0xffffffff ; DB "HELLO-OS " ; 磁盘名称 DB "FAT12 " ; 磁盘格式名称 RESB 18 ; 先空出18字节 ; 程序核心 entry: MOV AX,0 ; 初始化寄存器 MOV SS,AX MOV SP,0x7c00 MOV DS,AX MOV ES,AX MOV SI,msg putloop: MOV AL,[SI] ADD SI,1 ; 给SI加1 CMP AL,0 JE fin MOV AH,0x0e ; 显示一个文字 MOV BX,15 ; 指定字符颜色 INT 0x10 ; 调用显卡BIOS JMP putloop fin: HLT ; 让CPU停止,等待指令 JMP fin ; 无限循环 msg: DB 0x0a, 0x0a ; 换行2次 DB "hello, world" DB 0x0a ; 换行 DB 0 RESB 0x7dfe-$ ; 0x7dfe和当前字节之间用0x00填写 DB 0x55, 0xaa ; 以下是启动区以外部分的输出 DB 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00 RESB 4600 DB 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00 RESB 1469432
这段程序里有很多新指令,我们从上到下依次来看看。
ORG指令,来源于"origin",意思是“源头,起点”。它会告诉nask,在开始执行的时候,把这些机器语言指令装载到内存的哪个地址,这里是0x7c00。有了这条指令的话,美元符($)的含义也随之变化,它不再是指输出文件的第几个字节,而是将要读入的内存地址。
JMP指令,来源于“jump”,意思是“跳转”。
entry:是标签的声明,用于指定JMP指令的跳转目的地,是“入口”的意思。
MOV指令的使用次数仅次于DB指令。“MOV AX,0”,相当于“AX=0;”这样一个赋值语句。同样,“MOV SS,AX”就相当于"SS=AX;"。MOV命令源自“move”,意思是“移动”。一般说来,如果我们把一个东西移走了,它原来所占用的位置就会空出来。但是,在执行"MOV SS,AX"语句之后,AX并没有变空,还保留着原来的指不变。
现在说说AX和SS。CPU里有一种名为寄存器的存储电路,相当于机器语言中的变量。具有代表性的寄存器有以下8个。
AX:accumulator,累加寄存器 BX:base,基址寄存器 CX:counter,计数寄存器 DX:data,数据寄存器 SP:stack pointer,栈指针寄存器 BP:base pointer,基址指针寄存器 SI:source index,源变址寄存器 DI:destination index,目的变址寄存器
这些寄存器全部都是16位寄存器,因此可以存储16位的二进制数。虽然它们都有上面这种正式名称,但平常使用的时候,往往用英文字母来代替,称他们为“AX寄存器”、"SI寄存器"等。
其实寄存器的全名还是很能说明它本来的意义的。比如在这8个寄存器中,不管使用哪一个,差不多都能进行同样的计算,但如果都用AX来进行各种运算的话,程序就可以写得很简洁。
“ADD CX,0x1234”编译成81 C1 34 12,是一个4字节的命令 而 “ADD AX,0x1234”编译成05 34 12,是一个3字节的命令
从上面的例子可以看出,这里所说的“程序可以写得很简洁”是指“用机器语言写程序”的情况,从汇编语言的源代码上是看不出这些区别的。如果我们不懂机器语言,就会有很多地方难以理解。
再说说别的寄存器,CX是为方便计数而设计的,BX则适合作为计算内存地址的基点。其他的寄存器也各有优点。这8个寄存器全部合起来也才只有16个字节。
另一方面,CPU中还有8个8位寄存器。
AL:累加寄存器低位(accumulator low) CL:计数寄存器低位(counter low) DL:数据寄存器低位(data low) BL:基址寄存器低位(base low) AH:累加寄存器高位(accumulator high) CH:计数寄存器高位(counter high) DH:数据寄存器高位(data high) BH:基址寄存器高位(base high)
名字看起来有点像,其实这是有原因的:AX寄存器共有16位,其中0位到7位的低8位称为AL,而8位到15位的高位称为AH。所以,如果以为“再加上这8个8位寄存器,CPU就又可以多保存8个字节了”就大错特错,CPU还是那个CPU,依然只能存储区区16个字节。
那BP、SP、SI、DI怎么没分为“L”和“H”呢?如果无论如何都要分别取高位或低位数据的话,就必须先用“MOV,AX,SI”将SI的值赋到AX中去,然后再用AL、AH来取值。这貌似就是英特尔的设计人员的思维模式。
“喂,我家的电脑是32位的,可以不是16位。这样就能以32位为单位来处理数据了吧?那32位的寄存器在哪儿呀?”大家可能会有这样的疑问:
EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI
这些就是32位寄存器。这次的程序虽然没有用到它们,但如果想用也是完全可以使用的。在16位寄存器的名字前面加上一个E就是32位寄存器的名字了。这个字母E其实还是来源于“Extend”这个词。虽说EAX是个32位寄存器,但其实跟前面一样,它有一部分是与AX公用的,32位中的低16位就是AX,而高16位既没有名字,也没有寄存器编号。也就是说,虽然我们可以把EAX作为2个16位寄存器来用,但只有低16位用起来方便;如果我们要用高16位的话,就需要使用移位命令,把高16位移到低16位后才能用。
段寄存器,它们都是16位寄存器:
ES:附加段寄存器(extra segment)
CS:代码段寄存器(code segment)
SS:栈段寄存器(stack segment)
DS:数据段寄存器(data segment)
FS:没有名称(segment part2)
GS:没有名称(segment part3)
“MOV SI,msg”,MOV是赋值的意思:SI=msg。而msg是下面将会出现的标号,“把标号赋值给寄存器?”这到底是怎么回事?
前面我们已经看到了“JMP entry”这个指令,其实把它写成“JMP 0x7c50”也完全没有问题。本来JMP指令的基本形式就是跳转到指定的内存地址,因此这个指令就是让CPU去执行内存地址0x7c50的程序。
之所以可以用"JMP entry"来代替"JMP 0x7c50",是因为entry就是0x7c50。在汇编语言中,所有标号都仅仅是单纯的数字。每个标号对应的数字,是由汇编语言编译器根据ORG指令计算出来的。编译器计算出的“标号的地方对应的内存地址”就是那个标号的值。
所以,如果我们在这个程序中写了“MOV AX,entry”,那它就会把0x7c50代入到AX寄存器里,我们代入到AX寄存器中的就是这个简单的数字。大家可不要以为写在“entry”下面的程序也都被存储了,这是不可能的。
那么"MOV SI,msg"会怎么样呢?由于在这里msg的地址是0x7c74,所以这个指令就是把0x7c74代入到SI寄存器中去。
下面来看看"MOV AL,[SI]"。方括号代表“内存”,简单的说,它就是一个超大规模的存储单元。通过对寄存器的讲解,现在大家都知道了CPU的存储能力很差,如果我们想让CPU处理大量信息,就必须给它另外准备一套用于存储的电路。因为即便是32位的CPU,把所有普通的寄存器都加在一起,最多也只能存储32个字节的数据。就算把段寄存器也全部都用上,也才只有44字节。这么小的存储空间,就连启动电脑所必需的启动区数据都放不下。
内存并不在CPU的内部,而是在CPU的外面。所以对于CPU来说,内存实际上是外部存储器。这点很重要,就是说CPU要通过字节的一部分管脚(引线)向内存发送电信号,告诉内存:"把5678号地址的数据通过我的管脚传过来(严格说,CPU和内存之间还有称为芯片的控制单元)!"CPU向内存读写数据时,就是这样进行信息交换的。
CPU与内存之间的电信号交换,并不仅仅是为了存取数据。因为从根本上讲,程序本身也是保存在内存里的。程序一般都大于44字节,不可能保存在寄存器中,所以规定程序必须放在内存里。CPU在执行机器语言时,必须从内存一个命令一个命令地读取程序,顺序执行。
MOV指令的数据传送源和传送目的地不仅可以是寄存器或常数,也可以是内存地址。这个时候,我们就使用方括号来表示内存地址。另外,BYTE、WORD、DWORD等英文词也都是汇编语言的保留字:
MOV BYTE [678],123
这个指令是要用内存的“678”号地址来保存“123”这个数值。虽然指令里有数字,但实际上内存和CPU一样,根本就没有什么数值的概念。所谓的“678”,不过就是一大串开(ON)或者关(OFF)的电信号而已。当内存收到这一串信号是,电路中的某8个存储单元就会响应,这8个存储单元记住代表“123”的开(ON)或关“OFF”的电信号。为什么是8位呢?这是因为指令里指定了“BYTE”。同样,我们还可以写成:
MOV WORD [678],123
这种情况下,内存地址中的678号和旁边的679号都会做出反应,一共是16位。这时,123被解释成一共16位的数值,也就是0000000001111011,下位的01111011保存在678号,上位的00000000保存在旁边的679号。像这样在汇编语言里指定内存地址是,要用下面这种方式来写:
数据大小[地址]
这是一个固定的组合。如果我们指定“数据大小”为BYTE,那么使用的存储单元就是地址所指定的字节。如果我们指定“数据大小”位WORD,则与WORD相邻的一个字节也会成为这个指令的操作对象。如果指定为DWORD,则与WORD相邻的两个字节,也都成为这个指令的操作对象(共4个字节)。这里所说的相邻,指的是地址增加方向的相邻。
至于内存地址的指定方法,我们不仅可以使用常数,还可以用寄存器。比如“BYTE [SI]”、“WORD [BX]”等等。如果SI中保存的是987的话,“BYTE [SI]”就会被解释成“BYTE [987]”,即指定地址为987的内存。
虽然我们可以用寄存器来指定内存地址,但可作为此用途的寄存器非常有限,只有BX、BP、SI、DI这几个。剩下的AX、CX、DX、SP不能用来指定内存地址,这是因为CPU没有处理这种指令的电路,或者所没有表示这种处理的机器语言。没有对应的机器语言当然也就不能进行这样的处理了。所以想把DX内存里的内容复制给AL的时候,就会这样写:
MOV BX, DX
MOV AL, BYTE [BX]
根据以上说明我们知道可以用下面这个指令将SI地址的1字节内容读到AL。
MOV AL, BYTE [SI]
可是MOV指令有一个规则,那就是源数据和目的数据必须位数相同。也就是说,能向AL里代入的就只有BYTE,这样一来就可以省略BYTE,即可以写成:
MOV AL, [SI]
所以,这个指令的意思就是“把SI地址的1个字节的内容读入AL中”。ADD是加法指令,若以C语言的形式改写“ADD SI,1”的话,就是SI=SI+1。
CMP是比较指令,简单的说,它是if语句的一部分。比如C语言的“if(a==3){处理;}”即对a和3进行比较,将其翻译成机器语言是,必须先写"CMP a,3",告诉CPU比较的对象,然后下一步再写“如果二者相等,需要做什么”。
这里是“CMP AL,0”,意思就是讲AL中的值与0进行比较。这个指令源自英文中的cpmpare,意为”比较”。
JE是条件跳转指令中之一。所谓条件跳转指令,就是根据比较的结果决定跳转或不跳转。就JE指令而言,如果比较结果相等,则跳转到指定的地址;如果比较结果不等,则不跳转,继续执行下一条指令。因此:
CMP AL, 0 JE fin
这两条指令,就相当于:
if(AL == 0){ goto fin;}
这条指令源自英文"jump if equal",意思是如果相当就跳转。fin是个标号,它表示“结束(final)”的意思。
INT是软件中断指令。暂时先把它看作一个函数调用,这个指令源自"interrupt",是“中途打断”的意思。
电脑里有个名为BIOS的程序,出厂时就组装在电脑主板上的ROM单元里。电脑厂家在BIOS中预先写入了操作系统开发人员经常会用到的一些程序。BIOS是英文"basic input output system"的缩写,直译过来就是"基本输入输出系统"
最近的BIOS功能非常多,甚至包括了电脑的设定画面,不过它的本质正如其名,就是为操作系统开发人员准备的各种函数的集合。而INT就是用来调用这些函数的指令。INT额后面是个数字,使用不同的数字可以调用不同的函数。这次我们调用的是0x10(即16)号函数,它的功能是控制显卡。
比如我们现在想要显示文字,先假设一次只显示一个字,那么具体怎么做才能知道这个功能的使用方法呢?
首先,既然是要显示文字,就应该看跟显卡有关的函数。这么看来,INT 0x10好像有点关系:
显示一个字符 AH=0x0e; AL=character code; BH=0; BL=color code; 返回值:无 注:beep、退格(back space)、CR、LF都会被当做控制字符处理
所以,如果大家按照这里所写的步骤,往寄存器里代入各种值,再调用INT 0x10,就能顺利地在屏幕上显示一个字符出来。虽然BL中放入的是色彩字符码,但这里变更后,颜色并没有变。尚不清楚为什么只能显示白色,只能推测现在这个画面模式下,不能简单地指定字符颜色。
最后一个新出现的指令是HLT。这个指令很少用,它是让CPU停止动作的指令,不过并不是彻底的停止(如果要彻底停止CPU的动作,只能切断电源),而是让CPU进入待机状态。只要外部发生变化,比如按下键盘,或是移动鼠标,CPU就会醒过来,继续执行程序。
仔细看看这个程序,我们会发现其实不管有没有HLT指令,JMP fin都是无限循环,不写HLT指令也可以。但如果没有HLT指令,CPU就会不停地全力去执行JMP指令,这会是CPU符合达到100%,非常费电。最好是一开始就养成待机时使用HLT指令的习惯,HLT源自"halt",意思是“停止”。
说了这么多,终于把这个程序从头到尾都讲完了。用C语言总结一下就是这样的:
entry: AX = 0; SS = AX; SP = 0x7c00; DS = AX; ES = AX; SI = msg; putloop: AL = BYTE [SI]; SI = SI =1; if (AL == 0){goto fin;} AH = 0x0e; BX = 15; INT 0x10; goto putloop fin: HLT; goto fin;
就是有了这个程序,我们才能够把msg里写入的数据,一个字符一个字符地显示出来,并且数据变成0以后,HLT指令就会让程序进入无限循环。"hello, world"就是这样显示出来的。
ORG指令本身刚才已经讲过,但这个0x7c00又是从哪儿冒出来的呢?大家所用的内存,不是我们想怎么用就能怎么用的。比如说,内存的0号地址,也就是最开始的部分。它是BIOS程序用来实现各种不同功能的地方,如果我们随便使用的话,就会与BIOS发生冲突。结果不只是BIOS会出错,而且我们的程序也肯定会问题百出。另外,在内存的0xf0000号地址附近,还存放着BIOS程序本身,那里我们也不能使用。
0x00007c00-0x00007dff;启动区内容的装载地址
程序中ORG指令的指就是这个数字。而且正是因为我们使用的是这个同样的数字,所以程序才能正常运行。一旦有了规定,人们就会以此位前提开发各种操作系统。