解析1997年存档中的128字节演示

满足我的愿望是非常愉快的,尤其是从遥远的过去开始,如此遥远,以至于我已经忘记了曾经想要的。 我对这个场景不了解,当然也从来没有跟随作者或他们的作品,我只是喜欢看发生了什么。 有时我想弄清楚,但是后来我缺乏知识和经验,后来又没有毅力,然后我对此完全失去了兴趣。 但是最近,我当时与我们一起学习,并向我们提供了BBS和Fidonet的所有新产品(包括演示)的朋友,因为他几乎几乎同时拥有电话,调制解调器和计算机,他带着自己的作品访问了CAFePARTY 。这使我打开了第一台计算机的存档,选择一个演示并弄清楚了。

pentagra.com

客观地评估自己的长处,我做了一个128字节的介绍,我从视觉上很喜欢。 pentagra.com文件由Mcm签名,为128个字节,最后修改时间为1996年9月24日18:10:14,十六进制转储:

000000: b0 13 cd 10 68 00 a0 07 06 1f ac ba c8 03 ee 42
000010: b1 40 ee 40 6e 6e e2 fa b8 3f 3f bb 40 01 bf 40
000020: 05 57 b1 78 ab 03 fb e2 fb 5f b1 60 88 01 aa 03
000030: fb 03 fb e2 f7 b1 61 88 01 aa 2b fb 2b fb e2 f7
000040: bf d1 99 57 b1 78 ab 2b fb e2 fb 5f b1 8f f3 ab
000050: 81 fe 00 fa 73 12 ac 0a c0 74 0d 48 88 44 fe 88
000060: 04 88 40 ff 88 84 bf fe 03 f2 42 75 e3 e4 60 3c
000070: 01 75 a5 b8 03 00 cd 10 c3 00 00 00 00 4d 63 6d

从同一档案中,我退出了:

  • Hiew 6.11 (可在网站上找到6.50 )-我将其用作反汇编程序
  • TASM软件包-我用它收集了收到的代码,以确保我不会弄乱任何东西
  • Flambeaux软件的TECH帮助! 6.0-针对DOS API,BIOS函数,硬件和汇编器的详细而全面的在线参考
  • Mayko G.V. IBM PC的汇编程序 -几乎是袖珍大小的格式参考,适用于所有基本的Intel 8086命令和程序文本格式设置规则。 没有架构细节,只有基本示例,只有最基本的东西。 这里几乎有您需要的所有东西,但是除了环境之外,您无法使用汇编器进行编写。
  • 因此,第二本书Zubkov S.V. 汇编器。 对于DOS,Windows和Unix-硬件角落和DOS指南

从最低限度的最小实现上,应该可以期待使用技巧和非标准方法,但是除了在初始条件下的一些假设之外,我没有看到任何技术技巧,但是我看到了一种算法技巧。 在这里,您应该对体验说几句话。 可能会有什么困难? 在实现中或在算法中。 例如,在mov di, 099d1h命令mov di, 099d1h ,您可能会害怕魔术常数。 但是,如果您在使用环境中,很显然这是屏幕坐标X和Y上的访问地址,其中X = 17,Y = 123,320是屏幕的水平分辨率(以像素为单位)。 总之,这使我们获得了17 + 123 * 320,即二维坐标到一维的转换。

现在看一下屏幕上发生的事情,即使不是100%相似,即使不是100%相似,我也可以轻松想象如何实现它,但是我可以。 而20年前,尽管我从尘土飞扬的架子上拿出了所有使用过的工具,但我不必浏览互联网就可以了解它的工作原理。 因此,首先,这是一个上下文,是对正在发生的事情的理解,因此,技巧和操作方法的问题位于第二位。

我们看到的是:

  1. 五行五角星。 根据所有经典,这些不一定是不可分割的直线。 我们只看到一般数字,没有细节
  2. 火焰效果包括两个重要部分:正确选择的调色板和一种算法,该算法使用不确定性元素不断更改屏幕上点的颜色,但保持相邻点的连续调色板顺序。 例如,您可以通过平均上一个屏幕的相邻像素的值来计算整个当前屏幕,并在随机位置(或不在随机位置)添加更多的“亮”点,而在值上随机(或完全没有机会)添加更多“亮”点,只需远离线性顺序即可。 一种选择是如何在DOOM中完成 。 结果应该是颜色相互渗透的形式,从不断出现的明亮区域到褪色

