The section table is the only structure that answers the loader's real question: these bytes on disk become those bytes at which memory address, with what permissions? Without it the image is a flat blob — no way to know where code ends, which pages are writable, where relocations live, or how to run the RVA conversion you just learned. The loader reads nothing else to map the file; tools as different as objdump -h, Windows memory-mapping APIs, and packers that rewrite headers all speak this one structure.
It is also delightfully small: ten fields, 40 bytes per section, one row per section, sitting at a location you can compute from the bytes at offset 0x3C. Everything about PE layout converges here.
Picture a spreadsheet header followed by one row per section — fixed-width columns, no gaps:
0x100 "PE\0\0" 4 bytes signature 0x104 COFF header 20 bytes NumberOfSections, SizeOfOptionalHeader 0x118 Optional header 0xF0 entry, base, alignments, 16 directories 0x208 Section table 40 x 6 one row each: .text .rdata .data ... 0x2F8 (headers end) ... section data begins at each PointerToRawData
e_lfanew + 4 + 20 + SizeOfOptionalHeader. In Sample A that is 0x100 + 4 + 20 + 0xF0 = 0x208.NumberOfSections in the COFF header — 6 in Sample A, 5 in Sample B. The loader caps it at 96.The model's missing piece: two sizes per section that are deliberately different — VirtualSize (memory) and SizeOfRawData (disk) — plus a name field that is not guaranteed to be null-terminated.
The spec's IMAGE_SECTION_HEADER, 40 bytes per entry:
| Offset | Size | Field | What it says |
|---|---|---|---|
| 0 | 8 | Name | Null-padded UTF-8. Exactly 8 characters means no terminator; object files use slash-plus-decimal for long names, but images never do — names over 8 characters are truncated at link time |
| 8 | 4 | VirtualSize | Total size of the section once loaded — the true in-memory length |
| 12 | 4 | VirtualAddress | First byte of the section relative to ImageBase (the section's RVA base) |
| 16 | 4 | SizeOfRawData | Bytes present on disk, rounded up to FileAlignment; may exceed or fall below VirtualSize |
| 20 | 4 | PointerToRawData | File offset of the section's first byte; a multiple of FileAlignment |
| 24 | 4 | PointerToRelocations | Zero in images — COFF relocations belong to object files only |
| 28 | 4 | PointerToLinenumbers | Zero in images; COFF line numbers are deprecated |
| 32 | 2 | NumberOfRelocations | Zero in images (images use base relocations in .reloc instead) |
| 34 | 2 | NumberOfLinenumbers | Zero in images |
| 36 | 4 | Characteristics | Section flags: code/data class plus READ/WRITE/EXECUTE/DISCARDABLE |
The first row of Sample A's table — all 40 bytes of .text at file 0x208:
00000208: 2e74 6578 7400 0000 bc17 0000 0010 0000 .text........... 00000218: 0018 0000 0004 0000 0000 0000 0000 0000 ................ 00000228: 0000 0000 2000 0060 .... ..`.
Field by field, little-endian:
2e 74 65 78 74 00 00 00 — ASCII .text plus three null pads.bc 17 00 00 = 0x17BC — VirtualSize.00 10 00 00 = 0x1000 — VirtualAddress, the section's RVA base.00 18 00 00 = 0x1800 — SizeOfRawData, padded to 0x200.00 04 00 00 = 0x400 — PointerToRawData, where code starts in the file.20 00 00 60 = 0x60000020 — Characteristics, decoded next lesson.All six rows decoded (names, memory sizes, memory bases, disk sizes, disk bases):
| Name | VirtualSize | VirtualAddress | SizeOfRawData | PointerToRawData | Characteristics |
|---|---|---|---|---|---|
| .text | 0x17BC | 0x1000 | 0x1800 | 0x0400 | 0x60000020 |
| .rdata | 0x132C | 0x3000 | 0x1400 | 0x1C00 | 0x40000040 |
| .data | 0x0648 | 0x5000 | 0x0200 | 0x3000 | 0xC0000040 |
| .pdata | 0x01EC | 0x6000 | 0x0200 | 0x3200 | 0x40000040 |
| .rsrc | 0x01E0 | 0x7000 | 0x0200 | 0x3400 | 0x40000040 |
| .reloc | 0x0030 | 0x8000 | 0x0200 | 0x3600 | 0x42000040 |
Notice the names: seven bytes or fewer each, so every one fits with a null to spare. The name field is metadata for humans and linkers — the loader never dispatches on it.
Six sections times 40 bytes is 240 bytes of table — dump exactly that:
$ xxd -s 0x208 -l 240 cli-64.exe
00000208: 2e74 6578 7400 0000 bc17 0000 0010 0000 .text...........
00000218: 0018 0000 0004 0000 0000 0000 0000 0000 ................
00000228: 0000 0000 2000 0060 2e72 6461 7461 0000 .... ..`.rdata..
...
$ objdump -h cli-64.exe
Idx Name Size VMA File off
0 .text 000017bc 0000000140001000 00000400
1 .rdata 0000132c 0000000140003000 00001c00Walk the table programmatically — 40 bytes a row from offset 0x208:
$ python3 -c "import struct; d=open('cli-64.exe','rb').read(); [print(d[o:o+8].split(b'\x00')[0].decode(), *[hex(x) for x in struct.unpack_from('<IIII', d, o+8)], hex(struct.unpack_from('<I', d, o+36)[0])) for o in (0x208, 0x230, 0x258, 0x280, 0x2A8, 0x2D0)]"
.text 0x17bc 0x1000 0x1800 0x400 0x60000020
.rdata 0x132c 0x3000 0x1400 0x1c00 0x40000040
.data 0x648 0x5000 0x200 0x3000 0xc0000040
.pdata 0x1ec 0x6000 0x200 0x3200 0x40000040
.rsrc 0x1e0 0x7000 0x200 0x3400 0x40000040
.reloc 0x30 0x8000 0x200 0x3600 0x42000040Sample B starts its table at 0x1F8 (PE32 optional header is 0xE0, not 0xF0): same 40-byte layout, five rows, xxd -s 0x1F8 -l 200 cli-32.exe.
Without looking back: how do you compute where the section table begins, and how many rows does Sample A have?
Sample A's .text row reads VirtualSize = 0x17BC and SizeOfRawData = 0x1800. A parser is about to trust only one of them. Which statement is correct?
The pattern: memory questions use VirtualSize, disk questions use SizeOfRawData, and any span test uses the larger of the two.
Six rows, six names, six Characteristics values — and you have not yet decoded what 0x60000020 versus 0xC0000040 actually permits. Those flags are how the loader decides what is executable, what is writable, and what may be thrown away after relocations are applied.
Next: Common Sections — what .text, .rdata, .data, .pdata, .rsrc and .reloc are for, with every flag bit decoded from real binaries.