v0.0.2的项目地址如下:
github.com地址:
https://github.com/zenglong/zenglOX (只包含git提交上来的源代码)
Dropbox地址:
点此进入Dropbox网盘 (该版本位于zenglOX_v0.0.2的文件夹,该文件夹里的zip压缩包为源代码,PDF文档为VGA的编程手册)
sourceforge地址:
https://sourceforge.net/projects/zenglox/files (该版本位于zenglOX_v0.0.2的文件夹,该文件夹里的zip压缩包为源代码,PDF文档为VGA的编程手册)
源代码编译调试方法请参考 v0.0.1的文章。
之前的v0.0.1版本什么输出也没有,在bochs下还可以通过gdb远程调试,知道内核在做些什么,但是放在virtualbox里就不知道内核在做什么了,所以如果能让内核在屏幕上输出显示一些字符串的话,对内核的调试开发都会有很大的帮助。
不过,控制显卡的输出显示可以说是内核开发的另一个难点,它需要有专门的驱动程序来做,如果你想自己做这样的驱动程序的话,也会遇到很多的困难,比如,如何调整屏幕的分辨率,如何访问修改屏幕上的某个像素,如何操作显卡里的寄存器等等。
庆幸的是,大部分显卡都是VGA兼容的,VGA是Video Graphics Array(视频图形阵列)的首字母缩写,是IBM于1987年提出的一个使用模拟信号的计算机显示标准。这个标准已对于现今的个人计算机市场已经十分过时。即使如此,VGA仍然是最多制造商所共同支持的一个标准,个人计算机在加载自己的专门的驱动程序之前,都必须支持VGA的标准。例如,微软Windows系列产品的开机画面仍然使用的是VGA显示模式。
zenglOX是由GRUB来启动加载的,GRUB已经自动帮我们设置为VGA的彩色文本模式了,在该文本模式下,VGA的显存已通过硬件映射到我们可以访问到的线性地址空间里了,所以内核启动后,就可以直接像操作普通内存数据一样,操作VGA的显存了,在显存里设置的内容都会及时显示到屏幕上。
在VGA彩色文本模式下,显存的起始地址是0xB8000,该模式下,VGA可以在屏幕上显示输出80 x 25个字符,也就是可以显示25行,每行显示80个字符,其中的每个字符都由显存里的两个字节组成,较低的字节存储字符的ASCII码,较高字节存储字符的前景色和背景色,如下图所示:
图1
例如0xB8000对应的字节里包含的是屏幕左上角第一个字符的ASCII码,0xB8001对应的字节里,低4位里存储的是该字符的前景色,高4位里存储的是背景色,0xB8002里存储第二个字符,0xB8003里存储第二个字符的前景和背景色,以此类推。
由于前景色和背景色的二进制位数是4,所以可以表示16种不同的前景色,和16种不同的背景色,如下所示:
-
0表示black(黑色)
-
1表示blue(蓝色)
-
2表示green(绿色)
-
3表示cyan(青色)
-
4表示red(红色)
-
5表示magenta(品红色)
-
6表示brown(棕色)
-
7表示light grey(浅灰色)
-
8表示dark grey(深灰色)
-
9表示light blue(淡蓝色)
-
10表示light green(葱绿色或品绿色)
-
11表示light cyan(淡青色)
-
12表示light red(浅红色)
-
13表示light magenta(淡洋红色)
-
14表示light brown(浅棕色)
-
15表示white(白色)
所以我们只要对显存进行操作,就可以输出指定的字符串信息,甚至可以输出带颜色的字符串。
在屏幕上面,设置光标的位置也很重要,不过光标的位置设置涉及到VGA的寄存器,所以有必要先对VGA的寄存器做些介绍,在上面Dropbox和sourceforge项目地址里v0.0.2对应的文件夹里,我上传了一个
EGA_VGA_Program_Manual.pdf文档,该文档是翻译过的EGA与VGA的编程手册,里面介绍了EGA和VGA的编程原理,VGA是兼容EGA的,所以对于EGA的介绍同样适用于VGA,在该pdf文档的第44页,即
第三章 EGA的寄存器篇,就详细介绍了EGA与VGA里可用的寄存器。
对于外围设备,我们一般是通过I/O地址来访问这些设备里的寄存器的,但是,EGA里的寄存器数量很多,有将近六十个寄存器,VGA里的寄存器比EGA的还多,为了避免占用太多的处理器I/O地址,EGA的寄存器被复用为少量的I/O地址。在多数情况下,寄存器的存取需要两步完成。先通过一个I/O端口选择一个寄存器,再通过另一个I/O端口来读或写数据。
下面是EGA与VGA在彩色模式下的I/O地址映射:
图2
EGA与VGA的I/O地址按功能作了逻辑划分。CRT控制器,图形控制器,属性控制器和时序发生器都有各自的地址集。
在zenglOX v0.0.2里控制光标位置,主要用到CRT控制器,CRT控制器里也有很多寄存器,每个寄存器都有自己的索引值,从上图可以看出CRT控制器使用两个I/O地址,第一个地址3D4用于设置需要访问的CRT寄存器的索引,第二个地址用于从选中的寄存器里读数据或写数据到选中的寄存器。
下面显示的是一部分CRT里的寄存器,完整的列表请查看EGA_VGA_Program_Manual.pdf文档的第50页:
图3
上面的实心圆表示修改该寄存器会有危险,五角星表示该寄存器特别有用。(CRT控制器的许多寄存器不应由软件修改。和控制定时有关的某些寄存器如果被不小心修改的话,实际上会毁坏CRT显示器)
从上图可以看到,修改光标位置,需要用到CRT里索引值为E和索引值为F的两个寄存器。所以如果要设置光标位置的高字节,则需要首先通过out指令向CRT的3D4的I/O地址写入索引值E,表示选中该寄存器,然后再通过out指令向3D5的I/O地址写入字节值即可,如果要读取光标位置的高字节,则在选中寄存器的情况下,用in指令从3D5的I/O地址读取数据即可。
在v0.0.2版本zlox_monitor.c文件里的zlox_move_cursor函数,就是利用上面的原理通过读写I/O地址,来修改光标位置的:
// Updates the hardware cursor.
static ZLOX_VOID zlox_move_cursor()
{
// The screen is 80 characters wide...
ZLOX_UINT16 cursorLocation = cursor_y * 80 + cursor_x;
//Tell the VGA board we are setting the high cursor byte.
zlox_outb(0x3D4, 14);
// Send the high cursor byte.
zlox_outb(0x3D5, cursorLocation >> 8);
// Tell the VGA board we are setting the low cursor byte.
zlox_outb(0x3D4, 15);
// Send the low cursor byte.
zlox_outb(0x3D5, cursorLocation);
} |
上面在zlox_move_cursor函数里,先根据cursor_x和cursor_y即光标的X和Y坐标,通过计算得到需要设置的cursorLocation光标位置,该位置是和显存里的字符位置相对应的,只不过它是从0开始。
zlox_outb(0x3D4, 14);函数表示向0x3D4的I/O地址写入14(对应十六进制0xE)的索引值,表示选中CRT里光标位置高字节的寄存器,然后通过zlox_outb(0x3D5, cursorLocation >> 8);函数将cursorLocation的高字节写入0x3D5的I/O地址,这样数据就会自动写入前面选中的寄存器。
同理,通过zlox_outb(0x3D4, 15);函数选中索引值为15(对应十六进制0xF)的光标位置低字节的寄存器,然后通过zlox_outb(0x3D5, cursorLocation);函数将cursorLocation的低字节值写入该选中的寄存器。
这样就可以将屏幕上的光标位置设置到我们需要显示的位置了。
上面的zlox_move_cursor函数里用到了zlox_outb函数,该函数定义在zlox_common.c文件里:
// Write a byte out to the specified port.
ZLOX_VOID zlox_outb(ZLOX_UINT16 port,ZLOX_UINT8 value)
{
asm volatile("outb %1,%0"::"dN" (port),"a" (value));
//asm volatile("outb %1,%0"::"d" (port),"a" (value));
//asm volatile("outb %%al,%%dx"::"d" (port),"a" (value));
//asm volatile("outb %%al,(%%dx)"::"d" (port),"a" (value));
} |
zlox_outb函数里通过
asm volatile内嵌了一段out汇编指令,%1表示第二个参数value,该参数因为有
"a"修饰,所以值会被存储在al寄存器里,port对应的I/O地址端口号则会被设置在dx寄存器里,out指令的格式如下:
Opcode |
Mnemonic |
Description |
E6 ib |
OUT imm8, AL |
将AL里的字节值输出到imm8对应的I/O地址 |
E7 ib |
OUT imm8, AX |
将AX里的字(2个字节)值输出到
imm8对应的I/O地址 |
E7 ib |
OUT imm8, EAX |
将EAX里的双字(4个字节)值输出到
imm8对应的I/O地址 |
EE |
OUT DX, AL |
将AL里的字节值输出到dx寄存器对应的I/O地址 |
EF |
OUT DX, AX |
将AX里的字(2个字节)值输出到
dx寄存器对应的I/O地址 |
EF |
OUT DX, EAX |
将EAX里的双字(4个字节)值输出到
dx寄存器对应的I/O地址 |
|
上面是Intel的语法,GNU使用的AT&T语法里源操作数和目标操作数的位置,与Intel语法里的位置刚好相反,上面的imm8表示8位的立即数,它只能表示0到255的I/O地址,DX寄存器则可以表示0到65535的I/O地址。从上表可以看出,OUT指令只能使用AX和DX寄存器。IN指令也只能使用AX和DX寄存器,只是IN指令的源操作数和目标操作数的位置与OUT指令刚好相反,表示从某I/O端口进行读操作。
在上面zlox_outb的函数定义里,有几个注释,使用这几个注释里的任意一条代码,得到的结果都是一样的,说明取消dN旁边的N修饰符对结果没有影响,因为N修饰符主要用于限制立即数范围在0到255之间,但是DX寄存器不受这个限制,所以dN去掉右侧的N也是一样的,还可以直接使用outb %%al,%%dx或outb %%al,(%%dx)的指令。
在zlox_monitor.c文件里除了上面介绍的控制光标位置的函数外,其余的函数用于向显存输出字符串信息,在该文件的开头设置了一个全局变量video_memory = (ZLOX_UINT16 *)0xB8000;该变量被赋值为0xB8000即显存的起始位置,这样,后面的代码就可以像操作数组一样操作显存了。
在zlox_kernel.c文件里通过zlox_monitor.c里的函数向屏幕输出显示了一段字符串信息:
/*zlox_kernel.c Defines the C-code kernel entry point, calls initialisation routines.*/
#include "zlox_monitor.h"
//zenglOX kernel main entry
ZLOX_SINT32 zlox_kernel_main(ZLOX_VOID * mboot_ptr)
{
// Initialise the screen (by clearing it)
zlox_monitor_clear();
// Write out a sample string
zlox_monitor_write("hello world!\nwelcome to zenglOX!");
return 0;
}
|
下面是bochs里的显示效果:
图4
上面主要是对VGA显示输出字符串的原理做个介绍,至于具体的代码请参考源文件,或参考github.com上有关该版本的diff信息。
OK,就到这里,休息,休息一下 o(∩_∩)o~~