有待了解如何完成此操作。 进一步的描述不会替代有关计算机体系结构和DOS或汇编器功能的知识,但是拥有此知识将使您能够理解并专注于正在发生的事情的本质。 开始写作后,我意识到事实足够详细,但我不能拒绝它,以免失去故事的意义。

DOS和加载.COM程序


.com文件中的程序是干净的代码,没有标题,只需将其放在正确的位置即可。 这就是DOS所做的,或者更确切地说是4Bh系统调用。 正在采取许多行动,让我们来谈谈结果:

  • 所有段寄存器CS,DS,ES,SS均加载有单个值
  • 65536字节为整个程序保留,正好是所有段寄存器指示的一个段。 前256个字节由系统头PSP(程序段前缀)占用。 在CS:0(PSP的第一个字段)处,找到INT 20h命令-结束当前程序并将控制权转移到父进程。 程序本身以CS:100h地址开头,并占用以下128个字节
  • 字0000h被压入堆栈,SP寄存器为FFFEh。 这意味着该段中位于地址SS:FFFEh的最后两个字节被复位。 实际上,这是过程中最接近的返回地址,它将导致我们到达CS:0处的完成命令。
  • 寄存器AL和AH包含一个错误标志,用于在调用程序时从第一个和第二个参数确定驱动器号。 如果没有错误,则它们为0,如果存在则FFh

我真诚地认为,在一般情况下,寄存器的状态没有定义。 但是在我看来,在所分析的代码中,对它们的初始状态做出了非常大胆的假设,尤其是关于CX,SI寄存器和DF方向标志。 我在上面产生的源列表中没有找到对此的确认,因此我去浏览了MS-DOS 2.0源:

  • 关于DF,我们可以假定它已由cld命令重置,因为后者在将控制权转移到换行之前使用正向,因此DF被重置。 尽管此位置没有明确使用cld ,但是在进行许多其他传输之前,经常会遇到清除方向标志的命令
  • SI包含100h,因为它用于确定IP命令计数器将加载到寄存器中的偏移量
  • CX等于FFh,因为它用作初始值80h的计数器,用于传输整个命令行的内容,因此,在传输之后它为0。此后,CL作为临时变量加载FFh,并用于在AL和AH中设置驱动器号的错误标志

没有较新版本的来源,但有DOSBox来源

 reg_ax=reg_bx=0;reg_cx=0xff; reg_dx=pspseg; reg_si=RealOff(csip); reg_di=RealOff(sssp); 

也就是说,它与我在MS-DOS源代码(第二版!)中看到的一致,您可以看到其他寄存器的初始值,这是一个显式的特殊初始化。 对于MS-DOS,除AX,段和堆栈以外的寄存器的值都是将它们用于其他目的的基本条件;这不是教条或标准,因此,在任何地方都没有提及。 但是,另一方面,已经形成的生态系统以及Microsoft在支持与旧版本的兼容性上的全部痛苦(迫使他们拖延所有随机生成的值)变得有点可理解了,因为程序员非常习惯于它们。

最后,对于我们来说,这些知识已足够,我们开始从头文件恢复程序:

 .186 .model tiny .code .startup 

我们确定处理器80186的类型,因为我们使用了仅在此模型中出现的outsb命令。 一个代码段和一个程序入口点,再加上tiny内存模型的定义,将使编译器能够正确计算变量和转换的所有偏移量。 构建tlink ,将使用/t tlink ;在输出上将提供一个.com文件。

图形和调色板


要切换到图形模式,您需要转到BIOS函数,为此将中断10h,AH = 0,在AL中,我们将所需模式的标识符-13h放入:

 mov al, 13h ;b0 13 int 10h ;cd 10 

