MZ¶
Level: 101 → 201 · for anyone with a hex editor open
One line: An MZ header measures its file in 512-byte pages and its header in 16-byte paragraphs, so DOS computes an executable's size as (pages − 1) × 512 + bytes-on-the-last-page — on the PE launcher pip ships that is 1,168 bytes of a 108,032-byte file, the stub is all DOS can see — and Ghidra's MZ loader takes only the files that PE and NE have declined.
What the loader reads¶
The 1981 DOS executable header is 28 bytes of 16-bit little-endian fields, followed by relocations, followed by the program. Its initials are Mark Zbikowski's, who designed it. Almost none of its numbers are in bytes:
| offset | field | unit | what it says |
|---|---|---|---|
| 0 | e_magic |
— | 4d 5a, the letters MZ |
| 2 | e_cblp |
bytes | how many bytes of the last 512-byte page are used; 0 means all of them |
| 4 | e_cp |
pages of 512 | how many pages the file occupies |
| 6 | e_crlc |
entries | how many relocations follow |
| 8 | e_cparhdr |
paragraphs of 16 | how long this header is, relocations included |
| 10 | e_minalloc, e_maxalloc |
paragraphs | memory wanted beyond the program |
| 14 | e_ss, e_sp |
— | the initial stack, SS relative to the load segment |
| 18 | e_csum |
— | a checksum nobody fills in |
| 20 | e_ip, e_cs |
— | the entry point, CS relative to the load segment |
| 24 | e_lfarlc |
bytes | where the relocation table is — the one offset in bytes |
| 26 | e_ovno |
— | overlay number |
60 (0x3C) |
e_lfanew |
bytes | not in the 1981 header: where NE and PE put the offset of their own header |
The size of the file, as far as DOS is concerned, is (e_cp − 1) × 512 + e_cblp, and the header is e_cparhdr × 16 of it. The program is what lies between.
In Python¶
Verified output of mz_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. THE HEADER, IN UNITS THAT ARE NOT BYTES
------------------------------------------------------------------------
4d 5a 90 00 03 00 00 00 04 00 00 00 ff ff 00 00 b8 00 00 00 00 00 00 00 40 00 00 00
e_magic 0x5a4d 23117 'MZ'
e_cblp 0x0090 144 bytes used in the LAST 512-byte page
e_cp 0x0003 3 pages of 512 bytes in the file
e_crlc 0x0000 0 relocation entries
e_cparhdr 0x0004 4 header size, in 16-byte paragraphs
e_minalloc 0x0000 0 extra paragraphs needed
e_maxalloc 0xffff 65535 extra paragraphs wanted
e_ss 0x0000 0 initial SS, relative
e_sp 0x00b8 184 initial SP
e_csum 0x0000 0 checksum, usually 0
e_ip 0x0000 0 initial IP
e_cs 0x0000 0 initial CS, relative
e_lfarlc 0x0040 64 offset of the relocation table
e_ovno 0x0000 0 overlay number
file size DOS computes (e_cp - 1) * 512 + e_cblp = (3 - 1) * 512 + 144 = 1168
header size e_cparhdr * 16 = 4 * 16 = 64
load module bytes 64 .. 1168, 1104 of them
These are the numbers from the DOS stub of pip's t64.exe, a 108,032-
byte PE32+ launcher. DOS reads 1,168 of those bytes and stops, and
the stub inside that region is what prints the one sentence DOS
users of it ever see. No field here can count higher than 65,535
pages, which is 32 MB, and a 16-bit page count was plenty in 1981.
2. e_cblp = 0 MEANS THE LAST PAGE IS FULL
------------------------------------------------------------------------
e_cblp 144 e_cp 3 -> 1168 bytes
e_cblp 0 e_cp 3 -> 1536 bytes 0 bytes used of the last page means all 512
e_cblp 1 e_cp 3 -> 1025 bytes
e_cblp 512 e_cp 3 -> 1536 bytes 512 is never written: it would be 0 with one more page
3. THE RELOCATION TABLE IS PAIRS OF 16-BIT NUMBERS
------------------------------------------------------------------------
e_lfarlc 0x40 e_crlc 2 table bytes 03 00 00 00 10 00 01 00
entry 0 offset 0x0003 segment 0x0000 linear 0x00003: a word the loader adds the load segment to
entry 1 offset 0x0010 segment 0x0001 linear 0x00020: a word the loader adds the load segment to
segment:offset, 16 bits each, and the linear address is 16 * seg +
off, so one address has 4096 spellings. The table says where the
segment halves of far pointers sit, so DOS can patch them for
wherever the program was actually loaded.
4. TWO BYTES, TWO NUMBERS
------------------------------------------------------------------------
4d 5a b'MZ' little-endian 0x5a4d big-endian 0x4d5a Ghidra: MZ
5a 4d b'ZM' little-endian 0x4d5a big-endian 0x5a4d Ghidra: not MZ
The constant Ghidra compares, IMAGE_DOS_SIGNATURE, is 0x5a4d: the
letters M and Z read as a little-endian 16-bit number. The same
two bytes are 0x4d5a to a big-endian reader, and a file that
spells them ZM is 0x4d5a to this one. One picture, two numbers,
and a signature test picks one of them.
5. WHAT MAKES IT 'OLD-STYLE'
------------------------------------------------------------------------
e_lfanew at 0x3c 00 00 00 00 = 0
0x3c is inside the reserved words of the 1981 header. NE and PE
put a 32-bit offset there, pointing past the DOS stub at their own
header. Ghidra's MZ loader takes a file only when the signature
is MZ AND no NE header AND no PE header is found through e_lfanew:
it is the loader for what the other two decline.
Units that are not bytes¶
A 16-bit field cannot count past 65,535, and in 1981 a file could already be longer than that. So the header counts pages, and one field says how much of the last page is real; the header's own length is counted in paragraphs for the same reason, and because a paragraph is the unit of the 8086's segment registers. Section 1 above does the arithmetic on the values from pip's t64.exe: three pages, 144 bytes used of the third, so 1,168 bytes — and the real file is 108,032 bytes long. DOS never reads the other 106,864. That is the arrangement every Windows executable still carries: a DOS program in front that prints one sentence, and a header for it that measures only that program.
Which base did you mean? was about a field whose base is unstated. This is the neighbouring trap — a field whose unit is unstated in the file and stated in a document — and it is the reason a struct definition is not a specification.
Two bytes, two numbers¶
e_magic is two ASCII letters, and Ghidra's constant for it, IMAGE_DOS_SIGNATURE, is 0x5A4D: the bytes 4d 5a read as a little-endian 16-bit number. The same two bytes are 0x4D5A to a big-endian reader, and a file spelling them ZM is 0x4D5A to this one — section 4 prints all four. A text signature is one picture and two numbers, and the program that tests it has picked one. The DBG header's Ghidra class keeps both constants for its DI, which is the same fact admitted.
What Ghidra checks¶
MzLoader ↗ accepts a file when e_magic == 0x5A4D and no NE header and no PE header is found through e_lfanew — the check hasNewExeHeader() reads e_lfanew only when it is at most 0x10000, and hasPeHeader() when it is at most 0x1000000. So the same bytes are three files to three loaders, and MZ is the one that takes what the other two decline, which is why its name is Old-style DOS Executable (MZ). It also insists the chosen language have a segmented address space: the relocation entries are segment:offset pairs, sixteen bits each, and 16 × segment + offset is the address, so one linear address has 4,096 spellings.
If you are coming from Python or ABAP¶
Python. Fourteen Hs: struct.unpack('<14H', data[:28]). Do the size arithmetic yourself, because nothing in the standard library knows an MZ header, and check e_cblp == 0 before you subtract — it is the one value that means more rather than none. e_lfanew is struct.unpack_from('<I', data, 0x3C), and a value of 0 there, with the reserved words around it, is what an old-style file looks like.
ABAP. (Not machine-checked — CI cannot run ABAP.) A field in units other than its type is ordinary here: a quantity in a unit of measure, an amount in a currency with a scale, a length in characters that is not a length in bytes (Fixed-width byte fields). The habit that transfers is naming the unit next to the field, e_cp pages, because a reader who sees 3 and 144 and does not know the units will compute something plausible and wrong — which is exactly what a program does with 0x5A4D and 0x4D5A.
Try it¶
xxd -l 28 file.exeon any Windows executable, or pip'sdistlib/t32.exe. Reade_cblp,e_cpande_cparhdr, do the arithmetic, and compare withls -l.xxd -s 60 -l 4on the same file. Thenxxd -s <that value> -l 4:PE\0\0, orNE, or nothing.strings -n 8 file.exe | head -1. That sentence is inside the 1,168 bytes.- Find a genuine DOS program — a
.EXEfrom a FreeDOS package — and check thate_lfanewis 0 or garbage, and that the size arithmetic comes out to the whole file. - Write down what
e_cblp = 0ande_cp = 1mean for the file size before you look it up.
See also¶
- PE — what
e_lfanewpoints at today, and why the DOS stub is still there - NE — what it pointed at in 1990, with its own units: sectors whose size is a shift count
- Which base did you mean? — the base of a field is not in the field; neither is its unit
- Fixed-width byte fields — a length that is not in bytes, met in an interface rather than a header
- The bytes do not say which end —
4d 5ais0x5A4Donly to a little-endian reader - OSDev wiki: MZ ↗ — the reference Ghidra's own loader cites for the header layout and the size formula