第四节 Hello,World
这个时间是你期待的,终于我们等到了这个经典的“第一”程序。每本正统的程序设计的指导书都有一个“Hello,World”程序,现在我们有足够的知识实现这个“Hello,World”操作系统。如果你已经自己做过试验,实现了这个功能,你可以跳过这节。我们将要写一个显示字符的函数并用它来显示我们的信息。
在屏幕上一个一个字节的显示字符是枯燥的,我们将建立一个函数来在屏幕显示以0 结尾的字符串。它只是利用一个简单的循环来实现一次打印所有的字符。
; ---------------------------------------------
; Print a null-terminated string on the screen
; ---------------------------------------------
putstr:
lodsb ; AL = [DS:SI]
or al, al ; Set zero flag if al=0
jz putstrd ; jump to putstrd if zero flag is set
mov ah, 0x0e ; video function 0Eh (print char)
mov bx, 0x0007 ; color
int 0x10
jmp putstr
putstrd:
retn
现在,我们来使用这个函数。首先我们必须把字符串中第一个字节的地址传到寄存器SI中,然后调用子程序 putstr
你可以在你的汇编文件中创建一个字符串
msg db 'Hello, World!', 0
0 在字符串的结尾。然后你可以用下面的指令把这个字符串打印到屏幕上。
mov si, msg ; Load address of message
call putstr ; Print the message
在它工作以前,我们还需要做些事情。被载入SI寄存器的msg地址,实际上相对于段寄存器DS的偏移地址。所以,在你使用msg的地址前,你必须设置这个正确的数据段地址。现在,我们使用的是从物理内存地步开始的平坦物理地址。需要设置数据段的开始地址为物理内存的底部,即设置DS寄存器值为0。下面的指令完成这个过程.
xor ax, ax ; Zero out ax
mov ds, ax ; Set data segment to base of RAM
尝试把这些内容加到第三节的文件h.asm中,然后,用同样的方法,编译你的程序,并且把它拷贝到你的软盘中,启动它。多好玩!如果你失败了,你可以看以下我的解决方案:helowrld.asm,但应该等到你实在无法实现的时候再看它。
完成了这个,翻到下一节,我们将要学习如何去和操作系统交互。
第五节 和操作系统交互
打印字符到屏幕上是有趣的,但是没有操作系统在没有实现任何交互操作的情况下能运行好一切。我们来实现让它从键盘读取字符,我们将调用BIOS功能来读取键盘字符。
我们将要使用中断0x16的功能0。使用它是如此简单,仅用下面的两条指令:
xor ah, ah ; we want function zero
int 0x16 ; wait for a keypress
这个功能导致计算机中断,直到有一个键被按下,才返回。这个功能,可以被用到“Press any key to continue”的情形下,或者需要得到用户输入的情况。键盘按下的扫描码被存到AH寄存器,而ASCII码被存到AL寄存器。
这节你的任务去写一个简单的引导程序来示范交互,你可以实现在每次按下键的时候显示一条信息,或者是把用户输入的每个字符回显到屏幕上。
如果你感到困难,这儿有一个我写的例子(example)。但建议你在自己尽力以前不要去看它。
下一节,我们将要学习,如何使我们的操作系统大于单个引导扇区的方法。
第六节 引导载入
到现在,我们所做的一切使把真个内容放到一个引导扇区中,但这样的话,我们无法使我们的操作系统足够的大。我们需要扩展的方法,下面我们将要通过引导程序从磁盘上加载一个可执行文件并执行它。这就是所谓的引导载入,被加载到磁盘的文件可以足够大,再不受一个扇区的限制。
这个比我们先前所作的都困难,去找一些关于FAT格式文件的资料是一个不错的主意(或者是你选择的文件系统,但我将使用FAT格式讲解)。我先对引导载入程序做一个概述。
一个软盘中的内容,按顺序排的话是:DOS引导记录(我们已经操作过的第一扇区),文件配置表(FAT),根目录,然后是保存在这个盘上的数据。(硬盘的格式更复杂,它拥有一个主引导区和多个分区)。假定我们已经写了一个操作系统,把它编译成一个文件LOADER.BIN,并且把它存到磁盘上。引导加载程序将按下面的方式加载它。
1, Dos引导区(DBR)去判断DBR,FAT和根目录的大小,并检测它们所在的位置。
2, 根目录被读到内存。
3, 在根目录中查找文件LOADER.BIN,如果找到,我们查找目录项找到这个文件的第一簇(文件配置单元)。如果找不到,给出一个错误信息。
4, 文件分配表被读到内存中。
5, 通过文件的第一个簇,我们利用FAT去查找属于这个文件的所有簇。我们把这些所有的内容读取到内存中的一个特殊位置。
6, 我们跳转到哪个位置,开始执行一个操作系统。
从磁盘上读取内容的所有过程都是通过BIOS来实现的,如果你喜欢冒险,找一个关于BIOS功能的指导材料,试着写自己的引导加载程序。另外,我提供了一个对 John S. Fine的FAT12 bootstrap loader稍加修改的版本。如果你能找到他的”partcopy”的拷贝,使用它的编译器和安装指令(记得告诉我哪儿可以找到它)。或者,你可以领用前面章节的知识把我修改后的版本拷贝到软盘。
John Fine的引导加载程序中,有很多用户可以调整的设置。它的加载程序假定用户使用的是FAT12文件系统(软盘使用的文件系统)。对于不同的系统,你可能需要不同的加载方式,你可以通过改变这些设置来加载不同格式的文件系统,你也可以调整要加载的操作系统的名字。
在缺省情况下,加载器要把根目录下的一个名为Loader.bin的文件加载到内存地址,0x1000:0000(这个地址可以通过%define IMAGE_SEG来修改)。这样你需要编译一个名为Loder.bin的操作系统,并把它放到软盘的根目录。
作为一个例子,我们会使用第四节所做的hello world操作系统来作为我们要启动的对象。但我们不能直接使用它,需要做一些小的修改。首先,它会被引导到不同的地址空间(用0x1000:0000 代替0000:7C00)。第二,我们需要去除DOS引导区的数据。
我们需要设置数据段和堆栈段的基地址,并设置堆栈的指针。如下所示。现在代码段的基址在CS寄存器中,代码中需要的静态数据也在这个段内,所以把数据段基址也设置成和CS一样。同样段基址也用它,不过我们将来会对它进行修改。
mov ax, cs ; Get the current segment
mov ds, ax ; The data is in this segment
cli ; disable interrupts while changing stack
mov ss, ax ; We'll use this segment for the stack too
mov sp, 0xfffe ; Start the stack at the top of the segment
sti ; Reenable interrupts
最后,我们去除代码最低端检测代码是否超过1个扇区,并为扇区补充内容和增加结束标志的代码。其它代码是一样的。你可以在这儿下载这个程序lesson6.asm.。
使用下面的命令编译并且把它拷贝到软盘:
nasmw lesson6.asm –o lesson6.bin
copy lesson6.bin a:\LOADER.BIN
然后,如果你已经加载了引导加载程序,你就可以用它做引导盘。一旦你完成了这个工作,你就发现去改变你前面几节写的程序是如此的自由。以下的大多数章节都假定你使用这个加载器或者其它加载器加载你的操作系统文件。
现在,我们使我们的操作系统超过了一个扇区。
第七节 向BIOS说再见
现在我们拥有一个启动加载器,可以加载我们的操作系统,我们的操作系统超过了一个扇区,现在我们可以给我们的操作系统增加一些复杂的功能。我们要做的第一件事事摆脱BIOS的束缚。此前,我们利用BIOS实现输入输出功能。BIOS通过它的标准接口隐藏了实现的细节,我们无法确切的知道它如何实现自己的功能。使用BIOS,要比我们直接操纵I/O接口慢,而且我们使用I/O接口,可以知道设备运行的确切信息,并且在设计操作系统的时候会有得到更强大,更易控制,更富弹性的支持。BIOS的优点是它拥有统一的接口,不会随着机器的不同而改变;而另一方面,BIOS只能在实模式中使用,当我们的操作系统切换到保护模式的时候,BIOS就无能为力了。我直接操纵BIOS的原因是,我能知道它如何运行。如果有认对用操纵I/O来代替BIOS,存在疑问的话,可以联系我,我们可能会有更深入的探讨。
我们将从在屏幕上显示文字开始。先前我们使用BIOS的中断号0x10的0x0E来实现,现在我们自己操作显示。开始这个操作之,我们需要知道一些东西。首先,视频的内存被映射到主内存的开始地址是0xB0000(平坦物理地址)。控制屏幕单色显示的开始地址是0xB0000,彩色显示的是0xB8000。先试着使用彩色显示,如果失败的话,再尝试前者。我先假定使用彩色显示。
第一个字节(0xB8000)保存着屏幕左上角的字符的ASCII码,下面的字节(0xB8001)保存着这个字符的颜色和风格。紧接着的两个地址保存着下一个字符(右边)的信息。视频内存在一行结束,保存着分割的ASCII码和它的风格。再下面的内存保存着第二行的信息,如此直到整个屏幕。
所以,在屏幕上显示字符所要做的就是把这个ASCII码写到内存中的指定区域。(可以参看内存映射的指导)。当然,你还要跟踪光标的位置。
说说光标:你可以在屏幕上的任何位置写字符,但是你写的字符不在光标闪动的地方,看起来会很奇怪。视频控制器(在IBM PC中是6845芯片)负责在屏幕上绘制光标,我们通过它来控制光标。
6845视频控制器通过端口0x3B4-0x3B5来控制显示,通过0x3D4-0x3D5来控制颜色显示。另外,我可以告诉你,6845可以拥有不同的寄存器:假定你使用彩色显示,0x3D4指出向哪个端口输出,然后我们向0x3D5写入数据。寄存器14-15将告诉6845在哪儿画闪动的光标。下面是光标移动的伪码:
outbyte(0x3D4, 14); // write to register 14 first
outbyte(0x3D5, (cursorpos>>8) & 0xFF); // output high byte
outbyte(0x3D4, 15); // again to register 15
outbyte(0x3D5, cursorpos & 0xFF); // low byte in this register
光标的位置(cousorpos代替)是一个数字,开始是0,按顺序排列所有在视频内存中的字符。(给定光标位置后,视频内存的ASCII码位置是 cursorpos*2,字符颜色风格的位置是cursorpos*2 + 1 )。
现在的任务是使用I/O端口显示光标位置,通过视频内存来输出字符,来为你的操作系统实现你的字符驱动程序。实现一系列的函数来在屏幕上显示字符,字符串,数字,而且没有使用BIOS。确定你的字符驱动程序在光标到达低部的时候可以实现滚屏,并且实现一个清屏函数。
实现这些功能后,操作系统允许用户通过键盘敲入字符,并且把他键入的字符显示在屏幕上。如果在完成你的文本驱动程序有困难的话,你可以告诉我,我能给你一些指导。
另外对于颜色风格控制,你可以使用07(一直是黑色),但如果你很好奇,我来解释不同的颜色风格设置。一个字符的颜色风格设置是一个自家。这八位按下面的方式使用。
Bits 3-0 : 前景颜色
Bit
3
2
1
0
颜色
亮度
红
绿
篮
这四位可以有16种组合,位3是1的话,代表全亮度,0代表半亮度。例如 : 3代表的是蓝绿色(篮+绿),而11代表的是亮蓝绿色(亮度+蓝+绿)。
位5:视频反白。
位6:背景颜色,颜色值来自位3-0
位7:文字闪烁。
例如 0x2C (00101100)将要把屏幕底色设置成亮红色。