Back to course

Alignment: FileAlignment vs SectionAlignment

15 minintermediateModule 4: SectionsContinues from pe-common-sections
15 min · Module 4: Sections · Structures

Why This Matters

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.

A Simple Model

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
  1. FileAlignment pads bytes in the file: SizeOfHeaders and every PointerToRawData / SizeOfRawData are multiples of it. Spec: a power of two, 512 to 64K, default 512.
  2. SectionAlignment pads space in memory: every section VirtualAddress and the total SizeOfImage are multiples of it. Spec: at least FileAlignment, default the architecture page size.
  3. The trap: if SectionAlignment is smaller than the page size, the spec forces FileAlignment to match it and requires file offsets to equal RVAs — a special mode, not our samples' mode.

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 Actual Detail

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:

FieldSample A (PE32+)Sample B (PE32)How it is computed
SectionAlignment0x10000x1000Read at file 0x138 — the memory ruler
FileAlignment0x2000x200Read at file 0x13C — the disk ruler
SizeOfHeaders0x4000x400A: headers end 0x2F8, rounded up to 0x200 — B: table at 0x1F8 + 5 rows ends 0x2C0, rounded to 0x400
SizeOfImage0x90000x7000A: 0x8000 + VirtualSize 0x30 = 0x8030, rounded up to 0x1000 — B: 0x6000 + 0x1CC = 0x61CC, rounded to 0x7000
File size14336 (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.

Common mistakes. Comparing SizeOfImage to file size — A's image claims 0x9000 bytes in memory while the file is 0x3800; headers plus gaps plus rounded ends live only in memory. And assuming raw size equals VirtualSize — A's .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.

A Real Example

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.

Try It

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 0

The 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
14336

What 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.

Check Your Understanding

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?

Apply It

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.

Connect

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.