Two rulers govern every PE file, and they almost never agree. On disk, section data is padded to FileAlignment — historically 512 bytes, the granularity disks and installers care about. In memory, sections are placed on SectionAlignment boundaries — typically 4096, the page size the MMU maps. The gap between the rulers explains why Sample A is 14336 bytes on disk yet claims a SizeOfImage of 0x9000, why VirtualSize and SizeOfRawData disagree, and why a naive parser that assumes file offset equals RVA reads garbage after byte 0x400.
Get the two straight and several PE mysteries collapse into arithmetic you can do by hand: header size, image size, padding, and whether a file carries extra trailing bytes at all.
Hold two rulers against the same content:
DISK ruler: FileAlignment = 0x200 [headers........][.text..........pad][.rdata.....pad][.data][.pdata][.rsrc][.reloc] rounded to 0x200 each PointerToRawData multiple of 0x200 MEMORY ruler: SectionAlignment = 0x1000 [page 0: headers][page 1: .text][gap][page 3: .rdata][gap][.data]... each VirtualAddress multiple of 0x1000; SizeOfImage = end rounded to 0x1000
The model's missing piece: the two summary fields, SizeOfHeaders and SizeOfImage, are just the rulers applied to the header block and to the last section's end.
The spec's definitions: SizeOfImage is "the size of the image, as loaded in memory" and "must be a multiple of SectionAlignment"; SizeOfHeaders is "the combined size of an MS-DOS stub, PE header, and section headers rounded up to a multiple of FileAlignment." Both samples agree with the defaults — SectionAlignment and FileAlignment live at optional-header offsets 32 and 36, i.e. file 0x138 and 0x13C in these files:
| Field | Sample A (PE32+) | Sample B (PE32) | How it is computed |
|---|---|---|---|
| SectionAlignment | 0x1000 | 0x1000 | Read at file 0x138 — the memory ruler |
| FileAlignment | 0x200 | 0x200 | Read at file 0x13C — the disk ruler |
| SizeOfHeaders | 0x400 | 0x400 | A: headers end 0x2F8, rounded up to 0x200 — B: table at 0x1F8 + 5 rows ends 0x2C0, rounded to 0x400 |
| SizeOfImage | 0x9000 | 0x7000 | A: 0x8000 + VirtualSize 0x30 = 0x8030, rounded up to 0x1000 — B: 0x6000 + 0x1CC = 0x61CC, rounded to 0x7000 |
| File size | 14336 (0x3800) | 11776 (0x2E00) | Equals last PointerToRawData + SizeOfRawData in both files |
Where each ruler applies: headers block — DOS stub, signature, COFF, optional header, section table (0x2F8 raw bytes in A) — padded by FileAlignment to 0x400, which is also why the first PointerToRawData is 0x400 and not 0x2F8. Section VAs step by 0x1000: 0x1000, 0x3000, 0x5000, 0x6000, 0x7000, 0x8000. Raw pointers step by FileAlignment multiples: 0x400, 0x1C00, 0x3000, 0x3200, 0x3400, 0x3600.
The overlay question, computed: Sample A's last section .reloc has PointerToRawData 0x3600 plus SizeOfRawData 0x200 = 0x3800, and the file is exactly 14336 = 0x3800 bytes — zero trailing bytes, no overlay. Sample B: 0x2C00 + 0x200 = 0x2E00 = 11776 = file size, also no overlay. Signed binaries are where tails appear: OpenConsole.exe's sections end at 0x100E00, the Security directory (a file offset, per the spec) points at 0x100E00 with size 0x3D20, and 0x100E00 + 0x3D20 = 0x104B20 = the file size — a WIN_CERTIFICATE block appended past the sections because the loader never maps it.
.text carries 0x44 padding bytes on disk (0x1800 - 0x17BC) while its .data is short by 0x448 (0x648 - 0x200), the zero-fill tail. Both are normal alignment outcomes, not corruption.SizeOfHeaders for Sample A, by hand. The section table starts at 0x208; six rows of 40 bytes is 240 (0xF0) bytes, so raw headers end at 0x2F8. Round up to FileAlignment 0x200:
0x2F8 = 762 bytes of actual header round up to multiple of 0x200: 762 / 512 = 1.49 -> 2 sectors SizeOfHeaders = 2 x 0x200 = 0x400 (matches the header field)
SizeOfImage for Sample A, by hand: the last section's memory end is VirtualAddress 0x8000 + VirtualSize 0x30 = 0x8030. Round up to SectionAlignment 0x1000: 0x8030 lies inside the 0x8000 page run, so the image occupies pages through 0x8FFF and SizeOfImage = 0x9000 — exactly what objdump -p prints.
last section end: 0x8000 + 0x0030 = 0x8030 round up to 0x1000: ((0x8030 + 0x0FFF) / 0x1000) * 0x1000 = 0x9000
And the padding baked into .text: VirtualSize 0x17BC (5564 bytes of code) versus SizeOfRawData 0x1800 (6144 bytes on disk) — a difference of 0x44 = 68 bytes of FileAlignment filler after the last instruction. On disk the section ends at 0x400 + 0x1800 = 0x1C00, which is precisely where .rdata's PointerToRawData begins: sections butt together on the disk ruler with no gaps, while on the memory ruler each gets its own 0x1000-aligned region.
Verify all three facts — header rounding, image rounding, and overlay — in one script:
$ python3 -c "
import struct, os
d = open('cli-64.exe','rb').read()
e = struct.unpack_from('<I', d, 0x3C)[0]
o = e + 24
sa = struct.unpack_from('<I', d, o+32)[0]
fa = struct.unpack_from('<I', d, o+36)[0]
soimg = struct.unpack_from('<I', d, o+56)[0]
sohdr = struct.unpack_from('<I', d, o+60)[0]
n = struct.unpack_from('<H', d, e+6)[0]
hdr_end = o + 0xF0 + 40*n
print('align ', hex(sa), hex(fa))
print('headers ', hex(hdr_end), '-> stored', hex(sohdr))
print('image ', 'stored', hex(soimg))
last = max(struct.unpack_from('<I', d, o+0xF0+40*i+20)[0] + struct.unpack_from('<I', d, o+0xF0+40*i+16)[0] for i in range(n))
print('file ', hex(len(d)), 'last raw end', hex(last), 'overlay', len(d)-last)
"
align 0x1000 0x200
headers 0x2f8 -> stored 0x400
image stored 0x9000
file 0x3800 last raw end 0x3800 overlay 0The quick disassembler view of the same numbers:
$ objdump -p cli-64.exe | grep -E 'SectionAlignment|FileAlignment|SizeOfImage|SizeOfHeaders'
SectionAlignment 00001000
FileAlignment 00000200
SizeOfImage 00009000
SizeOfHeaders 00000400
$ stat -c %s cli-64.exe
14336What to look for: SectionAlignment is always the bigger ruler (0x1000 vs 0x200), SizeOfHeaders equals the first section's PointerToRawData when headers are padded normally, and overlay shows up as a positive len(d) - last.
Without looking back: Sample A's headers (DOS stub through section table) end at file offset 0x2F8, and FileAlignment is 0x200. What must SizeOfHeaders be, and why?
Someone claims Sample A has an overlay because SizeOfImage (0x9000) is larger than the file (0x3800). Check the real test: last PointerToRawData 0x3600 + SizeOfRawData 0x200 = 0x3800, file size 14336. Does an overlay exist?
The pattern: SizeOfImage answers "how big is this in memory", file size answers "how many bytes are on disk", and the overlay test compares file size to the last PointerToRawData plus SizeOfRawData. Three different questions, three different numbers.
Alignment closes Module 3 and Module 4: you can locate every structure, name every address kind, translate RVA to file offset, and now explain every size field in the optional header. The section table also showed you directory RVAs hiding inside .rdata — import descriptors at 0x3A04, the IAT at 0x3000 — without saying how those tables are shaped.
Next: The Import Tables — how a PE names the DLLs and functions it needs, and how the loader turns names into callable addresses.