请注意,根据程序加载条件,假设零为零,我们不会触摸AH。 所选模式对应于具有256色调色板的320 x 200像素的图形分辨率。 要在屏幕上显示一个点,您需要写入存储区,该存储区以地址A000h:0(与颜色相对应的字节)开头。 用以下值填充段数据寄存器:

 push 0a000h ;68 00 a0 pop es ;07 push es ;06 pop ds ;1f 

从逻辑上讲,内存被组织为一个二维数组,在其中显示屏幕坐标,0:0对应于左上角。 切换模式后,将以零填充-默认调色板中为黑色。 转换为线性位移的公式为X + Y * L ,其中L是水平分辨率,在我们的情况下为320。在这种形式下,我将在使用常量的地方编写代码,在转换程序文本时它们会自动计算出来。

要更改调色板,我们可以使用输入/输出端口直接访问设备:

 lodsb ;ac mov dx, 03c8h ;ba c8 03 out dx, al ;ee 

第一条命令将位于DS:SI的数据字节加载到AL中。 在DS中,我们已经加载了视频内存的段地址,并且在SI中我们知道它用零填充-在通常情况下,我们不知道至少为0。无论SI指示在哪里,我们几乎都可以进入占用的视频内存分辨率为320 * 200 = 64000字节,几乎是整个段。 因此,我们希望在此命令之后AL = 0。 对SI加上或减去一个单位,取决于DF方向标志的设置。 尽管这对我们来说也不是特别重要,但是无论SI移到哪里,我们仍然保留在填充有零的视频存储区域中。

接下来,使用端口号03C8h加载DX,该输出将确定我们将要覆盖的256种颜色。 在我们的情况下,AL为0。

颜色是在RGB调色板中编码的,为此,您应该连续三次写入端口03C9h(大于3C8h),每个组件一次。 组件的最大亮度为63,最小为0。

 inc dx ;42 mov cl, 64 ;b1 40 PALETTE: out dx, al ;ee inc ax ;40 outsb ;6e outsb ;6e loop PALETTE ;e2 fa(-6),    6   

将DX增加1,使其具有所需的端口号。 CL是我们的周期计数器64,我们假设CH = 0,如先前基于初始加载条件所述。 接下来,我们将第一个组件输出到端口-红色的组件,其亮度将存储在AL中,就是我们将在第一步0中对其进行更改。在此之后,我们将其亮度增加一个,以在下一次迭代中显示。 接下来,我们执行两个写入端口的outsb命令outsb该端口号包含在DS内存区中的字节DX中:SI,请记住,那里有零。 SI每次更改一个。

一旦推断出这三个成分,就会自动将一个单位添加到色号中。 因此,如果颜色是连续的,则不必根据需要输出到3C8h端口来重新定义颜色。 loop命令将CX减一,如果获得非零值,它将转到循环的开始,如果为0,则转到循环后的下一个命令。

总共64次重复。 在每次重复中,我们确定从0到63的颜色的红色分量,其亮度与当前色号一致。 我们重置绿色和蓝色分量,以获得从最小到最大红色亮度的调色板:

示意图


线数


设置初始颜色和坐标值:

 LINES: mov ax, 03f3fh ;b8 3f 3f mov bx, 0+1*320 ;bb 40 01 mov di, 64+4*320 ;bf 40 05 push di ;57 

在AL和AH中,我们分别加载最大可能的(最亮的)颜色63(3Fh),AX一次定义了两个点。 BX-最大水平分辨率。 将来,它将用于从当前坐标中添加或减去一行。 DI-坐标64:4,将它们保存在堆栈中。

从左上角到右端画第一行

 mov cl, 120 ;b1 78 LINE1: stosw ;ab add di, bx ;03 fb loop LINE1 ;e2 fb(-5) 

配置计数器-这将是行数。 接下来,将AX中的字(两个字节)保存到地址ES:DI。 此操作将在屏幕上以调色板中的最大颜色显示两个点,因为ES已配置为用于视频内存,并且已在DI中设置了特定坐标。 执行此操作后,由于写入了两个字节,因此将2添加到DI。 显然,我们没有设置DF方向标志,而是依靠将其重置的事实,因此我们再次回顾了加载程序的初始条件。 否则,两者将被带走,这将不允许绘制所需的线。

