COFF¶
Level: 201 · for anyone with a hex editor open
One line: A COFF object opens with the machine type, not a signature, so 4c 01 is an i386 object only because a table says so; its section names are eight bytes with an escape hatch that writes a decimal offset in ASCII (/4) into a binary header; and its symbol names are either eight bytes in place or four zero bytes followed by an offset — two shapes told apart by whether the first word is zero.
What the loader reads¶
The Common Object File Format is the 1983 Unix System V object format, long replaced there by ELF and still alive as the object and library format of every Windows toolchain, where Microsoft's PE Format ↗ page documents it as the COFF file header that follows PE\0\0. On its own — a .obj from cl, an .o from a TI or DJGPP toolchain — the same 20 bytes start the file:
| offset | field | width | what it says |
|---|---|---|---|
| 0 | f_magic |
2 | the machine: 0x14C i386, 0x8664 x86-64, 0xAA64 ARM64, 0x1C0 ARM, and some forty others |
| 2 | f_nscns |
2 | how many 40-byte section headers follow |
| 4 | f_timdat |
4 | seconds since 1970 |
| 8 | f_symptr |
4 | file offset of the symbol table |
| 12 | f_nsyms |
4 | how many 18-byte symbol entries it holds |
| 16 | f_opthdr |
2 | size of an optional header: 0 in an object, 224 or 240 in a PE |
| 18 | f_flags |
2 |
Then the section headers, each a name field of eight bytes and nine 32- and 16-bit numbers; then the sections' bytes; then the symbol table; and immediately after it the string table, whose first four bytes are its own length.
In Python¶
Verified output of coff_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. THE FIRST TWO BYTES ARE A MACHINE TYPE, NOT A SIGNATURE
------------------------------------------------------------------------
bytes 0..2 4c 01 read LE 0x014c I386
4c 01 0x014c a machine the table knows: I386
64 86 0x8664 a machine the table knows: AMD64
64 aa 0xaa64 a machine the table knows: ARM64
00 00 0x0000 IMAGE_FILE_MACHINE_UNKNOWN: defined, and refused by a guard
7f 45 0x457f not in the table: not COFF
There is no magic number. A file is COFF if its first 16-bit word
is a machine type the reader has heard of -- Ghidra's isValid() is
that lookup, plus one guard: 0x0000 is IMAGE_FILE_MACHINE_UNKNOWN,
which is 'defined', so a file that starts with 64 zero bytes is
refused by hand. Every other format in this chapter says its name;
this one says which CPU, and the reader infers the rest.
2. THE HEADER IS 20 BYTES
------------------------------------------------------------------------
4c 01 01 00 00 00 00 00 3f 00 00 00 02 00 00 00 00 00 00 00
f_magic 0x0000014c 332
f_nscns 0x00000001 1
f_timdat 0x00000000 0
f_symptr 0x0000003f 63
f_nsyms 0x00000002 2
f_opthdr 0x00000000 0
f_flags 0x00000000 0
f_symptr is the file offset of the symbol table, f_nsyms how many
18-byte entries it has, and the string table starts right after
the last one. f_opthdr is 0 in an object; in a PE it is 224 or 240.
3. A SECTION NAME IS EIGHT BYTES, OR A DECIMAL OFFSET IN ASCII
------------------------------------------------------------------------
name bytes 2f 34 00 00 00 00 00 00 b'/4\x00\x00\x00\x00\x00\x00'
starts with '/', so the rest is a DECIMAL number in ASCII: 4
string table at 99, offset 4 there -> '.text.startup'
Eight bytes hold '.text' and not '.text.startup', so a long name is
stored in the string table and the name field holds its offset --
as text, in base 10, in a header where every other number is
binary. '/4' is the two ASCII bytes 2f 34, and int('4') is the read.
4. A SYMBOL NAME HAS TWO SHAPES
------------------------------------------------------------------------
symbol 0 6d 61 69 6e 00 00 00 00 eight bytes in place -> 'main'
value 0x0 section 1 type 0x20 class 2 aux 0
symbol 1 00 00 00 00 12 00 00 00 zeroes then offset 18 -> 'a_much_longer_symbol_name'
value 0x10 section 1 type 0x0 class 3 aux 0
The first four bytes decide: zero means 'the next four are an
offset', anything else means 'these eight are the name'. A name
cannot start with a NUL, so the two shapes cannot collide -- but
a name of exactly eight characters has no terminator at all.
5. THE STRING TABLE'S FIRST FOUR BYTES ARE ITS OWN SIZE
------------------------------------------------------------------------
at 99: 2c 00 00 00 size 44, counting these four bytes
contents b'.text.startup\x00a_much_longer_symbol_name\x00'
So the smallest string table is 04 00 00 00, and offset 4 is the
first string. Both escapes above -- '/4' and the symbol's offset --
point at this table, and both count from its size field.
A format with no signature¶
There is nothing at offset 0 that says COFF. The first word is the target machine, and a file is COFF to a reader if that word is one the reader has a row for. Ghidra's CoffFileHeader.isValid() is that test, CoffMachineType.isMachineTypeDefined(magic), plus a guard for the value 0 — which is IMAGE_FILE_MACHINE_UNKNOWN, a defined machine, so a file of zeros would otherwise qualify; if the first 64 bytes are all zero the file is refused by hand. file(1)'s rules take the same shape — 0 uleshort 0x014C Intel 80386, with a plausibility check on the section count in front — because there is nothing else to test.
That is the same predicament as a text file with no declared encoding (file guesses), one chapter along: no signature, so the reader decides from plausibility, and a file that happens to open with 4c 01 is an i386 object until the section count says otherwise.
The decimal escape in a binary header¶
A section name is eight bytes, and .text.startup is thirteen. COFF's answer, kept by PE, is that a name beginning with / is not a name — the seven bytes after it are a number written in ASCII decimal, an offset into the string table. Section 3 above builds /4 and reads .text.startup back through it. Every other number in the header is binary; this one is text, in base ten, with no prefix, and a parser that treats the name field as opaque bytes shows /4 to the user, which is what the older objdumps did. Which base did you mean? has the general version of the trap.
A symbol name has the same problem and a different answer. It too gets eight bytes, and a longer name is stored as four zero bytes and a 32-bit offset — binary, this time. A name cannot begin with a NUL, so the first word being zero is unambiguous, and section 4 reads one of each. The two escapes point at the same string table and both count from the table's size field, so offset 4 is the first string and 04 00 00 00 is the smallest table there is.
What Ghidra checks¶
CoffLoader ↗ parses the header, refuses a file isValid() rejects, and then asks a second question: whether the file is Microsoft's dialect — a .drectve or .debug$S section, or a symbol named the way Visual Studio names them. Two loaders share the code, Common Object File Format (COFF) and MS Common Object File Format (COFF) (MSCoffLoader, which is not on the import list this chapter follows), and each takes only the files whose dialect matches its own.
If you are coming from Python or ABAP¶
Python. struct.unpack_from('<HHIIIHH', data, 0) is the header. For the name field take the eight bytes as 8s, and test startswith(b'/') before rstrip(b'\0') — then int(name[1:]), which is the one place in this chapter an int() call parses bytes out of a binary structure. For a symbol, struct.unpack_from('<I', entry, 0) == (0,) picks the shape. Nothing in the standard library reads COFF; pefile reads the PE flavour and is a third-party install.
ABAP. (Not machine-checked — CI cannot run ABAP.) A fixed-width name field with an escape for the long case is a familiar shape: a 40-character MATNR and a separate long-text table, or a SOLI line that says continued in the next line. The COFF lesson is that the escape has to be unambiguous in the field itself — a leading / in one case, a leading zero word in the other — and that an escape written in a different base from its neighbours is a reading error waiting to happen. Slice +off(len), test the first byte, and convert the decimal with a plain assignment to i, which reads digits and nothing else.
Try it¶
- On a machine with a C compiler that emits COFF — MSVC, MinGW,
clang --target=x86_64-pc-windows-msvc -c— compile any file andxxd -l 20the object. Two bytes of machine, no magic. - Find the section table with
20 + 0(there is no optional header) and dump 40 bytes per section. Look for a name that starts with/. f_symptrandf_nsymsgive you the symbol table;f_symptr + 18 × f_nsymsis the string table. Dump its first four bytes and compare with what follows.objdump -hordumpbin /HEADERSon the same file, and match every long name to its/offset.- Feed a file starting
4c 01tofile(1)and to Ghidra's importer. Both will call it COFF, and only the section count stands between that and a wrong answer.
See also¶
- PE — the same 20 bytes, one signature later, and the optional header this one leaves at size 0
- ELF — the format that replaced COFF on Unix and kept its section-name-as-offset idea
- Which base did you mean? —
/4is a number written in a base the field does not state fileguesses — what recognising a file by plausibility rather than signature looks like for text- The NUL byte — the terminator an eight-byte name may not have
- PE Format, COFF file header ↗ — the machine table and both name escapes, as Microsoft documents them