OMF¶
Level: 201 · for anyone with a hex editor open
One line: An OMF module is a stream of records, each a type byte, a 16-bit length and a checksum that makes the whole record sum to zero — unless it is zero, which means not computed and is accepted — with every name a length-prefixed string and the low bit of a record's type saying whether its fields are 16 or 32 bits wide.
What the loader reads¶
The Relocatable Object Module Format is Intel's 1981 object format for the 8086, later the Tool Interface Standards OMF 1.1 ↗, and the .obj format of every DOS and 16-bit Windows compiler, Borland's, Watcom's and Microsoft's. It has no header. A module is records, back to back, each:
| field | width | what it says |
|---|---|---|
| type | 1 | 80 THEADR, 96 LNAMES, 98 SEGDEF, 90 PUBDEF, A0 LEDATA, 9C FIXUPP, 8A MODEND, and some forty more; the low bit selects 32-bit fields |
| length | 2, little-endian | how many bytes follow, the checksum included |
| body | the record's fields, with every string as a length byte and the bytes | |
| checksum | 1 | the byte that makes the record sum to zero mod 256 — or zero, meaning nobody computed it |
The first record is THEADR or LHEADR, naming the module, and that is what a reader recognises the format by.
In Python¶
Verified output of omf_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. A RECORD IS A TYPE, A LENGTH, A BODY AND A CHECKSUM
------------------------------------------------------------------------
80 08 00 06 63 61 66 65 2e 63 52
type 0x80 THEADR
length 08 00 = 8, little-endian, counting the checksum
body 06 63 61 66 65 2e 63 a length byte, then the name
checksum 0x52 sum of every byte of the record mod 256 = 0
The same three fields Framing a format asks for -- type, length,
check -- with Intel's checksum rule from Intel HEX: the whole record,
type and length included, sums to zero. This is Intel's 1981 8086
object format, and the 8086's byte order: little-endian throughout.
2. THE MODULE, RECORD BY RECORD
------------------------------------------------------------------------
0x80 THEADR length 8 checksum 0x52 ok ['cafe.c']
0x96 LNAMES length 13 checksum 0x95 ok ['', '_TEXT', 'CODE']
0x98 SEGDEF length 7 checksum 0x12 ok 48 00 01 02 03 01
0xa0 LEDATA length 9 checksum 0xc0 ok 01 00 00 63 61 66 c3 a9
0x8a MODEND length 2 checksum 0x74 ok 00
THEADR names the module, LNAMES lists every name any later record
will refer to by index, SEGDEF defines a segment by those indexes,
LEDATA carries bytes for it, MODEND ends the module. No offsets:
every record is found by walking from the one before.
3. EVERY STRING HAS A LENGTH BYTE
------------------------------------------------------------------------
LNAMES body 00 05 5f 54 45 58 54 04 43 4f 44 45
name 1 length 0 b''
name 2 length 5 b'_TEXT'
name 3 length 4 b'CODE'
No terminator anywhere: the first name is the empty string, one
byte long, which is what index 1 in a SEGDEF means by 'no name'.
A name can be 255 bytes at most and may hold any byte, including
a NUL, which a C string could not. Ghidra's OmfUtils.readString
is exactly 'read a byte, read that many'.
4. THE LOW BIT OF THE TYPE IS THE WIDTH OF THE FIELDS
------------------------------------------------------------------------
98 07 00 48 00 01 02 03 01 12 type 0x98 = SEGDEF segment length 0x100 in 2 bytes
99 09 00 48 00 00 01 00 02 03 01 0f type 0x99 = SEGDEF32 segment length 0x10000 in 4 bytes
0x98 and 0x99 are one record type; the low bit says whether its
numeric fields are 16 or 32 bits, and Ghidra masks it off before
looking the type up. The 8086 format grew 32-bit fields for the
386 by spending one bit that was always zero.
5. A CHECKSUM OF ZERO MEANS 'NOT COMPUTED', AND IS ACCEPTED
------------------------------------------------------------------------
as written checksum 0x52 sum 0x00 valid
checksum byte set to 0 checksum 0x00 sum 0xae valid
one byte of the name changed checksum 0x52 sum 0xe0 INVALID
'Some compilers just set this to zero', says the comment in Ghidra's
OmfRecord.validCheckSum, and so a zero passes. A check that can be
switched off by the writer is a check the reader cannot rely on --
and a record damaged in a way that happens to leave the byte at
zero is, to this reader, a record nobody checked.
6. HOW GHIDRA RECOGNISES A MODULE
------------------------------------------------------------------------
a THEADR type 0x80 length 8 name length 6 OMF
an LHEADR type 0x82 length 5 name length 3 OMF
a THEADR with the wrong length type 0x80 length 9 name length 6 not OMF
an LNAMES first type 0x96 length 13 name length 0 not OMF
OmfFileHeader.checkMagicNumber: the first record must be THEADR
or LHEADR, and its length must be the name's length plus two --
the name byte and the checksum. There is no magic; the first
record's arithmetic is the signature.
Three fields, and Intel's checksum again¶
Section 1 is a record, and it is the three fields Framing a format asks for — type, length, check — in the same company's earlier format, with the same rule for the check: add up every byte of the record and expect zero. Intel HEX has the record mark outside the sum and a text frame; OMF is binary, little-endian like the 8086, and puts the type and the length inside the sum. Section 2 walks a five-record module, and there is no table of offsets anywhere: a reader finds each record by adding the previous one's length, and the format's only structure is that order.
Every string has a length byte¶
Section 3: OMF has no NUL-terminated strings at all. Every name is a byte of length and that many bytes, so a name can be at most 255 long, may hold any byte including a NUL, and needs no terminator — the NE and PEF formats do the same, against ELF, Mach-O and PE, which run to a NUL. LNAMES lists every name a later record will use, and later records refer to them by index, from 1; index 0 means no name, and the first name in the list is conventionally the empty string, one byte long, so that index 1 can mean no name too.
The low bit is the width¶
Section 4: SEGDEF is 0x98, and 0x99 is the same record with 32-bit length fields — every type on the list with a 16-bit field has an odd twin, and Ghidra masks the bit off before looking the type up. It is the ELF class byte per record rather than per file: a 386 compiler emits odd types for segments over 64 KB and even ones otherwise, in the same module. One bit that was always zero in 1981 bought the format its 32-bit future.
A check that can be switched off¶
Section 5 is the finding. OmfRecord.validCheckSum accepts a checksum byte of zero without adding anything up — "Some compilers just set this to zero," the comment says — so a writer can opt out, and a record damaged in a way that leaves the byte at zero is, to this reader, a record nobody checked. Compare a DEX file, whose two hashes cannot be declined, or Intel Hex, where a zero checksum is simply a checksum that happens to be zero. Knowing what a check does not cover was the second half of adding one; knowing whether it was run is the third.
What Ghidra checks¶
OmfLoader ↗ calls OmfFileHeader.checkMagicNumber, which reads the first record's type, masks the low bit, requires THEADR or LHEADR, and then requires the record's length to equal the name's length byte plus two — section 6 tries four openings against that rule. There is no magic number; the first record's arithmetic is the signature. The name it shows is Relocatable Object Module Format (OMF), and a sibling Omf51Loader, not on this list, reads Intel's separate OMF-51 format for the 8051.
If you are coming from Python or ABAP¶
Python. bytes([len(b)]) + b writes a name and body[pos + 1:pos + 1 + body[pos]] reads one; struct.unpack_from('<H', rec, 1) is the length and sum(rec) & 0xFF == 0 the check, with or rec[-1] == 0 if you want Ghidra's leniency and a comment saying so. Walking a module is a while pos < len(data) loop that adds 3 + length each time, and a record type looked up as rectype & 0xFE.
ABAP. (Not machine-checked — CI cannot run ABAP.) A stream of typed, length-prefixed records read in order is an IDoc without its control record, or a TAB-less flat file with a record type in column one — the shape every interface has when it has no index. Two OMF rules transfer directly: a length that includes its own check byte has to be subtracted before the slice, and a checksum field that may legitimately be zero has to be tested for presence before it is tested for correctness, or the absence reads as a pass.
Try it¶
- Find any 16-bit
.obj— a DOS compiler's output, or an old Windows SDK's.lib, which is OMF records wrapped in a library header — andxxd -l 3it. The first byte is80or82, and the next two are a length. - Read that length, add three, and
xxd -s <that> -l 3: the next record's type. - Add up every byte of the first record, mod 256. Then find a record whose last byte is
00and decide what it proves. grep -cfor99,9DorA1type bytes in a 386-era object and a DOS one.- Write the same name as an OMF string, a NUL-terminated string and a Java
writeUTFstring, and compare the three byte counts.
See also¶
- A record has to say what it is, how long it is, and whether it arrived — the three fields, and Intel's other checksum
- Intel Hex — the same company's hex-text format, where zero is a checksum like any other
- NE and PEF — the other two formats here with length-prefixed strings
- ELF — a width chosen per file rather than per record
- The NUL byte — the terminator a length byte makes unnecessary
- TIS OMF Specification 1.1 ↗ — the record types and the checksum rule, as this page checked them