接下来,DI = DI + BX,相当于将Y坐标增加一。 因此,在循环的主体中,在一条直线上绘制了两个点,X坐标增加了2,Y坐标增加了1,此动作重复了120次,结果图像稍低。

第二行是从左上方到顶部

 pop di ;5f mov cl, 96 ;b1 60 LINE2: mov [bx+di], al ;88 01 stosb ;aa add di, bx ;03 fb add di, bx ;03 fb loop LINE2 ;e2 f7(-9) 

我们将初始坐标恢复为64:4,并将计数器设置为96次重复。 我们打印一个点,但是在当前坐标下打印一行。 和以前一样,这是通过从BX添加一个值来实现的,只是不保存新坐标。 结构[bx+di][bx][di]被称为带有索引的基址,并且在处理器级别而不是在翻译器上工作。 BX的默认段寄存器是DS。 之后,我们显示第二个点,但是已经在当前坐标中。 DI,因此X增加1,因为仅使用了一个字节传输命令stosb 。 循环主体的最后两个命令是将Y增加2,为此我们再次使用BX。

绘制两条线后,在左上角附近获得以下图像:

1,2行


视频存储器中行偏移地址的左坐标和右坐标。 点64:4将被绘制两次。

第三行是从顶部到右上角

 mov cl, 97 ;b1 61 LINE3: mov [bx+di], al ;88 01 stosb ;aa sub di, bx ;2b fb sub di, bx ;2b fb loop LINE3 ;e2 f7(-9) 

DI已经包含所需的坐标值160:196,我们需要从上一行的顶部开始绘制一条线,在保持相同角度的情况下向上移动屏幕。 因此,周期几乎相同。 CX增加1,因为当前的Y坐标比上一行结束的位置多2(低),因此已经为下一次迭代计算了它。 因此,要移到上角,您需要采取额外的步骤。 沿X的移动沿相同方向继续-每次迭代后加一个,沿Y的移动而不是相加,而是减去两个。 点的显示顺序相同,先低后高。

3号线


第四行是从最左边到右上角:

 mov di, 17+123*320 ;bf d1 99 push di ;57 mov cl, 120 ;b1 78 LINE4: stosw ;ab sub di, bx ;2b fb(-5) loop LINE4 

我们再次处于必需的坐标中,但是显然不使用该坐标,以便不更改DF方向标志。 因此,新坐标将放置在DI中并存储在堆栈中。

此外,一切都与第一行相同,只有Y坐标不会增加,但会减小,然后上升。

第五行是水平的:

 pop di ;5f mov cl, 143 ;b1 8f rep stosw ;f3 ab 

这里的一切都很简单,使用了微处理器重传机制,因为水平线对应于每个下一个点地址的简单增加。 在DI中,恢复上一步中存储的与左极角坐标对应的地址。 设置CX中的重复次数,并使用单词传输命令应用重复前缀。

完成此操作后,我们将绘制出一个最亮的五角星。 使用了80个字节,保留了48个字节。

火魔法


我们为计算设置边界条件:

 FLAME: cmp si, 320*200 ;81 fe 00 fa jae NEXT_PIXEL ;73 12 lodsb ;ac or al,al ;0a c0 jz NEXT_PIXEL ;74 0d 

在SI中,将有当前点的坐标进行计算,如果我们超出了屏幕的边界,则我们不会对该点进行任何计算,而是继续计算下一个。

lodsb将DS:SI区域中的一个字节加载到AL中,即当前坐标中点的颜色。 如果它是0,那么我们也什么也不做,继续进行到下一点。

新颜色计算

这是更改屏幕上的颜色值的主要算法,这不是火焰,这是它的基础。 我们计算相邻点并实现颜色连续性:

 dec ax ;48 mov [si-2], al ;88 44 fe mov [si], al ;88 04 mov [bx+si-1], al ;88 40 ff mov [si-1-1*320], al ;88 84 bf fe 

