All 32bit or 64bit binary EXE-files for DOS, NT (Windows), or OS/2 begin with a 16bit DOS stub. They start with the magic MZ, the magic ZM might also work. The stub can be a useful DOS 16bit application, e.g., start a DOS extender to run the 32bit code after the stub, or offer (almost) the same functionality under 16bit DOS as the following code.

Older 32bit NT versions supported 16bit OS/2 NE (new executable) code, and so 16bit DOS MZ code combined with the corresponding 16bit OS/2 NE could run under 16bit DOS, 16bit OS/2, 32bit OS/2, and 32bit NT. This kind of "portable" application fails for 64bit NT platforms, but even 64bit PE EXE files still start with a 16bit DOS MZ stub. Typically the stub running under 16bit DOS reports an error and terminates itself with exit code 1, where 1 stands for FAIL instead of 0 for OKAY. All non-zero exit codes (1..255 for DOS) can indicate various errors.


A minimal still working DOS stub can simply terminate itself; its assembler source would be mov ax,4C01h followed by int 21h, where INT 21h is the DOS function interrupt (two bytes), AH=4Ch is the terminate process function number (one byte), AL=01h is the exit code (one byte), and the 16bit MOV opcode uses another byte. The DOS binary including the MZ header then needs 37=32+5 bytes with a decent linker.

For a smaller stub try mov ah,4Ch. The apparently undefined exit code in AL will be either 255 (FFh) or rarely 00h, because the DOS loader initializes AL to indicate a valid (00) or invalid (FF) drive letter in FCB1. Likewise AH corresponds to FCB2 for a drive letter in the second command line argument. FCBs are ancient DOS 1 and CP/M history, and the minimal stub started without arguments or no drive letter in the first argument should end with exit code 255. If the first argument contains a valid drive letter as in the stub exit code would be 0, if the loader still supports this ancient feature, and if drive C: is in fact valid.

Ignoring this FCB1 cruft the smaller stub needs 36=32+4 bytes. Combined with NE, PE, or other code formats using MZ stubs these 36 bytes end up in 68=64+4 bytes header, followed by four NUL bytes and the begin of the real binary, e.g., magic PE plus two NUL bytes at offset 72 (48h). This offset is also noted at the end of the MZ header at offset 60 (3Ch), just before the stub code, hex. B44C CD21 in this example. If you have no assembler and/or linker to create this stub use the following ASCII input with a hex. encoder:

4D5A2400 01000000 02002100 FFFF0100 00020000 00000000 20000000 00000000 B44CCD21

If all goes well the MD5 hash for the 36 bytes in a resulting binary DOSTUB.EXE should be a32aeee17d2424fb27036986a9a97b1a. Your 32bit or 64bit linker should have a stub option to specify this or another stub instead of its default stub. Of course you could also try something like X: hlt and jmp short X (3 bytes) or even int 18h (2 bytes, supported by DosBox).

Portable Executable

Linked with a PE binary some values in the MZ header for the stub are changed:

You can move the four DOS code bytes from 64 (40h) to 36 (28h); revert the header size to 2; adjust the start IP at offset 20 (14h) to 4 for 36=2*16+4; and clear the old DOS code bytes. The DOS stub and the PE code will still work. This suggests an even smaller stub containing no DOS code at all:

4D5A2000 01000000 02002000 FFFFF0FF 00020000 0000F0FF 20000000 00000000

MD5: d95cc14bcf20d46eee4478c1454e4a84. Here the size at offset 2 is 32 (20h). A minimum of 20h memory paragraphs are required at offset 10 for a total of 48=30h=20h+10h paragraphs with the PSP created by the DOS loader. 768=48*16 bytes are needed for the initial SP 512 (0200h) set at offset 16. The stack and the code segments FFF0h are set at offsets 14 and 22. These are relative distances in paragraphs from the PSP, the absolute addresses depend on where PSP plus binary are loaded.

A value 0 would begin at paragraph distance 10h directly after the PSP. For 10000h=FFF0h+10h memory wrap around, i.e., 16bit arithmetic modulo 10000h, ends up at distance 0 with CS=SS=PSP. The start address IP=0 set at offset 20 is the begin of the PSP. And an undamaged PSP begins with INT 20h, the old DOS program termination function with exit code 0.

This is ZEROSTUB.EXE, the smallest possible stub still working under DOS (and with my linker). As desired PE code linked with ZEROSTUB.EXE has its PE magic at offset 64 (40h) directly after the pointer at offset 60 (3Ch), no dummy DOS code, no obscure NUL bytes, no nonsense:


W3 validator Last update: 22 Jan 2014 19:00 by F.Ellermann