Module 2: COFF File Format Deep Dive
Every byte matters: headers, sections, symbols, strings, and relocations in raw binary.
Why This Module?
COFFLoader is fundamentally a COFF parser. To understand how it loads and executes BOFs, you must first understand the binary layout it parses. This module walks through the five major components of a COFF object file: the file header, section table, symbol table, string table, and relocation entries. These are the exact structures defined in COFFLoader.h that the loader reads.
Lab Files: step02-coff-inspector
A standalone COFF binary dumper. Compile hello_bof.c into a .o, then run coff_inspector hello_bof.o to see every COFF structure.
COFF Binary Layout
A COFF object file has a well-defined binary layout. Unlike a PE, there is no DOS header, no PE signature, and no optional header.[1] The file begins immediately with the COFF file header.
TEXTCOFF Object File Layout:
Offset 0x00: +--------------------------+
| COFF File Header | 20 bytes
| (coff_file_header_t) |
+--------------------------+
| Section Header 1 | 40 bytes each
| (coff_sect_t) |
+--------------------------+
| Section Header 2 |
+--------------------------+
| ... |
+--------------------------+
| Section Header N |
+--------------------------+
| Section 1 Raw Data | variable size
+--------------------------+
| Section 1 Relocations | 10 bytes each
+--------------------------+
| Section 2 Raw Data |
+--------------------------+
| Section 2 Relocations |
+--------------------------+
| ... |
+--------------------------+
| Symbol Table | 18 bytes per symbol
+--------------------------+
| String Table | variable size
+--------------------------+
COFF File Structure Overview
20 bytes
N × 40 bytes
Variable size
N × 18 bytes
Variable size
The COFF File Header
The file header is always 20 bytes and sits at offset 0.[1] COFFLoader defines it as coff_file_header_t:
Ctypedef struct coff_file_header {
uint16_t Machine; // 0x8664 = AMD64, 0x14C = i386
uint16_t NumberOfSections; // how many section headers follow
uint32_t TimeDateStamp; // compilation timestamp (often zero)
uint32_t PointerToSymbolTable; // file offset to the symbol table
uint32_t NumberOfSymbols; // total entries in symbol table
uint16_t SizeOfOptionalHeader; // always 0 for object files
uint16_t Characteristics; // flags (usually 0 for .obj)
} coff_file_header_t;
| Field | Offset | Size | Purpose |
|---|---|---|---|
| Machine | 0x00 | 2 | Target architecture. COFFLoader checks for 0x8664 (AMD64) or 0x14C (i386)[2] |
| NumberOfSections | 0x02 | 2 | Count of section headers immediately following this header |
| TimeDateStamp | 0x04 | 4 | Unix timestamp of compilation. Not used by the loader |
| PointerToSymbolTable | 0x08 | 4 | File offset to the symbol table. Critical for symbol resolution |
| NumberOfSymbols | 0x0C | 4 | Number of entries (including aux symbols). Used to locate the string table[3] |
| SizeOfOptionalHeader | 0x10 | 2 | Always 0 for object files (no optional header) |
| Characteristics | 0x12 | 2 | Flags. Usually 0 for unlinked objects |
Key Insight: Finding the String Table
The string table immediately follows the symbol table. Since each symbol entry is exactly 18 bytes, the string table starts at: PointerToSymbolTable + (NumberOfSymbols * 18). The first 4 bytes of the string table are a uint32_t giving the total size of the string table (including those 4 bytes). Symbol names longer than 8 characters are stored here and referenced by offset.
The Section Table
Immediately following the 20-byte file header is an array of section headers. Each header is 40 bytes,[4] defined as coff_sect_t:
C#pragma pack(push, 1)
typedef struct coff_sect {
char Name[8]; // section name (e.g., ".text\0\0\0")
uint32_t VirtualSize; // 0 for object files
uint32_t VirtualAddress; // 0 for object files
uint32_t SizeOfRawData; // size of section data in the file
uint32_t PointerToRawData; // file offset to the raw data
uint32_t PointerToRelocations; // file offset to relocation entries
uint32_t PointerToLineNumbers; // file offset to line numbers (usually 0)
uint16_t NumberOfRelocations; // count of relocation entries for this section
uint16_t NumberOfLinenumbers; // count of line number entries (usually 0)
uint32_t Characteristics; // flags: executable, readable, writable, etc.
} coff_sect_t;
#pragma pack(pop)
Common Sections in a BOF
| Section | Characteristics | Content |
|---|---|---|
.text | CODE | EXECUTE | READ | Compiled machine code (the go() function and helpers) |
.data | INITIALIZED | READ | WRITE | Initialized global/static variables |
.rdata | INITIALIZED | READ | Read-only data: string literals, constant tables |
.bss | UNINITIALIZED | READ | WRITE | Zero-initialized globals. SizeOfRawData is 0 (no file data) |
.xdata | INITIALIZED | READ | Exception handling unwind data (x64) |
.pdata | INITIALIZED | READ | Function table for structured exception handling |
Section Characteristics Flags
The Characteristics field is a bitmask.[5] COFFLoader defines the relevant flags:
C#define IMAGE_SCN_CNT_CODE 0x00000020 // section contains code
#define IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000080 // section contains uninitialized data (.bss)
#define IMAGE_SCN_MEM_EXECUTE 0x20000000 // section is executable
#define IMAGE_SCN_MEM_READ 0x40000000 // section is readable
#define IMAGE_SCN_MEM_WRITE 0x80000000 // section is writable
#define IMAGE_SCN_MEM_DISCARDABLE 0x02000000 // section can be discarded
The Symbol Table
The symbol table is an array of 18-byte entries[6] located at the file offset specified by PointerToSymbolTable. Each entry is defined as coff_sym_t:
Ctypedef struct coff_sym {
union {
char Name[8]; // short name (if <= 8 chars)
uint32_t value[2]; // value[0]==0 means value[1] is string table offset
} first;
uint32_t Value; // value depends on StorageClass and SectionNumber
uint16_t SectionNumber; // 1-based index of the section, or special values
uint16_t Type; // symbol type (0x20 = function)
uint8_t StorageClass; // IMAGE_SYM_CLASS_EXTERNAL (2), STATIC (3), etc.
uint8_t NumberOfAuxSymbols; // number of auxiliary symbol entries that follow
} coff_sym_t;
Symbol Name Resolution
Symbol names can be stored in two ways, depending on length:
TEXTIf the name is 8 characters or shorter:
first.Name[0..7] contains the name directly (null-padded)
If the name is longer than 8 characters:
first.value[0] == 0x00000000 (sentinel: first 4 bytes are zero)
first.value[1] == offset into string table
Example: Symbol name "__imp_KERNEL32$GetCurrentProcessId"
first.value[0] = 0x00000000
first.value[1] = 0x0000004A --> string table offset 0x4A
Important Symbol Fields
| Field | Key Values | Meaning |
|---|---|---|
| SectionNumber | 1, 2, 3... | 1-based index of the section containing this symbol |
| SectionNumber | 0 | IMAGE_SYM_UNDEFINED -- external symbol, must be resolved |
| StorageClass | 2 (EXTERNAL) | Symbol is globally visible or needs to be imported |
| StorageClass | 3 (STATIC) | Symbol is local to the section (e.g., section name) |
| Value | (offset) | For defined symbols: offset within the section. For undefined: 0 |
| NumberOfAuxSymbols | 0 or 1 | Auxiliary entries follow (e.g., section definition aux records) |
How COFFLoader Classifies Symbols
COFFLoader uses two helper functions to classify symbols.[7] A symbol is defined if its SectionNumber is greater than 0 (it exists in a section). A symbol is external if its StorageClass is IMAGE_SYM_CLASS_EXTERNAL (2).[6] An external symbol with SectionNumber == 0 is an unresolved import that must be linked at load time.
C// From COFFLoader -- symbol classification helpers
int coff_symbol_is_defined(coff_sym_t* sym) {
return (sym->SectionNumber > 0);
}
int coff_symbol_is_external(coff_sym_t* sym) {
return (sym->StorageClass == IMAGE_SYM_CLASS_EXTERNAL); // StorageClass == 2
}
The String Table
The string table immediately follows the symbol table. Its structure is simple:
TEXTString Table Layout:
Offset 0: uint32_t Size; // total size of string table (including this field)
Offset 4: char[] strings; // null-terminated strings packed sequentially
Example:
04 00 00 00 2E 74 65 78 74 00 5F 67 6F 00 5F 5F ....text._go.__
69 6D 70 5F 4B 45 52 4E 45 4C 33 32 24 47 65 74 imp_KERNEL32$Get
...
Symbols reference strings by offset from the START of the string table.
value[1] = 4 --> ".text"
value[1] = 10 --> "_go"
value[1] = 14 --> "__imp_KERNEL32$GetCurrentProcessId"
The first 4 bytes are the size field itself, so valid string offsets start at 4.[8] If the string table only contains the size field (size == 4), there are no long symbol names.
Relocation Entries
Each section can have its own relocation table. The relocation entries tell the loader which bytes in the section need to be patched once the final addresses of symbols are known. Each entry is 10 bytes:[9]
Ctypedef struct coff_reloc {
uint32_t VirtualAddress; // offset within the section to patch
uint32_t SymbolTableIndex; // index into the symbol table
uint16_t Type; // relocation type (architecture-specific)
} coff_reloc_t;
| Field | Size | Purpose |
|---|---|---|
| VirtualAddress | 4 | Byte offset within the section where the fixup must be applied |
| SymbolTableIndex | 4 | Index into the symbol table identifying the target symbol |
| Type | 2 | How to compute the fixup value (architecture-dependent) |
AMD64 Relocation Types (from COFFLoader.h)
C#define IMAGE_REL_AMD64_ADDR64 0x0001 // 64-bit absolute address
#define IMAGE_REL_AMD64_ADDR32NB 0x0003 // 32-bit address without image base (RVA)
#define IMAGE_REL_AMD64_REL32 0x0004 // 32-bit relative (RIP-relative)
#define IMAGE_REL_AMD64_REL32_1 0x0005 // REL32 + 1 byte displacement
#define IMAGE_REL_AMD64_REL32_2 0x0006 // REL32 + 2 byte displacement
#define IMAGE_REL_AMD64_REL32_3 0x0007 // REL32 + 3 bytes displacement
#define IMAGE_REL_AMD64_REL32_4 0x0008 // REL32 + 4 bytes displacement
#define IMAGE_REL_AMD64_REL32_5 0x0009 // REL32 + 5 bytes displacement
The REL32 type is the most common in x64 BOFs.[10] It computes: target_address - (fixup_address + 4). The variants REL32_1 through REL32_5 add an additional displacement of 1-5 bytes to account for instruction encodings where the relocation is not the last part of the instruction.[10]
Putting It All Together
When the compiler generates a call to BeaconPrintf, it emits a CALL instruction with a placeholder 32-bit offset, a symbol table entry for __imp_BeaconPrintf (or the architecture-specific variant), and a relocation entry pointing from the CALL instruction to the symbol. At load time, COFFLoader resolves the symbol to an actual memory address and patches the CALL instruction's offset to reach that address.
Edge Cases in COFF Parsing
A robust COFF parser must handle several edge cases that arise in real-world object files. Compilers do not always produce clean, predictable output, and a loader that assumes ideal input will crash or produce incorrect results.
Zero-Size Sections
The .bss section is the most common example of a section with SizeOfRawData == 0. Because .bss holds uninitialized data, the compiler sets its raw data size to zero and its PointerToRawData to zero as well.[4] The loader must still allocate memory for this section based on the VirtualSize field (or the symbol-derived size) and zero-fill it. COFFLoader handles this by checking SizeOfRawData before attempting to read section data from the file. If the loader blindly tries to memcpy zero bytes from a null pointer, the result is undefined behavior. Some compilers also emit empty .data sections when no initialized globals exist -- the loader should treat any section with SizeOfRawData == 0 as requiring allocation but no file read.
Sections With No Relocations
Read-only data sections like .rdata frequently have zero relocations. The NumberOfRelocations field will be 0 and PointerToRelocations may be 0 or may point to a valid but empty area. The loader must check NumberOfRelocations before iterating the relocation table. Attempting to dereference PointerToRelocations when there are no entries can lead to reading garbage data. Debug sections (.debug$S, .debug$T) and exception handling sections (.pdata, .xdata) may or may not carry relocations depending on compiler options.
Auxiliary Symbol Entries
When a symbol has NumberOfAuxSymbols > 0, the entries immediately following it in the symbol table are auxiliary records, not regular symbols.[6] Each auxiliary entry occupies the same 18 bytes as a regular symbol but has a completely different internal layout. Section definition symbols (StorageClass == IMAGE_SYM_CLASS_STATIC with a section name) typically have one auxiliary record containing the section length, number of relocations, and checksum. The loader must skip over auxiliary entries when iterating the symbol table. A common bug is to treat auxiliary entries as regular symbols, which corrupts symbol resolution. COFFLoader handles this by advancing the loop index by 1 + NumberOfAuxSymbols after processing each symbol.
Limitations of the COFF Format
COFF object files are intermediate artifacts -- they are designed to be consumed by a linker, not executed directly. This means COFF lacks many features present in the final PE executable format, and a COFF loader must work around these absences.
No ASLR Metadata
PE files include a DllCharacteristics field in the optional header with the IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE flag to signal ASLR compatibility.[1] COFF object files have no optional header at all, so there is no mechanism to declare ASLR support. The loader must position code in memory and handle all address computation through relocations without any base relocation directory. This is actually what makes COFF loading simpler in one sense: all addresses are computed from relocations rather than rebased from a preferred load address.
No TLS Directory
Thread Local Storage (TLS) in PE files is managed through the TLS directory in the optional header, which points to TLS callback functions and template data. COFF objects have no such directory. If a BOF uses __declspec(thread) variables, the resulting TLS references will appear as relocations against TLS-related sections, but there is no infrastructure for the loader to initialize per-thread storage. In practice, BOFs should avoid TLS variables entirely because the COFF loader has no way to set up the TLS index or call TLS callbacks.
No Exception Handling Registration
While COFF objects can contain .pdata and .xdata sections with unwind information, there is no mechanism to register this data with the Windows structured exception handling (SEH) system. In a PE file, the exception directory in the optional header points to the runtime function table. A COFF loader would need to manually call RtlAddFunctionTable to register unwind handlers, and most BOF loaders (including COFFLoader) skip this step.[7] This means that if a BOF triggers an exception, the stack unwinder will not find the BOF's frames, potentially causing process termination instead of graceful exception handling.
No Import or Export Directories
PE files have import and export directories that list DLL dependencies and exported functions. COFF objects express external dependencies purely through undefined symbols in the symbol table. The naming convention __imp_DLLNAME$FunctionName is a convention that COFFLoader parses to determine which DLL and function to resolve, but this is not a formal import table -- it is a string pattern that the loader interprets.[7]
Visualizing a Real BOF
Here is what a minimal BOF looks like at the binary level after compilation:
TEXTSource: void go(char* a, int l) { BeaconPrintf(0, "hello"); }
After compilation (x86_64-w64-mingw32-gcc -c):
COFF Header: Machine=0x8664, Sections=4, Symbols=12
Section 1: .text Size=0x2A, 1 relocation (the go() code)
Section 2: .data Size=0x00, 0 relocations (empty)
Section 3: .rdata Size=0x06, 0 relocations ("hello\0")
Section 4: .xdata Size=0x08, 0 relocations (unwind info)
Symbol Table:
[0] .text Section=1, Class=STATIC, Value=0
[2] .data Section=2, Class=STATIC, Value=0
[4] .rdata Section=3, Class=STATIC, Value=0
[6] go Section=1, Class=EXTERNAL, Value=0 <-- entry point
[7] __imp_BeaconPrintf Section=0, Class=EXTERNAL <-- unresolved import
Relocations for .text:
Offset=0x0F, Symbol=4 (.rdata), Type=REL32 <-- reference to "hello" string
Offset=0x1A, Symbol=7 (__imp_BeaconPrintf), Type=REL32 <-- call to BeaconPrintf
Pop Quiz: COFF Format
Q1: How does COFFLoader find the string table in a COFF file?
Q2: A symbol has SectionNumber=0 and StorageClass=2. What does this mean?
Q3: How are symbol names longer than 8 characters stored in the COFF symbol table?
References
- Microsoft Corporation, "PE Format -- COFF File Header (Object and Image)," Microsoft Learn, learn.microsoft.com/en-us/windows/win32/debug/pe-format#coff-file-header-object-and-image.
- Microsoft Corporation, "PE Format -- Machine Types," Microsoft Learn, learn.microsoft.com/en-us/windows/win32/debug/pe-format#machine-types.
- Microsoft Corporation, "PE Format -- COFF File Header," Microsoft Learn. The NumberOfSymbols field counts all entries including auxiliary symbol records.
- Microsoft Corporation, "PE Format -- Section Table (Section Headers)," Microsoft Learn, learn.microsoft.com/en-us/windows/win32/debug/pe-format#section-table-section-headers.
- Microsoft Corporation, "PE Format -- Section Flags," Microsoft Learn, learn.microsoft.com/en-us/windows/win32/debug/pe-format#section-flags.
- Microsoft Corporation, "PE Format -- COFF Symbol Table," Microsoft Learn, learn.microsoft.com/en-us/windows/win32/debug/pe-format#coff-symbol-table.
- TrustedSec, "COFFLoader -- Beacon Object File Loader," GitHub, github.com/trustedsec/COFFLoader.
- Microsoft Corporation, "PE Format -- COFF String Table," Microsoft Learn, learn.microsoft.com/en-us/windows/win32/debug/pe-format#coff-string-table.
- Microsoft Corporation, "PE Format -- COFF Relocations (Object Only)," Microsoft Learn, learn.microsoft.com/en-us/windows/win32/debug/pe-format#coff-relocations-object-only.
- Microsoft Corporation, "PE Format -- x64 Processors (Type Indicators)," Microsoft Learn, learn.microsoft.com/en-us/windows/win32/debug/pe-format#x64-processors.
Further Reading
- Microsoft PE Format Specification — the authoritative reference for COFF and PE binary layout, covering headers, sections, symbols, relocations, and all related structures.
- TrustedSec COFFLoader Source Code — the reference implementation this course is based on; study the parsing logic in COFFLoader.c alongside this module.
- OSDev Wiki: COFF Format — a community-maintained overview of the COFF format with practical examples and cross-platform notes.
- Microsoft DUMPBIN Reference — documentation for the DUMPBIN utility, which can dump COFF headers, symbols, and relocations from .obj files.
- TrustedSec Blog: COFFLoader -- Building Your Own In Memory Loader — a walkthrough of BOF loading internals from the team that created COFFLoader.
- GNU objdump Documentation — reference for objdump, the cross-platform tool for inspecting COFF object files from MinGW-compiled BOFs.
- Cobalt Strike: Beacon Object Files — official documentation on the BOF format and API that COFFLoader is designed to execute.