分类:
2010-09-07 19:11:35
自从学习操作系统开发以来,所接触到的操作系统开发资料都是关于文本模式的。然而黑色的命令行始终没有色彩斑斓的图形界面吸引眼球,所以查阅了很多资料后终于找到了真正的图形界面实现方法——VESA。
先上图:
这是一个800X600分辨率32位色(实际上是一个24位色,最高字节没用)的demo,当然你也可以把分辨率设置成1024X768或者1280X1024。
VBE的全称是VESA BIOS Extension。
VESA的全称是Video Electronics Standards Association即视频电子标准协会,是由代表来自世界各地的、享有投票权利的超过165家成员公司的董事会领导的非盈利国际组织。
VESA致力于开发、制订和促进个人计算机(PC)、工作站以及消费类电子产品的视频接口标准,为显示及显示接口业界提供及时、开放的标准,保证其通用性并鼓励创新和市场发展。
先来看看VBE的模式号及其对应的分辨率与颜色:
VBE最高可以支持1280X1024的分辨率,24位真彩色,完全可以满足我们创建图形化操作系统的需求。
下面是一张视频标准图:
只可惜VBE的标准比较老,不支持宽屏显示器。
要实现图形模式就要用到vbe函数,vbe函数标准定义了一系列VGA ROM BIOS服务扩展。这些vbe函数可以在实模式下通过10h中断调用或者直接通过高性能的32位程序和操作系统调用。
我们的demo是通过实模式下的int 10h中断来调用VBE函数的。我们将使用以下三个函数:
输入:
AX = 4F00h 返回VBE控制器信息
ES:DI = 指向存放VbeInfoBlock结构体的缓冲区指针
输出:
AX = VBE返回状态
备注: 其他的寄存器被保留。
这个函数返回一个VbeInfoBlock结构体,该结构体定义如下:
// Vbe Info Block
typedef struct {
unsigned char vbe_signature;
unsigned short vbe_version;
unsigned long oem_string_ptr;
unsigned char capabilities;
unsigned long video_mode_ptr;
unsigned short total_memory;
unsigned short oem_software_rev;
unsigned long oem_vendor_name_ptr;
unsigned long oem_product_name_ptr;
unsigned long oem_product_rev_ptr;
unsigned char reserved[222];
unsigned char oem_data[256];
} VbeInfoBlock;
我解释一下上面的结构体中比较重要的几个变量。
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是指向产品名字符串的指针
输入:
AX = 4F01h 返回VBE模式信息
CX = 模式号
ES:DI = 指向ModeInfoBlock结构体的指针
输出:
AX = VBE返回状态
备注: 所有其他的寄存器保留。
这个函数返回一个ModeInfoBlock结构体,该结构体定义如下:
// Vbe Mode Info Block
typedef struct {
// Mandatory information for all VBE revisions
unsigned short mode_attributes;
unsigned char wina_attributes;
unsigned char winb_attributes;
unsigned short win_granularity;
unsigned short win_size;
unsigned short wina_segment;
unsigned short winb_segment;
unsigned long win_func_ptr;
unsigned short bytes_per_scan_line;
// Mandatory information for VBE 1.2 and above
unsigned short xresolution;
unsigned short yresolution;
unsigned char xchar_size;
unsigned char ychar_size;
unsigned char number_of_planes;
unsigned char bits_per_pixel;
unsigned char number_of_banks;
unsigned char memory_model;
unsigned char bank_size;
unsigned char number_of_image_pages;
unsigned char reserved1;
// Direct Color fields (required for direct/6 and YUV/7 memory models)
unsigned char red_mask_size;
unsigned char red_field_position;
unsigned char green_mask_size;
unsigned char green_field_position;
unsigned char blue_mask_size;
unsigned char blue_field_position;
unsigned char rsvd_mask_size;
unsigned char rsvd_field_positon;
unsigned char direct_color_mode_info;
// Mandatory information for VBE 2.0 and above
unsigned long phys_base_ptr;
unsigned long reserved2;
unsigned short reserved3;
// Mandatory information for VBE 3.0 and above
unsigned short lin_bytes_per_scan_line;
unsigned char bnk_number_of_image_pages;
unsigned char lin_number_of_image_pages;
unsigned char lin_red_mask_size;
unsigned char lin_red_field_position;
unsigned char lin_green_mask_size;
unsigned char lin_green_field_position;
unsigned char lin_blue_mask_size;
unsigned char lin_blue_field_position;
unsigned char lin_rsvd_mask_size;
unsigned char lin_rsvd_field_position;
unsigned long max_pixel_color;
unsigned char reserved4[189];
} VbeModeInfoBlock;
解释一下几个我们要用到的比较重要的字段。
首先是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为起始的物理内存空间。
输入:
AX = 4F02h 设置VBE模式
BX = 需要设置的模式
D0 - D8 = 模式号
D9 - D10 = 保留(必须为0)
D11 = 0 使用当前缺省刷新率
= 1 使用用户指定的CRTC值为刷新率
D12 - D13 = 为VBE/AF保留(必须为0)
D14 = 0 使用窗口帧缓冲区模式
= 1 使用线性/平坦帧缓冲区模式
D15 = 0 清除显示内存
= 1 不清除显示内存
ES:DI = 指向CRTCInfoBlock结构体的指针
输出:
AX = VBE返回状态
备注: 所有其他的寄存器保留
这个函数就是用来设置我们的视频模式,通过用功能01查找我们所需要模式,然后用功能02即可设置我们所需要的模式。
因为我们要操作显存,对于一个1280X1024 32bit的视频模式来说,需要用到5M的内存,而我们知道在实模式下我们只能用段:位移的方式访问1M的地址空间,而我们的显存是在这1M的地址空间之外,那么我们如何才能在实模式下访问32位的地址空间呢?
这里有一种方法,那就是进入,是实模式的一个变体,在这种模式下我们的代码还是16位的实模式方式,但是我们可以使用32位的代码段来访问32位地址空间,这样我们就可以向显存中写数据来画图了。
下面是我们代码中main函数的片段:
;-------------------------------;
; Install our GDT ;
;-------------------------------;
call InstallGDT ; install our GDT
;-------------------------------;
; Enable A20 ;
;-------------------------------;
call EnableA20_KKbrd_Out
sti
;-------------------------------;
; Init Vesa ;
;-------------------------------;
call GetVbeInfo
call SetVideoMode
call EnterUnrealMode
;-------------------------------;
; Init video ;
;-------------------------------;
call VideoInit
在上面的代码中,我们要进入首先要调用InstallGDT来安装GDT,接着调用EnableA20_KKbrd_Out来打开A20地址总线,然后调用EnterUnrealMode来进入。
下面是EnterUnrealMode的代码:
;=========================================
; EnterUnrealMode
; enter unreal mode
;=========================================
EnterUnrealMode:
cli
push ds
mov eax, cr0
or al, 1
mov cr0, eax
mov bx, 0x08
mov ds, bx
and al, 0xFE
mov cr0, eax
pop ds
sti
ret
在上面的代码中,我们先暂时进入保护模式,接着将ds设为0×08,0×08是所安装的GDT的代码段的选择子。接着我们又返回到实模式,这样,处理器会使用所缓存的描述符,就像在保护模式中那样,我们就可以在实模式下访问4GB的内存空间了。
在main函数中,我们调用GetVbeInfo来获取VBE控制器的信息,下面是这个函数的代码:
;=============================================
; GetVbeInfo
; get veb controller information to vbe_info_block structure
;=============================================
GetVbeInfo:
pushad
mov ax, 4F00h ; Get VBE information
mov di, 0x7E00 ; Set param
int 10h
;call CheckVbeReturn ; Check return value
;call CheckVesaVersion ; Check vesa version
;call ShowOEMString
call FindVideoMode ; Find Video Mode
.end:
popad
ret
在这个函数中,我们会调用FindVideoMode函数来查找我们所需要的模式,并将其存放在VideoMode变量中。
下面是FindVideoMode函数的代码:
;=============================================
; FindVideoMode
;=============================================
FindVideoMode:
pushad
mov si, [0x7E00 + vbe_info_block.video_mode_ptr]
.loop:
push ds
mov ax, [0x7E00 + vbe_info_block.video_mode_ptr + 2]
mov ds, ax
lodsw
pop ds
mov [VideoMode], ax
cmp ax, 0xFFFF
je .notfound
.getmode:
call GetVideoMode
;mov ax, word [0x8000 + mode_info_block.xresolution]
;mov bx, word [0x8000 + mode_info_block.yresolution]
;mov cl, byte [0x8000 + mode_info_block.bits_per_pixel]
cmp word [0x8000 + mode_info_block.xresolution], SCREEN_WIDTH
jne .cnt
cmp word [0x8000 + mode_info_block.yresolution], SCREEN_HEIGHT
jne .cnt
cmp byte [0x8000 + mode_info_block.bits_per_pixel], COLOR_DEPTH
JNE .cnt
mov ax, [0x8000 + mode_info_block.mode_attributes]
and ax, 90h ;LFB
cmp ax, 90h
je .found
.cnt:
jmp .loop
.notfound:
;mov si, VideoModeNotFound
;call Puts16
cli
hlt
.found:
;mov si, VideoModeFound
;call Puts16
popad
ret
上面的函数是一个循环,根据VbeInfoBlock中的模式列表来查找每一个模式,调用GetVideoMode函数来获取 ModeInfoBlock结构体,并将ModeInfoBlock中的字段与我们所要设置的视频模式比较,如果找到我们所需要的模式,就将其模式号储存 起来。
下面是GetVideoMode函数的的代码:
;=============================================
; GetVideoMode
;=============================================
GetVideoMode:
pushad
mov cx, [VideoMode]
mov ax, 0x4F01
mov di, 0x8000
int 10h
;call CheckVbeReturn
popad
ret
main函数中接着调用了SetVideoMode函数,这个函数会设置我们刚刚查找到的视频模式,这样我们的视频模式就设置完了。进入到后我们就可以操作我们的内存并画出图像了。
下面是SetVideoMode函数的代码:
;=============================================
; SetVideoMode
;=============================================
SetVideoMode:
pushad
mov cx, [VideoMode] ; video mode number
mov ax, 0x4F01
mov di, 0x8000 ; buffer to get mode information
int 10h ; get mode information
;call CheckVbeReturn
mov ax, [0x8000 + mode_info_block.mode_attributes]
and ax, 0000000000000001b ; test hardware support
cmp ax, 01h
je .testLFB
;mov si, NotSupportedInHardware
;call Puts16
cli
hlt
.testLFB:
mov ax, [0x8000 + mode_info_block.mode_attributes]
and ax, 90h ; test linear frame buffer support
cmp ax, 90h
je .ok
;mov si, NotSupportedLinearFrameBuffer
;call Puts16
cli
hlt
.ok:
mov ax, 4F02h
mov bx, [VideoMode]
add bx, 4000h ; LFB mode
int 10h ; set video mode
;call CheckVbeReturn
mov bx, [VideoMode]
;mov word [boot_info + multiboot_info.vbe_mode], bx
;mov dword [boot_info + multiboot_info.vbe_mode_info], 0x8000
popad
ret
在Demo中我设置的是800X600 32位模式,每个像素占4字节,最高字节没有什么用处,第三字节是红色,第二字节是绿色,最低字节是蓝色,每个颜色值在0-255之间,总共有2的24次方种颜色。
main函数调用了VideoInit函数,这个函数的作用是初始化图形驱动,图形驱动提供了两个API来操作图形,一个是SetPixel16,它的作用是画一个点,第二个是ClearScreen16,它的作用是用指定的颜色清除屏幕。
SetPixel16代码如下:
;================================================
; SetPixel16
; - in ax, zero based x position
; - in bx, zero based y position
; - in ecx, color
;================================================
SetPixel16:
pushad
.check:
cmp ax, [VideoXResolution]
jnb .return
cmp bx, [VideoYResolution]
jnb .return
push ax
xor eax, eax
pop ax
push bx
xor ebx, ebx
pop bx
xor edi, edi
mov di, [VideoXResolution]
push eax
mov eax, edi
mul ebx ; y x VideoXResolution
mov edi, eax ; -> edi
pop eax
add edi, eax ; y x VideoXresolution + x -> edi
mov eax, [VideoMemoryPtr]
mov dword [ds:eax + edi*4], ecx ; write color
.return:
popad
ret
ClearScreen16的代码如下:
;================================================
; ClearScreen16
; - in eax, color
;================================================
ClearScreen16:
pushad
mov edi, [VideoMemoryPtr]
mov edx, 0
mov cx, [VideoXResolution]
.L1:
push cx
mov cx, [VideoYResolution]
.L2:
mov dword [ds:edi + edx*4], eax
inc edx
loop .L2
pop cx
loop .L1
popad
ret
关于VBE编程的大部分内容都讲完了,要实现GUI首先要能够打开视频模式,接着你可以写一个驱动,这样你就可以调用驱动的API来画图,然后你可 以实现自己的图形库,实现更加高级的图形函数,然后你就可以实现操作系统的API,这些API调用自己的图形库来画窗口,按钮等,这样就可以实现你自己的 GUI了。
本文中涉及到很多的内容,如GDT、实模式、保护模式、A20地址总线等,如果读者不知道什么是GDT、A20地址总线以及如何设置GDT和打开A20地址总线等,请参考【翻译】操作系统开发系列中关于GDT和A20地址总线的部分。
上面提供了Demo和Demo的源文件,将VMWare的软盘设置为Demo中的镜像文件,并将VMWare设置为软盘启动,启动后即可看到效果。
由于512字节引导扇区的限制,Demo中的很多代码都被我注释掉了,以减少代码量,Demo中的代码编译后几乎刚好占用了所有的512字节,增加一两条指令就有可能溢出这512字节,导致无法编译。