A COM file has no header and no table of contents, but it is rarely just a run of code. This page lists what is commonly found inside one, how each is recognised, and how the explorer shows it. Everything here is found from the bytes and the code, not read from fields, so it is the explorer’s reading of the file rather than a structure the format defines.
The program
Byte 0 of the file is at offset 0100h in memory, and it is the first
instruction. Every address in the program is an offset in its one segment, so a file
offset becomes an address by adding 0x100. Code and data are mixed
freely: there is nothing to say which bytes are which.
The explorer’s Code section disassembles the whole of the file that fits in the
segment, from 0100h, as 16-bit code. As in every Code section, an
instruction is decoded at every byte and laid out in columns: the first column is the
plain disassembly, each instruction straight after the one before, and the others hold
the instructions that start inside them, so a jump into the middle of an instruction
can be followed. The instructions are described in the
x86 instruction reference.
The entry jump
Since execution starts at the first byte, a program that wants data at the front
starts with a jump over it: E9 with a 16-bit displacement, or
EB with an 8-bit one. Assemblers produce this when the data is declared
before the code; resident programs do it on purpose, so that the part kept in memory
comes first and the set-up code at the end can be dropped (see
Ending the program).
When the first instruction is such a jump, forwards and inside the file, the explorer shows it as its own block and starts the rest of the program at its target.
What the jump skips
The bytes between the entry jump and its target are shown as one block. They may be data, such as a banner, tables, buffers or the variables of a resident part, or code that is called later; only the program says which. The Code section still disassembles them, so code among them can be read there.
An embedded .exe
Some COM files carry a complete MZ program inside, with a small loader of their own. The file starts with a jump over the MZ image to code after it, which sets up the embedded program the way DOS would load an EXE and starts it. The explorer looks for an MZ header within the first 64 bytes after the entry jump whose image, as its header gives it, ends before the jump target. It shows the bytes before it, the embedded program, and the bytes between its end and the jump target as separate blocks.
MS-DOS 7’s FORMAT.COM (Windows 95) is built this way: see the
worked example.
The ENUNS trailer
The COM tools of MS-DOS 7 (Windows 95), such as FORMAT.COM,
SYS.COM and KEYB.COM, end with seven extra bytes: the text
ENUNS and a 16-bit value.
| Offset from the end | Size | Contents |
|---|---|---|
| −7 | 5 | The text ENUNS |
| −2 | 2 | A value, little-endian |
The value differs from file to file: 0x1877 in FORMAT.COM,
0x0677 in SYS.COM and 0x0657 in
KEYB.COM, from one Windows 95 startup disk. It is not the file’s size.
Microsoft does not document the trailer, and we have not found what the value holds.
ENU is Microsoft’s code for US English, which suggests the trailer marks
the language of the build; that is a guess, not something we could confirm.
The explorer shows the trailer as its own block when a file ends with it, and stops the program before it.
Packed programs
Executable packers compress a COM program and add a small unpacker that runs first, expands the program in memory and jumps to it. The explorer names two by the marks they leave in the first 512 bytes:
- UPX – the text
UPX!. Its COM unpacker starts by checking that there is room for the expanded program, withcmp sp, …, and ends at once withINT 20hif there is not. - PKLITE – the text
PKLITE, part of its copyright notice.
The disassembly of a packed program shows the unpacker; the program itself is only in memory once it has run.
Bytes past 64 KB
Only the first 65,280 bytes (0xFF00) of a file fit in the program’s
segment after the PSP (see the size
limit). When a file is longer, the explorer ends the program and the Code section
at 0xFF00 and shows the rest as a block of its own, with a note on the
file.
Interrupt calls
A COM program talks to DOS and the BIOS through software interrupts, choosing the
service with a number in AH. The explorer’s Interrupt calls table lists
every INT instruction in the plain disassembly, with its address and the
interrupt’s name. For INT 21h (DOS), INT 10h (BIOS video) and INT 16h (BIOS keyboard)
it follows the instructions before it, back to the last jump, call or return, and
names the service when AH was set to a known value, by
mov ah, …, mov ax, … or by clearing it. For INT 33h (the mouse
driver) it does the same with AX.
The table is a reading of the code, not of the program’s behaviour. A few entries may
be data that happens to read as an INT, a service chosen by a value set
somewhere else is shown as not known, and a jump straight to an INT can
arrive with another value in AH.
Text
The Text table lists runs of at least six printable characters with at least four
letters among them, with their addresses. A run that ends with $ is marked:
that is the end marker of INT 21h service 09h, which prints a string, so
such runs are usually the program’s messages. A run followed by a zero byte is marked
too, the way C programs end their strings.
Worked example
FORMAT.COM from a Windows 95 startup disk, MS-DOS 7, is 49,543 bytes.
Its first 32 bytes:
00000000 E9 8D A8 43 6F 6E 76 65 72 74 65 64 00 00 00 00 ...Converted....
00000010 4D 5A 78 00 55 00 49 00 20 00 01 00 FF FF 13 05 MZx.U.I. .......
0x10The jump goes to 0xA990. The MZ header at file offset 0x10
gives an image of 85 pages with 0x78 bytes in the last, 43,128 bytes, with
73 relocations, so it ends at 0xA888, eight bytes before the jump target.
The explorer shows:
| File offset | Size | Block |
|---|---|---|
0x0000 | 3 | Entry jump: jmp 0xa990 |
0x0003 | 13 | Before the embedded program: “Converted” and four zero bytes |
0x0010 | 43,128 | Embedded MZ program: CS:IP 0532:1A00, 73 relocations |
0xA888 | 8 | After the embedded program: zero bytes |
0xA890 | 6,384 | Program from the jump target |
0xC180 | 7 | ENUNS trailer, value 0x1877 |
The code at the jump target is the loader. It finds its own address with a
call to the next instruction, works out the segment after the PSP, and
reads the embedded header’s fields through their addresses in memory:
[0x11e] is file offset 0x1E, the embedded program’s
e_ss.
a990 E8 00 00 call 0xa993
a993 5B pop bx
a994 50 push ax
a995 8C C0 mov ax, es
a997 05 10 00 add ax, 0x10 ; the segment after the PSP
a99a 8B 0E 1E 01 mov cx, word ptr [0x11e] ; e_ss
a99e 03 C8 add cx, ax
a9a0 89 4F FB mov word ptr [bx - 0x5], cx
a9a3 8B 0E 26 01 mov cx, word ptr [0x126] ; e_cs
a9a7 03 C8 add cx, ax
a9a9 89 4F F7 mov word ptr [bx - 0x9], cx
a9ac 8B 0E 20 01 mov cx, word ptr [0x120] ; e_sp
…
a9ba 8B 3E 28 01 mov di, word ptr [0x128] ; e_lfarlc
a9be 8B 16 18 01 mov dx, word ptr [0x118] ; e_cparhdr
…
a9c6 8B 0E 16 01 mov cx, word ptr [0x116] ; e_crlc
a9ca E3 1A jcxz 0xa9e6
So the COM file is a wrapper: the program that formats disks is the embedded EXE, and the wrapper relocates and starts it itself, as DOS would.