自研OS的VESA模式代码如何在VMware/VirtualBox正常运行
问题背景
正在从零构建自研操作系统,学习VESA教程后编写了可切换VESA模式并将屏幕填充为白色的程序,其中Stage1.asm是仅负责加载执行内核的简易引导程序,核心Stage2.asm汇编代码如下:
[BITS 16] MOV AX, 800 MOV BX, 600 MOV CL, 32 CALL vbe_set_mode JMP Fill vbe_set_mode: mov [.width], ax mov [.height], bx mov [.bpp], cl sti push es mov ax, 0x4F00 mov di, vbe_info int 0x10 pop es cmp ax, 0x4F jne .error mov ax, word[vbe_info.video_modes] mov [.offset], ax mov ax, word[vbe_info.video_modes+2] mov [.segment], ax mov ax, [.segment] mov fs, ax mov si, [.offset] .find_mode: mov dx, [fs:si] add si, 2 mov [.offset], si mov [.mode], dx mov ax, 0 mov fs, ax push dx mov dx, 0xFFFF cmp [.mode], dx pop dx je .error push es mov ax, 0x4F01 mov cx, [.mode] mov di, vbe_mode_info int 0x10 pop es cmp ax, 0x4F jne .error mov ax, [.width] cmp ax, [vbe_mode_info.width] jne .next_mode mov ax, [.height] cmp ax, [vbe_mode_info.height] jne .next_mode mov al, [.bpp] cmp al, [vbe_mode_info.bpp] jne .next_mode mov ax, [.width] mov word[vbe_screen.width], ax mov ax, [.height] mov word[vbe_screen.height], ax mov eax, [vbe_mode_info.framebuffer] mov dword[vbe_screen.physical_buffer], eax mov ax, [vbe_mode_info.pitch] mov word[vbe_screen.bytes_per_line], ax mov eax, 0 mov al, [.bpp] mov byte[vbe_screen.bpp], al shr eax, 3 mov dword[vbe_screen.bytes_per_pixel], eax mov ax, [.width] shr ax, 3 dec ax mov word[vbe_screen.x_cur_max], ax mov ax, [.height] shr ax, 4 dec ax mov word[vbe_screen.y_cur_max], ax ; Set the mode push es mov ax, 0x4F02 mov bx, [.mode] or bx, 0x4000 ; enable LFB mov di, 0 ; not sure if some BIOSes need this... anyway it doesn't hurt int 0x10 pop es cmp ax, 0x4F jne .error clc ret .next_mode: mov ax, [.segment] mov fs, ax mov si, [.offset] jmp .find_mode .error: LEA SI, MsgNoVESA CALL Print jmp $ .width dw 0 .height dw 0 .bpp db 0 .segment dw 0 .offset dw 0 .mode dw 0 vbe_screen: .width dw 0 .height dw 0 .bpp dw 0 .physical_buffer db 0 .bytes_per_pixel dw 0 .bytes_per_line dw 0 .x_cur_max dw 0 .y_cur_max dw 0 vbe_info: .signature db "VESA" .version dw 0 .oem dd 0 .capabilities dd 0 .video_modes dd 0 .video_memory dw 0 .software_rev dw 0 .vendor dd 0 .product_name dd 0 .product_rev dd 0 .reserved db 222 dup 0 .oem_data db 256 dup 0 vbe_mode_info: .attributes dw 0 .window_a db 0 .window_b db 0 .granularity dw 0 .window_size dw 0 .segment_a dw 0 .segment_b dw 0 .win_func_ptr dd 0 .pitch dw 0 .width dw 0 .height dw 0 .w_char db 0 .y_char db 0 .planes db 0 .bpp db 0 .banks db 0 .memory_model db 0 .bank_size db 0 .image_pages db 0 .reserved0 db 0 .red_mask db 0 .red_position db 0 .green_mask db 0 .green_position db 0 .blue_mask db 0 .blue_position db 0 .reserved_mask db 0 .reserved_position db 0 .direct_color_attributes db 0 .framebuffer dd 0 .off_screen_mem_off dd 0 .off_screen_mem_size dw 0 .reserved1 db 206 dup 0 Fill: MOV EBX, [vbe_mode_info.framebuffer] MOV EAX, [vbe_mode_info.framebuffer] ADD EAX, 800 * 600 * 4 MOV ECX, 0x00FFFFFF MOV EDX, 0 FillLoop: MOV [EBX], ECX ADD EBX, 4 CMP EBX, EAX JA EndFunc JMP FillLoop EndFunc: CLI HLT MsgNoVESA: db "This PC is not support VESA.", 0x00
问题现象
程序在QEMU环境可正常运行,在VMware、VirtualBox环境中无法生效。
故障原因
- VBE中断调用段寄存器配置错误:代码调用
int 0x10实现VBE功能(0x4F00读VBE信息、0x4F01读模式信息)时,只做了es寄存器的入栈出栈,根本没给es赋值指向信息结构体所在的内存段。QEMU的BIOS校验很松,默认es的值刚好和放结构体的段对上,能正常写数据;但VMware、VirtualBox的VBE实现完全按规范来,es:di不对就把VBE信息写到随机内存位置,后面读出来的分辨率、帧缓冲地址全是垃圾值,模式自然切不成。 - 结构体定义不符合VBE规范:存在两个明显错误,一是
vbe_screen里存帧缓冲地址的physical_buffer定义成了db 0,只留了1字节空间,但32位的帧缓冲地址要占4字节,写的时候直接把后面相邻的内存变量冲了;二是vbe_mode_info结构体末尾的预留区只分了206字节,算下来整个结构体比VBE要求的256字节少2字节,读framebuffer这类靠偏移取值的字段时,直接读错位置。 - 模式匹配缺必要校验:遍历VBE模式列表时,只判断宽高和色深对不对,完全没检查模式属性。VMware、VirtualBox返回的模式里,同参数的可能是不支持线性帧缓冲的老式banked模式,甚至是没开放的保留模式,直接给这种模式加
0x4000标志要开LFB,BIOS直接返回失败。 - 填屏逻辑硬编码参数:填白色的时候直接写死按
800*600*4算帧缓冲长度,根本没用VBE返回的pitch(每行实际占的字节数)。绝大多数虚拟机的VBE模式都会给行字节数做内存对齐,pitch比宽度*每像素字节数大,硬编码长度要么写越界碰了别的内存,要么没填完整个屏幕,甚至直接触发异常。 - 中断状态不对:进
vbe_set_mode就直接sti开中断,不少虚拟机的BIOS VBE服务要求调用时关中断,中途被中断打断的话,BIOS直接返回错误码。
修复方案
- 补全VBE调用时的段寄存器赋值:每次调VBE中断前,明确把
es设为0(实模式下这些结构体都在段基址0的平坦地址空间里),比如调0x4F00的时候要写成:
push es mov ax, 0 mov es, ax mov ax, 0x4F00 mov di, vbe_info int 0x10 pop es
调0x4F01、0x4F02的时候也做一样的es赋值,别光push不赋值。
- 修正结构体错误:把
vbe_screen.physical_buffer改成dd 0,留够4字节存32位地址;把vbe_mode_info末尾的reserved1改成db 208 dup(0),凑够256字节的规范要求长度。 - 加模式属性校验:找到宽高、色深匹配的模式后,先查属性位,不符合就直接跳过找下一个:
; 校验模式是否可用、是否支持线性帧缓冲 test word[vbe_mode_info.attributes], 0x90 jnz .next_mode
这里属性位bit0为1代表模式当前可用,bit7为1代表支持线性帧缓冲,两个位都满足才是需要的模式。
- 改填屏逻辑,别硬编码参数:用VBE返回的pitch算帧缓冲总长度,不要写死分辨率和每像素字节数:
mov ebx, [vbe_screen.physical_buffer] xor eax, eax mov ax, [vbe_screen.bytes_per_line] mul word[vbe_screen.height] add eax, ebx ; 算出来eax就是帧缓冲的结束地址 mov ecx, 0x00FFFFFF ; 32位色下的白色值 .fill_loop: mov [ebx], ecx add ebx, 4 cmp ebx, eax jb .fill_loop
- 调整中断开关:把
vbe_set_mode开头的sti删掉,整个VBE设置过程关中断跑,等模式切完、填完屏再按需开中断就行。
内容的提问来源于stack exchange,提问作者Nine
相关产品推荐
相关产品推荐

