一段仅有 16 字节的 x86 汇编代码,通过将显存用作计算空间,生成了无限的谢尔宾斯基分形。它巧妙地利用异或运算和特定的内存步进,不仅在屏幕上绘制出图案,还将其直接输出到 PC 扬声器,创造出独特的视听体验。程序的核心在于揭示了数学、内存布局、音频和视觉效果之间的深刻联系。
这段代码的创作灵感源于作者对早期 IBM PC 和单色显示器的怀旧之情,以及对算法密度探索的兴趣。在数百次微小的实验中,这个版本因其独特的声音脱颖而出。它将谢尔宾斯基分形的声音,转变为矩阵式的视觉效果。
以下是这 16 字节的 x86 实模式 DOS 汇编代码:
int 10h
mov bh, 0xb8
mov ds, bx
L:
lodsb
sub si, byte 57
xor [si], al
out 61h, al
jmp short L
预设的画布:显存空间
程序首先通过 int 10h 将显示模式设置为 40x25 的文本模式。接着,它将数据段(ds)指向 0xb800,即 VGA/CGA 文本缓冲区的内存地址。
- 一个并非空白的起点:当 BIOS 清屏时,它并不会用零填充内存。每个字符位置(2 字节)都被设置为
0x20(空格) 和0x07(颜色属性)。 - 初始状态:因此,屏幕看起来是空的,但内存中已经充满了统一的模式,这为后续的计算提供了初始数据。
计算引擎:累加的前缀和
要理解代码的数学原理,可以先将其简化:假设内存是零,使用 add 而非 xor,并且步进为 16 字节。
在这种简化模型下,程序会在内存单元之间累加值,形成部分和。由于 DOS 段大小(65536 字节)和寄存器大小(8 位)的特性,数值会遵循一个二项式序列,在每次遍历段时干净地重置,形成一个可预测的数学结构。
尽管实际代码使用了更复杂的方法,但这个“累加”模型揭示了其背后隐藏的数学秩序。
分形生成:异或运算与谢尔宾斯基三角形
现在,回到实际代码中的 xor(异或)运算。
- 无进位加法:在位运算层面,异或等同于无进位的二进制加法。这一特性使得二项式序列在模 2 运算下,自然而然地生成了谢尔宾斯基分形。
- 规则 60:程序的行为与初等元胞自动机中的 规则 60 完全吻合。由于初始值为 2 (二进制
00000010),计算只影响第 1 位,从而在内存中精确地“绘制”出分形图案。
通过异或运算,一个简单的算术过程被转化为一个复杂而优美的几何结构。
机器之声:从数据到音频
代码中最巧妙的部分之一是 out 61h, al。
- 直通扬声器:
61h端口直接连接到 PC 扬声器。该端口的第 1 位控制扬声器锥盆的推(1)拉(0)。 - 数据即声音:程序计算出的分形数据字节被直接发送到这个端口。分形图案中的 0 和 1 序列因此变成了驱动扬声器的方波,创造出脉冲宽度和频率自然变化的“字节音乐”(bytebeats)。
- 意外的噪音:更有趣的是,程序处理的不仅是屏幕文本区域,还包括内存段中剩余的数据,其中可能包含视频 ROM BIOS 代码的影子。这为纯净的分形声音增添了朋克和粗砺的质感。
56字节的步进:音高变化与视觉倾斜
代码没有采用简单的步进,而是通过 lodsb 和 sub si, byte 57 实现了 -56 字节的向后移动。这个看似奇怪的选择同时影响了音频和视觉。
- 音频效果:56 无法被段大小 65536 整除。这导致循环周期加倍,声音的基频因此降低了一个八度。
- 视觉效果:在 80 字节宽的屏幕上,向后移动 56 字节,视觉上等同于向前移动 24 字节(12 个字符列)。这使得分形图案不再是完整的三角形,而是被“剪切”成 10 根在屏幕上向上移动的倾斜字符柱。
真实硬件与最终思考
由于代码直接与内存中已有的数据进行异或运算,其输出对运行环境高度敏感。
模拟器和不同版本的 BIOS 会在内存中留下略微不同的“残留物”。清除内存会得到统一的输出,但这需要额外的字节。拥抱硬件的自然状态,正是 sizecoding(极限编程)的魅力之一。
最终,这段代码不仅是一次技术展示,更是一次对算法、数学和硬件之间关系的深刻探索,它让我们“听见”了分形的结构,也“看见”了声音的形状。