DOS编程:EGA BGI驱动反汇编分析与DJGPP迁移方案
Contents
背景
Borland 时代 TC(Turbo C)系列的图形库依赖 BGI(Borland Graphics Interface)驱动文件(.BGI)实现跨显卡支持。这套机制在 20 世纪 80 年代是相当超前的——本质上就是"显卡驱动 + 图形框架接口合体"的架构,允许开发者在不改动图形库源码的前提下,通过替换不同的驱动文件来适配各种硬件。
然而,这些 BGI 文件包含的是 16 位实模式 x86 机器码,在现代环境(DJGPP 32 位保护模式、VESA VBE 高分辨率显示)上无法直接运行。我们有几千行基于 TC3 的老程序要迁移到 DJGPP + 高分辨率 VESA 模式,没有办法只能理解 BGI 驱动的底层设计思想,重新实现一套兼容接口。
这篇文章通过反汇编 EGAVGA.BGI 驱动文件,揭示其硬件交互细节,为后续的现代化兼容层实现奠定基础。
EGAVGA.BGI 文件结构
EGAVGA.BGI 的文件头包含了驱动的元数据和对外暴露的函数表。完整的二进制解析结果如下:
|
|
这段 banner 本身很有意思——pk\x08\x08 这个魔数序列是 Borland 的私有标记,用来识别 BGI 驱动文件。\x08\x08 对应了两个"退格"字符,可能是古早工具的产物。
文件的实质是一段可重定位的 16 位实模式机器码,包含:
- 数据段(驱动状态、显示模式表、颜色调色板等)
- 代码段(初始化、硬件访问、像素绘制等函数)
- 导出函数表(图形库运行时通过中断或远调用跳转到这些函数)
BIOS 中断调用分析
BGI 驱动对硬件的初始化和模式切换全部通过 INT 10h(视频服务中断)实现。反汇编总共发现了 5 次 INT 10h 调用,按执行顺序和用途分类如下:
| 位置 | 功能码 (AH/AL) | 功能说明 | 用途 |
|---|---|---|---|
| 0x14b | AH=5 | Select Active Display Page | 选择显示页码(用于页交换动画) |
| 0x1bb | AH=0x10, AL=0x00 | Set Individual Palette Register | 探测/初始化调色板(EGA/VGA 通用) |
| 0x1c9 | AH=0x10, AL=0x10 | Set Individual DAC Register | VGA 专属功能,硬件探测(EGA vs VGA 区分) |
| 0x3da | AH=0x10, AL=0 | Set Palette Register | 正式设置调色板,参数来自驱动内部数据表 |
| 0x636 | AH=0, AL=[0x4e5] | Set Video Mode | 关键调用:切换到显示模式,模式号从驱动数据表读取(0x10 = EGA 640x350 16色) |
硬件探测逻辑
驱动在初始化时先后调用了两个不同的"设置调色板"子功能:
- AL=0x00:EGA/VGA 都支持
- AL=0x10:仅 VGA 支持
通过这个差异,驱动可以在运行时识别当前显卡是 EGA 还是 VGA。这是 1980 年代的经典检测手法——有些硬件特性不是通过状态寄存器读取,而是通过"尝试调用,不出错就说明支持"来反推的。
显卡寄存器直接操作
一旦进入图形模式,BIOS 太慢了(每次中断有几千个 CPU 周期的开销)。驱动改为直接操作 EGA/VGA 的 Graphics Controller 寄存器,绕过 BIOS。
这是关键部分。EGA/VGA 的像素操作采用了一套复杂的位面寄存器系统(planar memory architecture),4 个 1-bit 位面分别存储红(R)、绿(G)、蓝(B)、亮度(I)四个分量,8 个像素打包成一个字节。
关键寄存器组(Graphics Controller,I/O 端口 0x3CE/0x3CF)
| 寄存器索引 | 名称 | 反汇编中的用法 |
|---|---|---|
| 0 | Set/Reset | 预置要写入的颜色值 |
| 1 | Enable Set/Reset | 指定哪些位面参与 Set/Reset(通常设为 0x0F = 全 4 位面) |
| 5 | Mode Register | 切换写模式(0/1/2),影响 latch 电路的工作方式 |
| 8 | Bit Mask | 掩码,指定字节内的哪些比特位会被写入 |
putpixel(画单像素)的硬件级实现
驱动的画点函数遵循教科书式的 EGA latch 读改写算法:
|
|
Latch 电路详解
Latch 是 EGA/VGA 显卡上的一个硬件缓冲电路,用来加速位面像素的读改写操作。它的工作原理:
-
读取时:当 CPU 从显存地址读取一个字节时,硬件会同时从 4 个位面(R/G/B/I)的同一偏移地址读出 4 个字节,存入 latch 寄存器的 4 个 latch 单元中,但只把其中一个(由 Read Plane Select 寄存器选定的位面)返回给 CPU。这样 4 个位面的数据都被"锁存"住了,不会被后续操作破坏。
-
写入时:Bit Mask 寄存器指定了本次写操作只影响哪些比特位。硬件根据 Set/Reset 寄存器的值和 Enable Set/Reset 寄存器的掩码,把 latch 中已有的数据和新值通过逻辑电路混合,最后只把 Bit Mask 选中的位写回显存。
为什么这套电路这么关键?
考虑画一个像素的场景:一个字节存 8 个像素,如果要改其中一个像素的颜色,传统做法是:
- ① CPU 读字节
- ② CPU 在寄存器中修改那一位
- ③ CPU 写回字节
这需要 3 步操作,而且读出的数据可能在读和写之间被其他设备(DMA、刷新电路)改掉(竞态条件)。
EGA latch 的方案是:
- ① CPU 读字节 → latch 同时锁存 4 个位面的数据
- ② 硬件根据 Set/Reset、Bit Mask 等寄存器设置直接在 latch 内部修改
- ③ CPU 写字节 → 使用 latch 中已修改过的数据
这样只需 2 步 CPU 操作,且修改发生在硬件逻辑内,不受其他设备干扰。在 CPU 频率远低于现在的年代,这是一种关键的性能优化。
这套算法的巧妙之处在于:利用硬件 latch 电路,只用一次读+一次写就能改单个比特,而不需要从内存读出整字节、手工修改某一位、再写回(如果没有 latch,需要 3 步操作且容易出现竞态条件)。
批量写入优化(写模式 2)
对于画线(line)和填充(bar)等需要连续写入的操作,驱动切换到 Write Mode 2,配合 CPU 的字节操作直接批量写入。
|
|
Write Mode 2 下,CPU 写入的每个字节的值直接被解释为"这 8 个像素对应的颜色值",硬件自动完成 8 个像素的同步更新,大幅加速批量绘制。
端口 I/O 的访问模式
反汇编统计了所有的 IN/OUT 端口指令:
|
|
0x3CF 的 992 次访问说明驱动的大部分执行时间花在了改写 GC 寄存器上——这正是画点/画线的核心瓶颈。现代 CPU 上端口 I/O 已经被虚拟化/模拟,性能相对无关紧要,但在 1980 年代的 8086/286/386 上,直接操作这些寄存器是追求画图速度的必然选择。
总结:BGI 驱动的架构设计
这个 EGAVGA.BGI 驱动体现了当年 Borland 对图形系统设计的理解:
-
分层架构
- 图形库内核(graphics.lib): 坐标变换、直线算法、字体渲染等通用逻辑,独立于硬件
- 驱动(.BGI 文件): 纯硬件相关操作(模式切换、寄存器操作),可热插拔
-
BIOS 作为"初始化工具"
- 使用 INT 10h 做"一次性"的事(切换模式、初始化调色板)
- 初始化完成后,绘制循环全部绕过 BIOS,直接操作硬件寄存器
-
微观优化
- 利用 EGA/VGA 的 latch 电路加速单像素写入
- 批量操作时切换写模式降低单像素的指令数
- 最小化端口 I/O 次数
迁移到 DJGPP 和 VESA 的障碍
16 位实模式 BGI 驱动无法在 DJGPP(32 位保护模式)上直接运行的原因:
| 方面 | 16 位实模式(BGI) | 32 位保护模式(DJGPP) |
|---|---|---|
| 内存模型 | 段式寻址(CS:IP) | 平坦内存空间,32 位线性地址 |
| 中断机制 | 实模式 INT 向量表 | 保护模式 IDT,需要 DPMI |
| 显卡访问 | 直接端口 I/O(ring 0) | 需要 DPMI 代理(__dpmi_int) |
| 显存映射 | 固定地址 0xA0000 | 需要 __dpmi_physical_address_mapping 映射 |
| 显示模式 | BIOS INT 10h(实模式) | DPMI INT 10h 模拟(需 __dpmi_int) |
| 高分辨率 | 最高 640x480(VGA) | VESA VBE 3.0,1024x768+ 甚至 4K |
最后一点最关键:EGA/VGA 的位面寄存器架构根本无法支持现代的高色深显示。VESA 模式普遍采用 LFB(线性帧缓冲)——一个像素直接对应内存中的 1~4 个字节(8-bit 调色板、16-bit 真彩色、24/32-bit 真彩色),没有复杂的位面寄存器逻辑。
迁移方案(预告)
鉴于现有 BGI 文件的架构限制,我们无法"反编译后直接复用"其二进制代码。正确的做法是:
- 理解 BGI 的接口契约(如本文所示)
- 重新实现一套 graphics.h 兼容层,接口完全遵循 BGI,但底层用 VESA VBE + DJGPP DPMI 实现
- 可选:设计新的驱动文件格式(比如 .VBD),允许加载不同的显示后端(VESA、FrameBuffer、SDL 等),保持 BGI 时代"可热插拔驱动"的优势
现有代码里 BGI 调用范围很小(只用 17 个函数),工作量可控。
参考资料
- Borland Graphics Interface Developer’s Guide
- VGA/EGA 硬件手册(Graphics Controller 寄存器定义)
- DJGPP 文档:DPMI 服务、物理地址映射
- VESA VBE 3.0 规范
相关阅读
Author 软件开发大郭
LastMod 2023-06-28