自从学习操作系统开发以来,所接触到的操作系统开发资料都是关于文本模式的。然而黑色的命令行始终没有色彩斑斓的图形界面吸引眼球,所以查阅了很多资料后终于找到了真正的图形界面实现方法——VESA。
先上图:
这是一个800X600分辨率32位色(实际上是一个24位色,最高字节没用)的demo,当然你也可以把分辨率设置成1024X768或者1280X1024。
什么是VBE?
VBE的全称是VESA BIOS Extension。
什么是VESA?
VESA的全称是Video Electronics Standards Association即视频电子标准协会,是由代表来自世界各地的、享有投票权利的超过165家成员公司的董事会领导的非盈利国际组织。
VESA致力于开发、制订和促进个人计算机(PC)、工作站以及消费类电子产品的视频接口标准,为显示及显示接口业界提供及时、开放的标准,保证其通用性并鼓励创新和市场发展。
VBE视频模式
先来看看VBE的模式号及其对应的分辨率与颜色:
VBE最高可以支持1280X1024的分辨率,24位真彩色,完全可以满足我们创建图形化操作系统的需求。
下面是一张视频标准图:
只可惜VBE的标准比较老,不支持宽屏显示器。
用到的VBE函数
要实现图形模式就要用到vbe函数,vbe函数标准定义了一系列VGA ROM BIOS服务扩展。这些vbe函数可以在实模式下通过10h中断调用或者直接通过高性能的32位程序和操作系统调用。
我们的demo是通过实模式下的int 10h中断来调用VBE函数的。我们将使用以下三个函数:
功能00h – 返回控制器信息
3 | ES:DI = 指向存放VbeInfoBlock结构体的缓冲区指针 |
这个函数返回一个VbeInfoBlock结构体,该结构体定义如下:
03 | unsigned char vbe_signature; |
04 | unsigned short vbe_version; |
05 | unsigned long oem_string_ptr; |
06 | unsigned char capabilities; |
07 | unsigned long video_mode_ptr; |
08 | unsigned short total_memory; |
09 | unsigned short oem_software_rev; |
10 | unsigned long oem_vendor_name_ptr; |
11 | unsigned long oem_product_name_ptr; |
12 | unsigned long oem_product_rev_ptr; |
13 | unsigned char reserved[222]; |
14 | unsigned char oem_data[256]; |
我解释一下上面的结构体中比较重要的几个变量。
vbe_signature是VBE标识,应该填充的是”VESA”
vbe_version是VBE版本,如果是0300h则表示3.0版本
oem_string_ptr是指向oem字符串的指针,该指针是一个16位的selector:offset形式的指针,在实模式下可以直接使用。
video_mode_ptr是指向视频模式列表的指针,与oem_string_ptr类型一样
total_memory是64kb内存块的个数
oem_vendor_name_ptr是指向厂商名字符串的指针
oem_product_name_ptr是指向产品名字符串的指针
功能01 – 返回VBE模式信息
4 | ES:DI = 指向ModeInfoBlock结构体的指针 |
这个函数返回一个ModeInfoBlock结构体,该结构体定义如下:
04 | unsigned short mode_attributes; |
05 | unsigned char wina_attributes; |
06 | unsigned char winb_attributes; |
07 | unsigned short win_granularity; |
08 | unsigned short win_size; |
09 | unsigned short wina_segment; |
10 | unsigned short winb_segment; |
11 | unsigned long win_func_ptr; |
12 | unsigned short bytes_per_scan_line; |
15 | unsigned short xresolution; |
16 | unsigned short yresolution; |
17 | unsigned char xchar_size; |
18 | unsigned char ychar_size; |
19 | unsigned char number_of_planes; |
20 | unsigned char bits_per_pixel; |
21 | unsigned char number_of_banks; |
22 | unsigned char memory_model; |
23 | unsigned char bank_size; |
24 | unsigned char number_of_image_pages; |
25 | unsigned char reserved1; |
28 | unsigned char red_mask_size; |
29 | unsigned char red_field_position; |
30 | unsigned char green_mask_size; |
31 | unsigned char green_field_position; |
32 | unsigned char blue_mask_size; |
33 | unsigned char blue_field_position; |
34 | unsigned char rsvd_mask_size; |
35 | unsigned char rsvd_field_positon; |
36 | unsigned char direct_color_mode_info; |
39 | unsigned long phys_base_ptr; |
40 | unsigned long reserved2; |
41 | unsigned short reserved3; |
44 | unsigned short lin_bytes_per_scan_line; |
45 | unsigned char bnk_number_of_image_pages; |
46 | unsigned char lin_number_of_image_pages; |
47 | unsigned char lin_red_mask_size; |
48 | unsigned char lin_red_field_position; |
49 | unsigned char lin_green_mask_size; |
50 | unsigned char lin_green_field_position; |
51 | unsigned char lin_blue_mask_size; |
52 | unsigned char lin_blue_field_position; |
53 | unsigned char lin_rsvd_mask_size; |
54 | unsigned char lin_rsvd_field_position; |
55 | unsigned long max_pixel_color; |
56 | unsigned char reserved4[189]; |
解释一下几个我们要用到的比较重要的字段。
首先是mode_attributes字段,这个字段描述了图形模式的一些重要属性。其中最重要的是第4位和第7位。第4位为1表示图形模式(Graphics mode),为0表示文本模式(Text mode)。第7位为1表示线性帧缓冲模式(Linear frame buffer mode),为0表示非线性帧缓冲模式。我们主要要检查这两个位。
xresolution,表示该视频模式的X分辨率。
yresolution,表示该视频模式的Y分辨率。
bits_per_pixel,表示该视频模式每个像素所占的位数。
phys_base_ptr,这是一个非常重要的字段,它给出了平坦内存帧缓冲区的物理地址,你可以理解为显存的首地址。如果每个像素占32位的话,屏幕左上角第一个点所占的缓冲区就是phys_base_ptr所指的第一个4个字节。按照先行后列的顺序,每个像素点所占缓冲区依次紧密排列。我们要想在屏幕上画出像素点,就得操作以phys_base_ptr为起始的物理内存空间。
功能02 – 设置VBE模式
08 | D12 - D13 = 为VBE/AF保留(必须为0) |
13 | ES:DI = 指向CRTCInfoBlock结构体的指针 |
这个函数就是用来设置我们的视频模式,通过用功能01查找我们所需要模式,然后用功能02即可设置我们所需要的模式。
具体实现方法
因为我们要操作显存,对于一个1280X1024 32bit的视频模式来说,需要用到5M的内存,而我们知道在实模式下我们只能用段:位移的方式访问1M的地址空间,而我们的显存是在这1M的地址空间之外,那么我们如何才能在实模式下访问32位的地址空间呢?
这里有一种方法,那就是进入Unreal模式,Unreal模式是实模式的一个变体,在这种模式下我们的代码还是16位的实模式方式,但是我们可以使用32位的代码段来访问32位地址空间,这样我们就可以向显存中写数据来画图了。
下面是我们代码中main函数的片段:
01 | ;-------------------------------; |
03 | ;-------------------------------; |
05 | call InstallGDT ; install our GDT |
07 | ;-------------------------------; |
09 | ;-------------------------------; |
11 | call EnableA20_KKbrd_Out |
14 | ;-------------------------------; |
16 | ;-------------------------------; |
21 | ;-------------------------------; |
23 | ;-------------------------------; |
在上面的代码中,我们要进入Unreal模式首先要调用InstallGDT来安装GDT,接着调用EnableA20_KKbrd_Out来打开A20地址总线,然后调用EnterUnrealMode来进入Unreal模式。
下面是EnterUnrealMode的代码:
01 | ;========================================= |
04 | ;========================================= |
在上面的代码中,我们先暂时进入保护模式,接着将ds设为0×08,0×08是所安装的GDT的代码段的选择子。接着我们又返回到实模式,这样,处理器会使用所缓存的描述符,就像在保护模式中那样,我们就可以在实模式下访问4GB的内存空间了。
在main函数中,我们调用GetVbeInfo来获取VBE控制器的信息,下面是这个函数的代码:
01 | ;============================================= |
03 | ; get veb controller information to vbe_info_block structure |
04 | ;============================================= |
07 | mov ax, 4F00h ; Get VBE information |
08 | mov di, 0x7E00 ; Set param |
10 | ;call CheckVbeReturn ; Check return value |
11 | ;call CheckVesaVersion ; Check vesa version |
13 | call FindVideoMode ; Find Video Mode |
在这个函数中,我们会调用FindVideoMode函数来查找我们所需要的模式,并将其存放在VideoMode变量中。
下面是FindVideoMode函数的代码:
01 | ;============================================= |
03 | ;============================================= |
06 | mov si, [0x7E00 + vbe_info_block.video_mode_ptr] |
10 | mov ax, [0x7E00 + vbe_info_block.video_mode_ptr + 2] |
20 | ;mov ax, word [0x8000 + mode_info_block.xresolution] |
21 | ;mov bx, word [0x8000 + mode_info_block.yresolution] |
22 | ;mov cl, byte [0x8000 + mode_info_block.bits_per_pixel] |
23 | cmp word [0x8000 + mode_info_block.xresolution], SCREEN_WIDTH |
25 | cmp word [0x8000 + mode_info_block.yresolution], SCREEN_HEIGHT |
27 | cmp byte [0x8000 + mode_info_block.bits_per_pixel], COLOR_DEPTH |
29 | mov ax, [0x8000 + mode_info_block.mode_attributes] |
38 | ;mov si, VideoModeNotFound |
44 | ;mov si, VideoModeFound |
上面的函数是一个循环,根据VbeInfoBlock中的模式列表来查找每一个模式,调用GetVideoMode函数来获取ModeInfoBlock结构体,并将ModeInfoBlock中的字段与我们所要设置的视频模式比较,如果找到我们所需要的模式,就将其模式号储存起来。
下面是GetVideoMode函数的的代码:
01 | ;============================================= |
03 | ;============================================= |
main函数中接着调用了SetVideoMode函数,这个函数会设置我们刚刚查找到的视频模式,这样我们的视频模式就设置完了。进入到Unreal模式后我们就可以操作我们的内存并画出图像了。
下面是SetVideoMode函数的代码:
01 | ;============================================= |
03 | ;============================================= |
06 | mov cx, [VideoMode] ; video mode number |
08 | mov di, 0x8000 ; buffer to get mode information |
09 | int 10h ; get mode information |
11 | mov ax, [0x8000 + mode_info_block.mode_attributes] |
12 | and ax, 0000000000000001b ; test hardware support |
15 | ;mov si, NotSupportedInHardware |
21 | mov ax, [0x8000 + mode_info_block.mode_attributes] |
22 | and ax, 90h ; test linear frame buffer support |
25 | ;mov si, NotSupportedLinearFrameBuffer |
33 | add bx, 4000h ; LFB mode |
34 | int 10h ; set video mode |
39 | ;mov word [boot_info + multiboot_info.vbe_mode], bx |
40 | ;mov dword [boot_info + multiboot_info.vbe_mode_info], 0x8000 |
在Demo中我设置的是800X600 32位模式,每个像素占4字节,最高字节没有什么用处,第三字节是红色,第二字节是绿色,最低字节是蓝色,每个颜色值在0-255之间,总共有2的24次方种颜色。
main函数调用了VideoInit函数,这个函数的作用是初始化图形驱动,图形驱动提供了两个API来操作图形,一个是SetPixel16,它的作用是画一个点,第二个是ClearScreen16,它的作用是用指定的颜色清除屏幕。
SetPixel16代码如下:
01 | ;================================================ |
03 | ; - in ax, zero based x position |
04 | ; - in bx, zero based y position |
06 | ;================================================ |
10 | cmp ax, [VideoXResolution] |
12 | cmp bx, [VideoYResolution] |
24 | mov di, [VideoXResolution] |
27 | mul ebx ; y x VideoXResolution |
31 | add edi, eax ; y x VideoXresolution + x -> edi |
33 | mov eax, [VideoMemoryPtr] |
34 | mov dword [ds:eax + edi*4], ecx ; write color |
ClearScreen16的代码如下:
01 | ;================================================ |
04 | ;================================================ |
08 | mov edi, [VideoMemoryPtr] |
11 | mov cx, [VideoXResolution] |
14 | mov cx, [VideoYResolution] |
16 | mov dword [ds:edi + edx*4], eax |
小结
关于VBE编程的大部分内容都讲完了,要实现GUI首先要能够打开视频模式,接着你可以写一个驱动,这样你就可以调用驱动的API来画图,然后你可以实现自己的图形库,实现更加高级的图形函数,然后你就可以实现操作系统的API,这些API调用自己的图形库来画窗口,按钮等,这样就可以实现你自己的GUI了。
本文中涉及到很多的内容,如GDT、实模式、保护模式、A20地址总线等,如果读者不知道什么是GDT、A20地址总线以及如何设置GDT和打开A20地址总线等,请参考【翻译】操作系统开发系列中关于GDT和A20地址总线的部分。
上面提供了Demo和Demo的源文件,将VMWare的软盘设置为Demo中的镜像文件,并将VMWare设置为软盘启动,启动后即可看到效果。
由于512字节引导扇区的限制,Demo中的很多代码都被我注释掉了,以减少代码量,Demo中的代码编译后几乎刚好占用了所有的512字节,增加一两条指令就有可能溢出这512字节,导致无法编译。