DOS下也要色色文字


最近一个项目需要通过 telnet 展示一些 16 色前景和背景的内容,我使用了 ANSI 转义序列(escape sequence)中的 SGR(Select Graphic Rendition,图形表现选择,CSI … m)序列来控制前景和背景色。写好测试文本后在现代终端输出上是没问题的,而当我来到 MS-DOS 6.22,才发现一切并不是我想的那么简单,展示出来的数据只有一部分颜色是正常的。

ANSIBlog-1.png

xterm 和 MS-DOS 对比

我是一个出生在 00 年的人,几十年前的人们对于电脑显示颜色的追求和技术发展速度之间的矛盾,是我没经历过的。而我这里使用的 SGR 历史比较久远且现在还在广泛使用,老的资料不是很好找。下面提到的内容对于一些专家、一些古董爱好者来说可能是常识,但我觉得解决这个问题的探究过程、发掘背后的发展历史是很有意思的,故将其整理成文。

TL;DR: 太长不读版:

为了节约空间,CGA/EGA/VGA 模式的显卡如果想要使用 16 色背景色,需要关闭硬件闪烁卸载(我写了一个 BLINK.EXE 来开关),否则背景最高位会作为文本闪烁与否的标记。

维基百科中所标注的、目前被广泛使用的 ANSI 转义序列中编号 90-97、100-107 的 8 种亮度提升的颜色是后来的非标准扩展,在现代使用没有任何问题,但没法在 MS-DOS 中使用。

在上古时代,“终端”(terminal)是物理的设备,比如最古早的电传打字机,或者一个通过串口之类的接口或协议连接到计算机的显示器。它们的工作十分轻松:从数据接口获取数据,然后打印、显示出来。而如果想让终端完成一些响铃、换行、回车之类的操作,就要靠一些不显示出来的控制字符实现(这种随着数据传输控制信息的方式也叫做带内传输)。ASCII(ANSI X3.4)中就有很多的控制字符来完成不同的功能,只是一套编码系统的容量是有限的,随着人们玩的花样越来越多,比如移动光标控制字符打印的位置、修改输出颜色等功能都需要有指令来交互。于是各个终端厂商都开发了自己的控制指令,用来实现各种高级功能,当然这样他们也就相互都不兼容了。

