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).
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.
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.
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.