https://susam.net/self-printing-machine-code.html
Self-Printing Machine Code
By Susam Pal on 27 Oct 2005
The following 12-byte program composed of pure x86 machine code
writes itself to standard output when executed in a DOS environment:
fc b1 0c ac 92 b4 02 cd 21 e2 f8 c3
We can write these bytes to a file with the .COM extension and
execute it in DOS. It runs successfully in MS-DOS 6.22, Windows 98,
as well as in DOSBox and writes a copy of itself to standard output.
Contents
* Demo
* Quine Conundrums
* Proper Quines
* A Note on DOS Services
* Writing to Video Memory Directly
* Boot Program
Demo
On a Unix or Linux system, the following commands demonstrate this
program with the help of DOSBox:
echo fc b1 0c ac 92 b4 02 cd 21 e2 f8 c3 | xxd -r -p > foo.com
dosbox -c 'MOUNT C .' -c 'C:\FOO > C:\OUT.COM' -c 'EXIT'
diff foo.com OUT.COM
The diff command should produce no output confirming that the output
of the program is identical to the program itself. On an actual
MS-DOS 6.22 system or a Windows 98 system, we can demonstrate this
program in the following manner:
C:\>DEBUG
-E 100 fc b1 0c ac 92 b4 02 cd 21 e2 f8 c3
-N FOO.COM
-R CX
CX 0000
:C
-W
Writing 0000C bytes
-Q
C:\>FOO > OUT.COM
C:\>FC FOO.COM OUT.COM
Comparing files FOO.COM and OUT.COM
FC: no differences encountered
In the DEBUG session shown above, we use the debugger command E to
enter the machine code at offset 0x100 of the code segment. Then we
use the N command to name the file we want to write this machine code
to. The command R CX is used to specify that we want to write 0xC
(decimal 12) bytes to this file. The W command writes the 12 bytes
entered at offset 0x100. The Q command quits the debugger. Then we
run the new FOO.COM program while redirecting its output to OUT.COM.
Finally, we use the FC command to compare the two files and confirm
that they are exactly the same.
Let us disasssemble this program now and see what it does. The output
below is generated using the Netwide Disassembler (NDISASM), a tool
that comes with Netwide Assembler (NASM):
$ ndisasm -o 0x100 foo.com
00000100 FC cld
00000101 B10C mov cl,0xc
00000103 AC lodsb
00000104 92 xchg ax,dx
00000105 B402 mov ah,0x2
00000107 CD21 int 0x21
00000109 E2F8 loop 0x103
0000010B C3 ret
When DOS executes a program in .COM file, it loads the machine code
in the file at offset 0x100 of the code segment chosen by DOS. That
is why we ask the disassembler to assume a load address of 0x100 with
the -o command line option. The first instruction clears the
direction flag. The purpose of this instruction is explained later.
The next instruction sets the register CL to 0xc (decimal 12). The
register CH is already set to 0 by default when a .COM program
starts. Thus setting the register CL to 0 effectively sets the entire
register CX to 0xc. The register CX is used as a loop counter for the
loop 0x103 instruction that comes later. Everytime this loop
instruction executes, it decrements CX and makes a near jump to
offset 0x103 if CX is not 0. This results in 12 iterations of the
loop.
In each iteration of the loop, the instructions from offset 0x103 to
offset 0x109 are executed. The lodsb instruction loads a byte from
address DS:SI into AL. When DOS starts executing this program, DS and
SI are set to CS and 0x100 by default, so at the beginning DS:SI
points to the first byte of the program. The xchg instruction
exchanges the values in AX and DX. Thus the byte we just loaded into
AL ends up in DL. Then we set AH to 2 and generate the software
interrupt 0x21 (decimal 33) to write the byte in DL to standard
output. This is how each iteration reads a byte of this program and
writes it to standard output.
The lodsb instruction increments or decrements SI depending on the
state of the direction flag (DF). When DF is cleared, it increments
SI. If DF is set, it decrements SI. We use the cld instruction at the
beginning to clear DF, so that in each iteration of the loop, SI
moves forward to point to the next byte of the program. This is how
the 12 iterations of the loop write 12 bytes of the program to
standard output. In many DOS environments, the DF flag is already in
cleared state when a .COM program starts, so the CLD instruction
could be omitted in such environments. However, there are some
environments where DF may not be in cleared state when our program
starts, so it is a best practice to clear DF before relying on it.
Finally, when the loop terminates, we execute the RET instruction to
terminate the program.
Quine Conundrums
While reading the description of the self-printing program presented
earlier, one might wonder if it is a quine. While there is no
standardised definition of the term quine, it is generally accepted
that a quine is a computer program that takes no input and produces
an exact copy of its own source code as its output. Since a quine
cannot take any input, tricks involving reading its own source code
or evaluating itself are ruled out.
For example, this shell script is a valid quine:
s='s=\47%s\47;printf "$s" "$s"\n';printf "$s" "$s"
However, the following shell script is not considered a proper quine:
cat $0
The shell script above reads its own source code which is considered
cheating. Improper quines like this are often called cheating quines.
Is our 12-byte x86 program a quine? It turns out that we have a
conundrum. There is no notion of source code for our program. There
would have been one if we had written out the source code of this
program in assembly language. In such a case we would first need to
choose an assembler and a proper quine would need to produce an exact
copy of the assembly language source code (not the machine code
bytes) for the chosen assembler. But we are not doing that here. We
want the machine code to produce an exact copy of itself. There is no
source code involved. We only have machine code. So we could argue
that the whole notion of machine code quine is nonsense. No machine
code quine can exist because there is no source code to produce as
output.
However, we could also argue that the machine code is the input for
the CPU that the CPU fetches, decodes, and converts to a sequence of
state changes in the CPU. If we define a machine code quine to be a
machine code program that writes its own bytes, then we could say
that we have a machine code quine here.
Let us now entertain the thought that our 12-byte program is indeed a
machine code quine. Now we have a new conundrum. Is it a proper
quine? This program reads its own bytes from memory and writes them.
Does that make it a cheating quine? What would a proper quine written
in pure machine code even look like? If we look at the shell script
quine above, we see that it contains parts of the executable part of
the script code embedded in a string as data. Then we format the
string cleverly to produce a new string that looks exactly like the
entire shell script. It is a common pattern followed in many quines.
The quine does not read its own code but it reads some data defined
by the code and formats that data to look like its own code. However,
in pure machine code like this the lines between data and code are
blurred. Even if we try to keep the bytes we want to read at a
separate place in the memory and treat it like data, they would look
exactly like machine instructions, so one might wonder if there is
any point in trying to make a machine quine that does not read its
own bytes. Nevertheless the next section shows how to accomplish
this.
Proper Quines
If the thought of a machine code quine program reading its own bytes
from the memory makes you uncomfortable, here is an adapation of the
previous program that keeps the machine instructions to be executed
separate from the data bytes to be read by the program.
fc b3 02 b1 14 be 14 01 ac 92 b4 02 cd 21 e2 f8 4b 75 f0 c3
fc b3 02 b1 14 be 14 01 ac 92 b4 02 cd 21 e2 f8 4b 75 f0 c3
Here is how we can demonstrate this 40-byte program:
echo fc b3 02 b1 14 be 14 01 ac 92 b4 02 cd 21 e2 f8 4b 75 f0 c3 | xxd -r -p > foo.com
echo fc b3 02 b1 14 be 14 01 ac 92 b4 02 cd 21 e2 f8 4b 75 f0 c3 | xxd -r -p >> foo.com
dosbox -c 'MOUNT C .' -c 'C:\FOO > C:\OUT.COM' -c 'EXIT'
diff foo.com OUT.COM
Here is the disassembly:
$ ndisasm -o 0x100 foo.com
00000100 FC cld
00000101 B302 mov bl,0x2
00000103 B114 mov cl,0x14
00000105 BE1401 mov si,0x114
00000108 AC lodsb
00000109 92 xchg ax,dx
0000010A B402 mov ah,0x2
0000010C CD21 int 0x21
0000010E E2F8 loop 0x108
00000110 4B dec bx
00000111 75F0 jnz 0x103
00000113 C3 ret
00000114 FC cld
00000115 B302 mov bl,0x2
00000117 B114 mov cl,0x14
00000119 BE1401 mov si,0x114
0000011C AC lodsb
0000011D 92 xchg ax,dx
0000011E B402 mov ah,0x2
00000120 CD21 int 0x21
00000122 E2F8 loop 0x11c
00000124 4B dec bx
00000125 75F0 jnz 0x117
00000127 C3 ret
The first 20 bytes is the executable part of the program. The next 20
bytes is the data read by the program. The executable bytes are
identical to the data bytes. The executable part of the program has
an outer loop that iterates twice. In each iteration, it reads the
data bytes and writes them to standard output. Therefore, in two
iterations of the outer loop, it writes the data bytes twice. In this
manner, the output is identical to the program itself.
Here is another simpler 32-byte quine based on this approach:
b8 23 09 fe c0 a2 20 01 ba 10 01 cd 21 cd 21 c3
b8 23 09 fe c0 a2 20 01 ba 10 01 cd 21 cd 21 c3
Here are the commands to demostrate this quine:
echo b8 23 09 fe c0 a2 20 01 ba 10 01 cd 21 cd 21 c3 | xxd -r -p > foo.com
echo b8 23 09 fe c0 a2 20 01 ba 10 01 cd 21 cd 21 c3 | xxd -r -p >> foo.com
dosbox -c 'MOUNT C .' -c 'C:\FOO > C:\OUT.COM' -c 'EXIT'
diff foo.com OUT.COM
Here is the disassembly:
$ ndisasm -o 0x100 foo.com
00000100 B82309 mov ax,0x923
00000103 FEC0 inc al
00000105 A22001 mov [0x120],al
00000108 BA1001 mov dx,0x110
0000010B CD21 int 0x21
0000010D CD21 int 0x21
0000010F C3 ret
00000110 B82309 mov ax,0x923
00000113 FEC0 inc al
00000115 A22001 mov [0x120],al
00000118 BA1001 mov dx,0x110
0000011B CD21 int 0x21
0000011D CD21 int 0x21
0000011F C3 ret
This example too has two parts. The first half has the executable
bytes and the second half has the data bytes. Both parts are
identical. This example sets AH to 9 in the first instruction and
then later uses int 0x21 to invoke the DOS service that prints a
dollar-terminated string beginning at the address specifed in DS:DX.
When a .COM program starts, DS already points to the current code
segment, so we don't have to set it explicitly. The dollar symbol has
an ASCII code of 0x24 (decimal 36). We need to be careful about not
having this value anywhere within the the data bytes or this DOS
function would prematurely stop printing our data bytes as soon as it
encounters this value. That is why we set AL to 0x23 in the first
instruction, then increment it to 0x24 in the second instruction, and
then copy this value to the end of the data bytes in the third
instruction. Finally, we execute int 0x21 twice to write the data
bytes twice to standard output, so that the output matches the
program itself.
While both these programs take care not to read the same memory
region that is being executed by the CPU, the data bytes they read
look exactly like the executable bytes. This is what I meant when I
mentioned earlier that the lines between code and data are blurred in
an exercise like this. This is why I don't really see a point in
keeping the executable bytes separate from the data bytes while
writing machine code quines.
A Note on DOS Services
The self-printing programs presented above use int 0x21 which offers
DOS services that support various input/output functions. In the
first two programs, we selected the function to write a character to
standard output by setting AH to 2 before invoking this software
interrupt. In the next program, we selected the function to write a
dollar-terminated string to standard output by setting AH to 9.
The ret instruction in the end too relies on DOS services. When a
.COM program starts, the register SP contains 0xfffe. The stack
memory locations at offset 0xfffe and 0xffff contain 0x00 and 0x00,
respectively. Further, the memory address at offset 0x0000 contains
the instruction int 0x20 which is a DOS service that terminates the
program. As a result, executing the ret instruction pops 0x0000 off
the stack at 0xfffe and loads it into IP. This results in the
instruction int 0x20 at offset 0x0000 getting executed. This
instruction terminates the program and returns to DOS.
Relying on DOS services gives us a comfortable environment to work
with. In particular, DOS implements the notion of standard output
which lets us redirect standard output to a file. This lets us
conveniently compare the original program file and the output file
with the FC command and confirm that they are identical.
But one might wonder if we could avoid relying on DOS services
completely and still write a program that prints its own bytes to
screen. We definitely can. We could write directly to video memory at
address 0xb800:0x0000 and show the bytes of the program on screen. We
could also forgo DOS completely and let BIOS load our program from
the boot sector and execute it. The next two sections discuss these
things.
Writing to Video Memory Directly
Here is an example of an 18-byte self-printing program that writes
directly to the video memory at address 0xb800:0x0000.
fc b4 b8 8e c0 31 ff b1 12 b4 0a ac ab e2 fc f4 eb fd
Here are the commands to create and run this program:
echo fc b4 b8 8e c0 31 ff b1 12 b4 0a ac ab e2 fc f4 eb fd | xxd -r -p > foo.com
dosbox foo.com
With the default code page active, i.e., with code page 437 active,
the program should display an output that looks approximately like
the following and halt:
n++A+1 #|+*1/41/2Gn[?]d2
Now of course this type of output looks gibberish but there is a
quick and dirty way to confirm that this output indeed represents the
bytes of our program. We can use the TYPE command of DOS to print the
program and check if the symbols that appear in its output seem
consistent with the output above. Here is an example:
C:\>TYPE FOO.COM
n++A+1 #|+
1/41/2Gn[?]d2
C:\>
This output looks very similar to the previous one except that the
byte value 0x0a is rendered as a line break in this output whereas in
the previous output this byte value is represented as a circle in a
box. This method would not have worked if there were any control
characters such as backspace or carriage return that result in
characters being erased in the displayed output.
A proper way to verify that the output of the program represents the
bytes of the program would be to find each symbol in the output in a
chart for code page 437 and confirm that the byte value of each
symbol matches each byte value in the program. Here is one such chart
that approximates the symbols in code page 437 with Unicode symbols:
cp437.html.
Here is the disassembly of the above program:
$ ndisasm -o 0x100 foo.com
00000100 FC cld
00000101 B4B8 mov ah,0xb8
00000103 8EC0 mov es,ax
00000105 31FF xor di,di
00000107 B112 mov cl,0x12
00000109 B40A mov ah,0xa
0000010B AC lodsb
0000010C AB stosw
0000010D E2FC loop 0x10b
0000010F F4 hlt
00000110 EBFD jmp short 0x10f
This program sets ES to 0xb800 and DI to 0. Thus ES:DI points to the
video memory at address 0xb800:0x0000. DS:SI points to the first
instruction of this program by default. Further AH is set to 0xa.
This is used to specify the colour attribute of the text to be
displayed on screen. Each iteration of the loop in this program loads
a byte of the program and writes it along with the colour attribute
to video memory. The lodsb instruction loads a byte of the program
from the memory address specified by DS:SI into AL and increments SI
by 1. AH is already set to 0xa. The value 0xa (binary 00001010) here
specifies black as the background colour and bright green as the
foreground colour. The stosw instruction stores a word from AX to the
memory address specified by ES:DI and increments DI by 2. In this
manner, the byte in AL and its colour attribute in AH gets copied to
the video memory.
Once again, if you are not happy about the program reading its own
executable bytes, we can keep the bytes we read separate from the
bytes the CPU executes. Here is a 54-byte program that does this:
fc b3 02 b4 b8 8e c0 31 ff be 1b 01 b9 1b 00 b4
0a ac ab e2 fc 4b 75 f1 f4 eb fd fc b3 02 b4 b8
8e c0 31 ff be 1b 01 b9 1b 00 b4 0a ac ab e2 fc
4b 75 f1 f4 eb fd
Here is how we can create and run this program:
echo fc b3 02 b4 b8 8e c0 31 ff be 1b 01 b9 1b 00 b4 | xxd -r -p > foo.com
echo 0a ac ab e2 fc 4b 75 f1 f4 eb fd fc b3 02 b4 b8 | xxd -r -p >> foo.com
echo 8e c0 31 ff be 1b 01 b9 1b 00 b4 0a ac ab e2 fc | xxd -r -p >> foo.com
echo 4b 75 f1 f4 eb fd | xxd -r -p >> foo.com
dosbox foo.com
With code page 437 active, the output should look approximately like
this:
n|++A+1 +-+- +*1/41/2GnKu+-[?]d2n|++A+1 +-+- +*1/41/2GnKu+-[?]d2
We can clearly see in this output that the first 27 bytes of output
are identical to the next 27 bytes of the output. Like the proper
quines discussed earlier, this one too has two halves that are
identical to each other. The executable code in the first half reads
the data bytes from the second half and prints the data bytes twice
so that the output bytes is an exact copy of all 54 bytes in the
program. Here is the disassembly:
$ ndisasm -o 0x100 foo.com
00000100 FC cld
00000101 B302 mov bl,0x2
00000103 B4B8 mov ah,0xb8
00000105 8EC0 mov es,ax
00000107 31FF xor di,di
00000109 BE1B01 mov si,0x11b
0000010C B91B00 mov cx,0x1b
0000010F B40A mov ah,0xa
00000111 AC lodsb
00000112 AB stosw
00000113 E2FC loop 0x111
00000115 4B dec bx
00000116 75F1 jnz 0x109
00000118 F4 hlt
00000119 EBFD jmp short 0x118
0000011B FC cld
0000011C B302 mov bl,0x2
0000011E B4B8 mov ah,0xb8
00000120 8EC0 mov es,ax
00000122 31FF xor di,di
00000124 BE1B01 mov si,0x11b
00000127 B91B00 mov cx,0x1b
0000012A B40A mov ah,0xa
0000012C AC lodsb
0000012D AB stosw
0000012E E2FC loop 0x12c
00000130 4B dec bx
00000131 75F1 jnz 0x124
00000133 F4 hlt
00000134 EBFD jmp short 0x133
This disassembly is rather long but we can clearly see that the bytes
from offset 0x100 to offset 0x11a are identical to the bytes from
offset 0x11b to 0x135. These are the bytes we see in the output of
the program too.
Boot Program
The 32-byte program below writes itself to video memory when executed
from the boot sector:
ea 05 7c 00 00 fc b8 00 b8 8e c0 8c c8 8e d8 31
ff be 00 7c b9 20 00 b4 0a ac ab e2 fc f4 eb fd
We can create a boot image that contains these bytes, write it to the
boot sector of a drive and boot an IBM PC compatible computer with
it. On booting, this program prints its own bytes on the screen.
On a Unix or Linux system, the following commands can be used to
create a boot image with the above program:
echo ea 05 7c 00 00 fc b8 00 b8 8e c0 8c c8 8e d8 31 | xxd -r -p > boot.img
echo ff be 00 7c b9 20 00 b4 0a ac ab e2 fc f4 eb fd | xxd -r -p >> boot.img
echo 55 aa | xxd -r -p | dd seek=510 bs=1 of=boot.img
Now we can test this boot image using DOSBox with the following
command:
dosbox -c cls -c 'boot boot.img'
We can also test this image using QEMU x86 system emulator as
follows:
qemu-system-i386 -fda boot.img
We could also write this image to the boot sector of an actual
physical storage device, such as a USB flash drive, and then boot the
computer with it. Here is an example command that writes the boot
image to the drive represented by the device path /dev/sdx.
cp a.img /dev/sdx
CAUTION: You need to be absolutely sure of the device path of the
device being written to. The device path /dev/sdx is only an example
here. If the boot image is written to the wrong device, access to the
data on that would be lost.
On testing this boot image with an emulator or a real computer, the
output should look approximately like this:
O| n+ +A+i+A+1 + |+ +*1/41/2Gn[?]d2
This looks like gibberish, however every symbol in the above output
corresponds to a byte of the program mentioned earlier. For example,
the first symbol (omega) represents the byte value 0xea, the second
symbol (club) represents the byte value 0x05, and so on. The chart at
cp437.html can be used to confirm that every symbol in the output
indeed represents every byte of the program.
Here is the disassembly of the program:
$ ndisasm -o 0x7c00 boot.img
00007C00 EA057C0000 jmp 0x0:0x7c05
00007C05 FC cld
00007C06 B800B8 mov ax,0xb800
00007C09 8EC0 mov es,ax
00007C0B 8CC8 mov ax,cs
00007C0D 8ED8 mov ds,ax
00007C0F 31FF xor di,di
00007C11 BE007C mov si,0x7c00
00007C14 B92000 mov cx,0x20
00007C17 B40A mov ah,0xa
00007C19 AC lodsb
00007C1A AB stosw
00007C1B E2FC loop 0x7c19
00007C1D F4 hlt
00007C1E EBFD jmp short 0x7c1d
00007C20 0000 add [bx+si],al
00007C22 0000 add [bx+si],al
...
The ellipsis in the end represents the remainder of the bytes that
contains zeroes and the boot sector magic bytes 0x55 and 0xaa in the
end. They have been omitted here for the sake of brevity.
When a computer boots, the BIOS reads the boot sector code from the
first sector of the boot device into the memory at physical address
0x7c00 and jumps to this address. Most BIOS implementations jump to
0x0000:0x7c00 but there are some implementations that jump to
0x07c0:0x0000 instead. Both these jumps are jumps to the same
physical address 0x7c00 but this difference poses a problem for us
because the offsets in our program depend on which jump the BIOS
executed. In order to ensure that our program can run with both types
of BIOS implementations, we use a popular trick of having the first
instruction of our program execute a jump to address 0x0000:0x7c05 in
order to reach the second instruction. This sets the register CS to 0
and IP to 0x7c05 and we don't have to worry about the differences
between BIOS implementations anymore. We can now pretend as if a BIOS
implementation that jumps to 0x0000:0x7c00 is going to load our
program.
The remainder of the program is similar to the one in the previous
section. However, there are some small but important differences.
While the DOS environment guarantees that AH and CH are initialised
to 0 when a .COM program starts, the BIOS offers no such guarantee
while loading and executing a boot program. This is why we use the
registers AX and CX (as opposed to only AH and CL) in the mov
instructions to initialise them. Similarly, while DOS initialises SI
to 0x100 when a .COM program starts, for a boot program, we set the
register SI ourselves.
If you feel uncomfortable about calling the above program a quine
because it reads its own bytes from the memory, we could have the
program read the bytes it needs to print from a separate place in
memory. We do not execute these bytes. We only read them and copy
them to video memory. The following 76-byte program does this:
ea 05 7c 00 00 fc bb 02 00 b8 00 b8 8e c0 8c c8
8e d8 31 ff be 26 7c b9 26 00 b4 0a ac ab e2 fc
4b 75 f1 f4 eb fd ea 05 7c 00 00 fc bb 02 00 b8
00 b8 8e c0 8c c8 8e d8 31 ff be 26 7c b9 26 00
b4 0a ac ab e2 fc 4b 75 f1 f4 eb fd
Here is how we can create a boot image with this:
echo ea 05 7c 00 00 fc bb 02 00 b8 00 b8 8e c0 8c c8 | xxd -r -p > boot.img
echo 8e d8 31 ff be 26 7c b9 26 00 b4 0a ac ab e2 fc | xxd -r -p >> boot.img
echo 4b 75 f1 f4 eb fd ea 05 7c 00 00 fc bb 02 00 b8 | xxd -r -p >> boot.img
echo 00 b8 8e c0 8c c8 8e d8 31 ff be 26 7c b9 26 00 | xxd -r -p >> boot.img
echo b4 0a ac ab e2 fc 4b 75 f1 f4 eb fd | xxd -r -p >> boot.img
echo 55 aa | xxd -r -p | dd seek=510 bs=1 of=boot.img
Here are the commands to test this boot image:
dosbox -c cls -c 'boot boot.img'
qemu-system-i386 -fda boot.img
The output should look like this:
O| n+ + +A+i+A+1 +&|+& +*1/41/2GnKu+-[?]d2O| n+ + +A+i+A+1 +&|+& +*1/41/2GnKu+-[?]d2
Here is the disassembly of this program:
$ ndisasm -o 0x7c00 boot.img
00007C00 EA057C0000 jmp 0x0:0x7c05
00007C05 FC cld
00007C06 BB0200 mov bx,0x2
00007C09 B800B8 mov ax,0xb800
00007C0C 8EC0 mov es,ax
00007C0E 8CC8 mov ax,cs
00007C10 8ED8 mov ds,ax
00007C12 31FF xor di,di
00007C14 BE267C mov si,0x7c26
00007C17 B92600 mov cx,0x26
00007C1A B40A mov ah,0xa
00007C1C AC lodsb
00007C1D AB stosw
00007C1E E2FC loop 0x7c1c
00007C20 4B dec bx
00007C21 75F1 jnz 0x7c14
00007C23 F4 hlt
00007C24 EBFD jmp short 0x7c23
00007C26 EA057C0000 jmp 0x0:0x7c05
00007C2B FC cld
00007C2C BB0200 mov bx,0x2
00007C2F B800B8 mov ax,0xb800
00007C32 8EC0 mov es,ax
00007C34 8CC8 mov ax,cs
00007C36 8ED8 mov ds,ax
00007C38 31FF xor di,di
00007C3A BE267C mov si,0x7c26
00007C3D B92600 mov cx,0x26
00007C40 B40A mov ah,0xa
00007C42 AC lodsb
00007C43 AB stosw
00007C44 E2FC loop 0x7c42
00007C46 4B dec bx
00007C47 75F1 jnz 0x7c3a
00007C49 F4 hlt
00007C4A EBFD jmp short 0x7c49
00007C4C 0000 add [bx+si],al
00007C4E 0000 add [bx+si],al
...
This program has two identical halves. The first half from offset
0x7c00 to offset 0x7c25 are executable bytes. The second half from
offset 0x7c26 to 0x7c4b are the data bytes read by the executable
bytes. The executable part of the code has an outer loop that uses
the register BX as the counter variable. It sets BX to 2 so that the
outer loop iterates twice. In each iteration, it reads data bytes
from the second half of the program and prints them. The code to read
bytes and print them is very similar to our earlier program. Since
the data bytes in the second half are identical to the executable
bytes in the first half, printing the data bytes twice amounts to
printing all bytes of the program.
While this program does avoid reading the bytes that the CPU
executes, the data bytes look exactly like the executable bytes.
Although I do not see any point in trying to avoid reading executable
bytes in an exercise like, this program serves as an example of a
self-printing boot program that does not execute the bytes it reads.
Comments | #assembly | #programming | #dos | #technology
---------------------------------------------------------------------
Home Links Feed Subscribe About GitHub Mastodon
(c) 2001-2024 Susam Pal