Disassembly of Farbrausch's "fr-016: bytes"

In 2001, Farbrausch released a graphics demo that was 16 bytes long, called "fr-016: bytes". Here's my disassembly of it:

kragen@thrifty:~/pkgs/fr-016$ objdump -m i8086 -b binary -D fr-016.com

fr-016.com:     file format binary

Disassembly of section .data:

0000000000000000 <.data>:
   0:   b0 13                   mov    $0x13,%al
   2:   cd 10                   int    $0x10
   4:   c4 2f                   les    (%bx),%bp
   6:   aa                      stos   %al,%es:(%di)
   7:   11 f8                   adc    %di,%ax
   9:   64 13 06 6c 04          adc    %fs:1132,%ax
   e:   eb f6                   jmp    0x6

(Don't try to disassemble it in 386 mode; the results will be mysteriously wrong.)

It runs in DOSBOX successfully and draws a pretty awesome animated pattern, but in QEMU the pattern draws successfully but is static. (Until I twiddled memory with GDB; see below.) Here's the pattern, blown up from 320x200 to 640x400:

It's a little bit mysterious to me how this works. In particular, the fs: prefix on the adc (hex 64) looks suspicious (the default segment would be %ds which has the same value as %fs by default, 0x22e4 in FreeDOS running in QEMU).

Probing and disassembling it in QEMU with GDB

In order to understand it better, I ran the program inside QEMU, IIRC more or less as follows:

kragen@thrifty:~/pkgs/fr-016$ dd if=/dev/zero bs=1k count=1440 of=diskimage
kragen@thrifty:~/pkgs/fr-016$ mkfs -t msdos diskimage
kragen@thrifty:~/pkgs/fr-016$ mkdir mnt
kragen@thrifty:~/pkgs/fr-016$ sudo mount -o loop diskimage mnt
kragen@thrifty:~/pkgs/fr-016$ sudo cp fr-016.com mnt
kragen@thrifty:~/pkgs/fr-016$ sudo umount mnt
kragen@thrifty:~/pkgs/fr-016$ qemu -snapshot -fda diskimage \
                              ~/devel/qemu/freedos.qcow2

(and inside QEMU:)

C:\> A:
A:\> fr-016

(It would have been a lot easier to use qemu -fda fat:floppy:. instead of making the disk image. Oh well.)

Then, while it was still running (easy, since there's no way to exit it as far as I can tell) I ran the gdbserver command in the QEMU console (ctrl-alt-2). Then I attached GDB to the running QEMU to see what's going on. Here's a transcript of starting this out, minus all the false starts:

kragen@thrifty:~/pkgs/fr-016$ gdb
GNU gdb 6.4.90-debian
Copyright (C) 2006 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i486-linux-gnu".
(gdb) target remote localhost:1234
Remote debugging using localhost:1234
0x00000106 in ?? ()
(gdb) info registers
eax            0x40c57e 4244862
ecx            0xff     255
edx            0x22e4   8932
ebx            0x0      0
esp            0xfffe   0xfffe
ebp            0x20cd   0x20cd
esi            0xffff0100       -65280
edi            0x85c9a  547994
eip            0x106    0x106
eflags         0x286    [ PF SF IF ]
cs             0x22e4   8932
ss             0x22e4   8932
ds             0x22e4   8932
es             0x9f7f   40831
fs             0x22e4   8932
gs             0x1f49   8009
(gdb) set architecture i8086
The target architecture is assumed to be i8086
(gdb) x/10i $cs*16+0x100
0x22f40:        mov    $0x13,%al
0x22f42:        int    $0x10
0x22f44:        les    (%bx),%bp
0x22f46:        stos   %al,%es:(%di)
0x22f47:        adc    %di,%ax
0x22f49:        adc    %fs:1132,%ax
0x22f4e:        jmp    0x22f46
0x22f50:        add    %al,(%bx,%si)
0x22f52:        add    %al,(%bx,%si)
0x22f54:        add    %al,(%bx,%si)
(gdb) x/16cx $cs*16+0x100
0x22f40:        0xb0    0x13    0xcd    0x10    0xc4    0x2f    0xaa    0x11
0x22f48:        0xf8    0x64    0x13    0x06    0x6c    0x04    0xeb    0xf6

(MS-DOS COM files load into memory at address 0x100.)

So I guess when we start the program, %ah is 0; so we stick a video mode in %al and call interrupt 10h to set the video mode. Video mode 13h is 320x200, 8-bit pseudocolor; and video memory starts at A0000h, just above the MS-DOS 640K limit.

According to Intel document 253666, les is "load far pointer into ES segment register"; it apparently loads a 32-bit segment:offset "far pointer" from the place pointed to in memory by %bx into the ES register and the %bp register specified as the destination. (%bp is never used again as far as I can tell.)

Now, in the four-instruction loop, %bx doesn't change; so presumably the value it has at the point where I interrupted it, 0, is the same value it had at the time this instruction was executed. Presumably %bx will be interpreted relative to %ds; so what's at %ds:(%bx)? %ds, like the other segment registers, gets initialized to point at the 64kiB segment where the .COM file is loaded; the first 256 bytes are the "PSP" or "program segment prefix".

(gdb) x/4cx $ds*16+(int)$ebx
0x22e40:        0xcd    0x20    0x7f    0x9f

Well, surprise surprise, there's a little-endian 9f7fh, just like we see in %es above, plus an offset. I guess some value close to that must always be in those bytes, but I didn't remember enough about how DOS launches .COM files to know why; Randall Hyde's Art of Assembly Language Programming says that's the program ending address. (I don't know why that isn't ffff.) 9f7fh is just 81h less than A000h, so by indexing off of it (as stos does by default) gives us access to video RAM.

So that's the setup for the loop. Here's the loop again:

   6:   aa                      stos   %al,%es:(%di)
   7:   11 f8                   adc    %di,%ax
   9:   64 13 06 6c 04          adc    %fs:1132,%ax
   e:   eb f6                   jmp    0x6

So, each time through the loop, we store %al at %es:(%di), then add %di and %fs:1132 to %ax. With carry. 1132 is 046Ch; I'm kind of mystified about what that's supposed to be. Looks like there happen to be zeroes at that address:

(gdb) x/4x $fs*16+1132
0x232ac:        0x00    0x00    0x00    0x00

So, anyway, the stos increments %di to point to the next byte; this will run all over the 65536-byte segment pointed to by %es, including the 64000 bytes that comprise video memory, and also 129 bytes before them and 1407 bytes after them, which hopefully will be harmless.

For reasons I don't understand, the pattern you get from repeatedly adding %di to %ax in this way contains a bunch of circular waveplates. (Is that what you call them? Colors according to x² + y² modulo some base.) This is probably the really interesting part of the program, so I'm sad that I'm not able to throw any light on this.

It looks like the purpose of the extra adc is to perturb the pattern so that it changes in phase each frame. I did this and I started getting ripples in QEMU:

(gdb) p *(int*)($fs*16+1132) = -10
$3 = -10
(gdb) c
Continuing.

Interestingly neither 0 nor -1 gives any motion: I guess adding -1 results in a carry that gets added back in on the next loop.

Without fs:

I edited the file with Emacs and made the following version without the fs: prefix:

   0:   b0 13                   mov    $0x13,%al
   2:   cd 10                   int    $0x10
   4:   c4 2f                   les    (%bx),%bp
   6:   aa                      stos   %al,%es:(%di)
   7:   11 f8                   adc    %di,%ax
   9:   13 06 6c 04             adc    1132,%ax
   d:   eb f7                   jmp    0x6
   f:   20                      .byte 0x20

In Dosbox, this runs and produces the same pattern --- but it doesn't animate! So maybe FreeDOS is setting %fs to point to the same segment as the other segment registers, and consequently it points at zeroes, but Dosbox (and presumably MS-DOS) leaves it pointing somewhere else. Say, to the bottom of memory.

Without the second adc

I also made a version where the loop is only three instructions:

kragen@thrifty:~/pkgs/fr-016$ objdump -m i8086 -b binary -D fr-016-static.com

fr-016-static.com:     file format binary

Disassembly of section .data:

0000000000000000 <.data>:
   0:   b0 13                   mov    $0x13,%al
   2:   cd 10                   int    $0x10
   4:   c4 2f                   les    (%bx),%bp
   6:   aa                      stos   %al,%es:(%di)
   7:   11 f8                   adc    %di,%ax
   9:   eb fb                   jmp    0x6

As expected, this generates the same pattern, but it doesn't animate.