从AX减去实际上从AL减去一个包含从当前坐标获得的非零颜色值的单位。 接下来,我们基于调色板将获得的值写入相对于当前坐标的所有相邻点,即它们的一小部分。

由于在lodsb之后,SI值增加了一个且不再对应于我们在AL中读取其颜色的点,因此必须对其进行调整。 请注意,不再使用stosb字节传输命令;而是使用mov来精确定位将要放置值的地址。 如果我们接受当前坐标为X:Y,对于它们来说为SI-1,则:

  • mov [si-2], al在当前颜色左侧的X-1:Y点记录新颜色。 由于上述原因,从SI中减去2,因为已经向其中添加了一个额外的单位
  • mov [si], al在当前颜色右边的X + 1:Y处记录新颜色。 SI已经有X +1
  • mov [bx+si-1], al向当前点下方的点X:Y + 1写入新颜色。 再次将BX用作Y + 1
  • mov [si-1-1*320], al在当前颜色上方的点X:Y-1处写入新颜色。 我们将无法使用BX,因为我们需要删除坐标,处理器架构不允许我们以这种形式执行此操作,因此根据坐标缩减公式使用了一个常数

段寄存器是DS,默认情况下与SI和BX一起使用。

当该点到达屏幕边缘时,不会检查任何情况。 这不会导致失败,因为我们将始终处于视频片段的边界之内。 从进一步的说明中可以看出,相邻点可以落入地址大于64,000的未报告区域中,也可以落入相邻行中,这对我们无害,甚至无济于事。

一样的魔法,计算下一点的坐标

 NEXT_PIXEL: add si, dx ;03 f2 inc dx ;42 jnz FLAME ;75 e3(-29) 

让我们回头看看,我们没有在任何地方专门设置初始SI值,在DX中,我们仍然有用于调色板的输出输入端口号。 我们仅执行三个简单的动作SI = SI + DX,显然这将设置新的坐标,哪个坐标? DX = DX + 1,并且如果DX不等于0,那么回到获取和计算相邻点的基本算法,即DX是某种计数器吗?

我们知道我们需要遍历所有点并计算其邻居的亮度变化。 如果您连续执行此操作,那么我们可能会得到一个静态渐变,也许不是很均匀,但是在我们的行周围没有变化。我们知道屏幕的大小以及需要绕开多少个点,但是在这里我们几乎忽略了它,而是选择收盘价65536而不是精确的64000。DX实际上是一个计数器,仅为65536。但是为什么其初始值并不重要,为什么我们采用最终值是否大于屏幕上的总点数?

因为我们绕过的点不是连续的,也不是全部。每个后续线性坐标比上一个线性坐标大DX的值。也就是说,在SI中,简单算术级数的DX元素的总和:0、1、2、3、4、5、6,...,362,363,...,65535。这已经给我们非线性了,如果您从SI = 0和DX = 0开始,那么在SI中,我们得到:0,1,3,4,6,10,15,21,...,65341,65703,..., 2147450880。

但这还不是全部,因为SI尺寸为16位,我们不能获得大于65535的值,会发生溢出,并且SI中的余数保持为65536模。线性坐标计算公式的格式为SI =(SI + DX)MOD 65536,它完全破坏了连续顺序:0,1,3,4,6,10,15,21,...,65341,167,530,894,...

现在我们回想起SI并没有以任何方式初始化,也就是说,下次我们返回该循环时那么我们将从停止的坐标开始,而不是从0或给定的某个坐标开始。这将给我们的序列增加混乱-延长非重复元素的数量。否则,这些点的遍历将始终是相同的,尽管是非线性的。会有火焰效果,但不是很清楚。如果我们谈论把戏,那仅此而已。由于第一次溢出,DX总是隐式地从0开始,除非是第一次使用inc dx

我们的边界值会增加一些混乱,因为对于SI> = 64000,将不会绘制任何点,并且输出序列会有些混乱。跳过所有零值点会在程序的前几秒钟产生点火效果。这是因为整个循环结束得更快,因为大多数点都没有处理。但最重要的是,由于大多数点的亮度只会增加,因此它们不会被相邻的调光器部分所遮盖-它们根本不存在,并且不会计算零值。在完全黑色的区域消失后,就建立了平衡,某些区域将增加亮度,而某些区域将减少。