正是为了解决这种情况,ANSI 组织专家们设计了 ANSI X3.64(这个标准也被接受为国际标准 ISO/IEC 6429 和 ECMA-48、联邦信息处理标准 FIPS 86、日本产业标准 JIS X 0211),也就是前文提到的“ANSI 转义序列”(终于讲到正题了)。VT100 是第一个实现了这个标准的终端机,因此大家也经常能看到以 VT100 命名的终端类型,它指的就是原版 ANSI 转义序列的实现。ANSI 转义序列定义了 7 位数据和 8 位数据两种写法,大家熟悉的 ESC\x1b\033)起头的序列就是 7 位的写法,ESC 后面跟着的就是 C1 控制序列,例如常用的 ESC [ 起头的是 CSI 控制序列,CSI … ; … m 这种模式的就是 CSI 控制序列下的的 SGR 序列。8 位数据的写法就不介绍了,因为它和扩展 ASCII、Unicode 系列等编码方式冲突,没有使用意义。

即使大家后来抛弃了终端、用上了显卡,各种操作系统、虚拟终端也支持使用 ANSI 转义序列来实现光标、颜色、显示模式的操作。微软在 1986 年发布的 MS-DOS 4 中带上了 ANSI.SYS,即用于支持 ANSI 转义序列的驱动。只需要在 CONFIG.SYS 中加入 DEVICE=C:\PATH\TO\ANSI.SYS 即可开启。接着输出我生成的测试文本……诶?!怎么颜色不对?(见前文第一张图)

我在生成测试文本的时候使用的是维基百科上介绍的 SGR 颜色定义:30-37(40-47 为对应的背景色)分别是黑、红、绿、黄、蓝、洋红、青、白 8 种颜色,90-97(100-107 为对应的背景色)是上面 8 种颜色的亮度提升版。那么是哪里出了问题呢。

其实,这是现代终端处理 SGR 所使用的颜色表,我这么多年也一直以为八九十年代的终端使用同样的定义,毕竟 1981 年的 CGA 显卡已经支持了 16 色的文本模式,并没有觉得有什么不对。实际上,在 1979 年发布的 ANSI X3.64 中的 SGR 部分(5.77 节)只定义了字形的变换操作。一直到 1982 年的 ISO/IEC 6429 才增加了颜色变换的部分,且只有 30-37 前景色、40-47 背景色两部分。而上面 90-97、100-107 的定义来自 IBM 后来发布的 AIX 系统自带的虚拟终端程序(即 aixterm),后来被包括 xterm 在内的多个虚拟终端程序接纳,才变成了我们现在使用的事实标准。

研究到这里,我以为这个项目在 MS-DOS 这边只能用 8 色降级处理了。正当我准备放弃时,又想到了人类对于在电脑上显示图形和颜色追求。既然彩色的 CGA 显卡在 1981 年发布的时候就已经有了 16 色文本模式支持,人们不可能愿意放弃其一半的能力。于是我继续查找资料,了解到了原因。

从 CGA 到 EGA、VGA,虽然他们支持的文本模式的尺寸不一样,但是单个文字格式都是一样的:

++-------------++-------------++--------------------------++
|| b3 b2 b1 b0 || f3 f2 f1 f0 ||      8bit character      ||
++-------------++-------------++--------------------------++

显示的字符存在低 8 位,接着是 4 位前景属性和 4 位背景属性,一共 16 位。前景和背景属性的低 3 位(f0-f2,b0-b2)没有啥问题,就是色号(这里是指显卡中可配置的 16 色调色板中的顺序)。但是背景最高位(b3)同时兼任字符闪烁的标记,前景最高位(f3)同时兼任字体切换的标记,而这正是 8 色和 16 色的痛苦之源。这么设计是可以理解的:当时主流的处理器是 16 位,这样就可以在一个操作内完成一个字符的显示,如果要继续增加数据,对于内存、对于处理器来说都是不经济实惠的。所以我在前文说这是“人们对于电脑显示颜色的追求和技术发展速度之间的矛盾”。

说到这里,终于可以继续解决我遇到的问题了。

前景的 16 色很好解决,其实在 SGR 的标准定义中就有说明:参数 1 表示加粗或者增强。而怎么表示增强呢?当然就是提高它颜色的亮度啦。因此只要在前景的展示时追加一个参数 1,例如 CSI 33 m 是暗黄色,那么 CSI 1 ; 33 m 就是亮黄色。在显卡这边,就是把 f3 属性位设置成 1,显卡会去调用调色盘的后 8 种颜色,后 8 种颜色默认正是前 8 种颜色的亮度提升。f3 属性同时还是字体切换的标记,如果在第二字体区域设置了不同的字体的话,字形也会同时改变,所以在第二字体区域放置加粗的字形也可以实现标准中的加粗。

ANSIBlog-2.jpg

在文本模式下,显卡处理的是一个个字符,但也提供了一些效果的硬件卸载,闪烁的光标和闪烁的字符就是这样一个例子。SGR 在标准定义中就有开启字符闪烁的参数,即 CSI 5 m,在显卡这边实际上就是把 b3 属性位设置成 1。例如 CSI 5 ; 33 m 就是闪烁的黄色背景。

占用了背景色的一位,那这不是只能显示 8 种颜色了吗?好在显卡还是提供了关闭硬件字符闪烁的功能,关闭以后就可以释放背景色最高位,这样背景也可以调用调色板的后 8 种颜色,CSI 5 ; 33 m 就会变成不闪烁的亮黄色背景了。

怎么关闭硬件上的字符闪烁呢?EGA 和 VGA 显卡直接提供了 BIOS 中断 INT10h 1003h 来开关闪烁,CGA 显卡则需要手动修改模式选择寄存器的值,麻烦一点。查阅 MS-DOS 的编程手册,可知加载了 ANSI.SYS 驱动的话,还有 IOCTL 的操作以通过 dmFlags 字段来设置闪烁模式,不需要直接操作显卡硬件。

ANSIBlog-3.jpg

为了测试这个能不能用,简单起见,我写了下面这样的小代码。

.data
    dm  dw 0, 14
        db 14 dup(?)
.code
ioctl:  ; cl(5fh set, 7fh get)
    mov dx, seg dm
    mov ds, dx
    lea dx, dm
    mov ch, 03h    ; device category: screen
    mov bx, 01h    ; stdout
    mov ax, 440ch  ; IOCTL function: generic character device
    int 21h
    ret
main proc
    mov cl, 7fh  ; get current mode
    call    ioctl
    mov bx, dx
    xor word ptr [bx+4], 01h  ; invert blink bit
    mov cl, 5fh  ; set current mode
    call    ioctl
    mov ax, 4c00h  ; exit
    int 21h
main endp
end main

执行以后,文本确实不会闪烁了,也切换了新的颜色。但当我去测试的时候发现 IOCTL 的做法好像也只能对 EGA 和 VGA 显卡有效果,对 CGA 显卡无效。于是我又另外写了一个 BLINK.EXE源码),效果如下图所示。这个程序通过调用 EGA 的 BIOS 中断来确定使用的是 CGA 显卡还是 EGA/VGA 显卡,如果是前者就手动修改 CGA 的模式选择寄存器,如果是后者则调用 BIOS 中断。

ANSIBlog-4.gif

注意在开关闪烁模式的时候有很明显的颜色亮度变化,这样,16 色的显示也正常了。

ANSIBlog-5.png

另外,除了 86Box 模拟器上用 CGA/EGA/VGA 测试之外,我还使用了我的 Book8088(运行 8088 处理器的 2023 年生产的笔记本)配合 HD6845 CGA 显卡、Cirrus Logic GD5429 VGA 显卡,以及 Toshiba Satellite Pro 480CDT(运行 Pentium w/ MMX 233MHz 处理器的 1997 年生产的笔记本)配合 Chips & Technologies F65555 VGA 显卡进行测试,均能正常工作。最后以本文标题作结吧。

ANSIBlog-6.png

另:有些虚拟终端程序确实会将加粗用粗字重的字体表示、将闪烁用闪烁的形式表示。

  • Windows Terminal 等虚拟终端的默认行为和默认情况下的 CGA/EGA/VGA 显卡一样:加粗代表亮度提升的前景色,闪烁只会闪烁、背景还是普通亮度的颜色。
  • Konsole 和 Windows Terminal 差不多,但是加粗不光是颜色改变,也真的会加粗。
  • putty 默认的行为和 MS-DOS 关闭闪烁后效果一致:加粗代表亮度提升的前景色,闪烁代表亮度提升的背景色且不会闪烁。

因此如果不是折腾 MS-DOS 下的展示的话,还是使用 90-97、100-107 来实现亮度提升版本的颜色吧,就算要考虑 MS-DOS 也需要给用户切换终端模式的选项,比如默认 xterm、允许主动选择纯 ANSI 标准模式这样。


协议: 本文根据 Creative Commons Attribution-NonCommercial-ShareAlike 4.0 License 进行授权。

标签: dos 汇编


撰写新评论

account_circle
mail
insert_link
mode_comment