结果,我们再也无法谈论任何顺序或梯度,每次都以新的顺序分配点,包括重复几次或完全跳过。这导致形成彼此混合的不同亮度的区域,并在每次新的迭代中改变。

但这还不是全部,如果您不添加新的亮点,那么最终它们将全部还清。因此,在DX达到最大值之后,我们再次绘制五条亮线,并再次计算屏幕上的所有点:

 in al, 60h ;e4 60 cmp al, 01h ;3c 01 jne LINES ;75 a5(-91) 

但在此之前,我们从端口60h读取,这是键盘,是最后按下的键的扫描代码。对于ESC等于1。如果是,则按ESC键,我们向出口移动。

完成时间


值得注意的是,在更新当前屏幕时(这会花费一些时间),您无法退出程序,也就是说,对ESC的反应将会延迟。如果在等待过程中以及按ESC键后,我们仍然保留在程序中,则只能从端口读取最后的扫描代码。还有一件事,我们不为此替换或使用DOS和BIOS系统功能,无论我们从端口读取什么,按下的键都放在循环缓冲区中,并且在介绍完成后很可能由下一个程序从该缓冲区中读取该文件,很可能是文件经理或command.com。这将导致其处理,例如,ESC上的Volkov Commander将隐藏其面板。

返回文本模式3:

 mov ax, 03h ;b8 03 00 int 10h ;cd 10 

假定我们在程序启动之前就处于这种模式,但是在一般情况下可能并非如此。在这里,我们更新整个AX,因为我们确定AH不包含0。

现在您可以退出:

 retn ;c3 

这是一个过程的近出口命令,该过程从堆栈中获取放置在那里的字的值(两个字节)并将其装入IP命令计数器。根据初始条件,我们在堆栈中有零,这将导致我们到达CS:0地址,即我们所知道的命令代码所在的位置int 20h-关闭。

还有7个字节的版权:

 dd 0h ;00 00 00 00 db 'Mcm' ;4d 63 6d end 

可以说,我还有地方要进行更严格的初始化,但是由于一切都可以在现代DOSBox中运行,因此作者可能所做的一切都正确。

让我们再经历一次:

  1. ,
  2. 4 , : X+1 Y+2, X+2 Y+1. , . ,
  3. SI=(SI+DX) MOD 65536, DX , , , SI. 1. 65536 , , . , — add si, dx inc dx , ,
  4. ESC ,

.
 .186 .model tiny .code .startup mov al, 13h int 10h push 0a000h pop es push es pop ds lodsb mov dx, 03c8h out dx, al inc dx mov cl, 040h PALETTE: out dx, al inc ax outsb outsb loop PALETTE LINES: mov ax, 03f3fh mov bx, 0+1*320 mov di, 64+4*320 push di mov cl, 120 LINE1: stosw add di, bx loop LINE1 pop di mov cl, 96 LINE2: mov [bx+di], al stosb add di, bx add di, bx loop LINE2 mov cl, 97 LINE3: mov [bx+di], al stosb sub di, bx sub di, bx loop LINE3 mov di, 17+123*320 push di mov cl, 120 LINE4: stosw sub di, bx loop LINE4 pop di mov cl, 143 rep stosw FLAME: cmp si, 320*200 jae NEXT_PIXEL lodsb or al,al jz NEXT_PIXEL dec ax mov [si-2], al mov [si], al mov [bx+si-1], al mov [si-1-1*320], al NEXT_PIXEL: add si, dx inc dx jnz FLAME in al, 60h cmp al, 01h jne LINES mov ax, 03h int 10h retn dd 0h db 'Mcm' end 

要进行编译,您必须执行:tasm pentagra.asmtlink /t pentagra.obj

我不知道您是否清楚该如何实施以及如何实施,但在我看来似乎是采用了一种美丽而又不寻常的方法来产生火焰效果。尽管我没有什么可比的,但也许每个人都可以做到,现在您可以做同样的事情。

Source: https://habr.com/ru/post/zh-CN482826/


All Articles