Open almost any Windows binary and you meet the same cast: .text, .rdata, .data, .pdata, .rsrc, .reloc. Knowing what belongs where turns PE reading from byte-hunting into navigation — import names live in read-only data, mutable globals are the only writable section, exception unwind info sits in its own table the dispatcher can find by directory index rather than by name.
But the names are conventions chosen by the linker, not rules enforced by the format. The loader never reads them; it reads the Characteristics flags. Learn both: names for orientation, flags for truth.
Sort sections by what they are allowed to do, not by what they are called:
.text — machine code. Nothing else may execute (that is DEP/NX doing its job)..rdata — constants, import name strings, the import address table, debug and load-config structures. Read but never written by the program..data — initialized globals, plus a zero-filled tail when VirtualSize exceeds SizeOfRawData..pdata (x64 unwind entries), .rsrc (resource tree), .reloc (base relocations, discardable after loading).The model's missing piece: the directory index, not the name, is the contract. Sample A's import table lives in .rdata, and there is no .idata section in either sample — the Import directory (RVA 0x3A04) points wherever the linker put it.
The spec's flag bits that matter for images, and what each sample sets:
| Mask | Flag | Meaning |
|---|---|---|
| 0x00000020 | IMAGE_SCN_CNT_CODE | Section contains executable code |
| 0x00000040 | IMAGE_SCN_CNT_INITIALIZED_DATA | Section contains initialized data |
| 0x02000000 | IMAGE_SCN_MEM_DISCARDABLE | May be discarded once the loader is done with it |
| 0x20000000 | IMAGE_SCN_MEM_EXECUTE | Page may be executed |
| 0x40000000 | IMAGE_SCN_MEM_READ | Page may be read |
| 0x80000000 | IMAGE_SCN_MEM_WRITE | Page may be written |
Both samples, decoded from their real Characteristics fields:
| Section | Purpose | A / B Characteristics | Decode |
|---|---|---|---|
| .text | Machine code; entry point lives here | 0x60000020 | CODE | EXECUTE | READ |
| .rdata | Constants, import names, IAT, load config, debug pointers | 0x40000040 | INITIALIZED_DATA | READ |
| .data | Mutable globals; zero-fill tail beyond SizeOfRawData | 0xC0000040 | INITIALIZED_DATA | READ | WRITE |
| .pdata | x64 RUNTIME_FUNCTION entries for the unwinder (directory 3) | 0x40000040 | INITIALIZED_DATA | READ |
| .rsrc | Resource tree (directory 2): icons, version info, manifests | 0x40000040 | INITIALIZED_DATA | READ |
| .reloc | Base relocations (directory 5); loader may discard after fixups | 0x42000040 | MEM_DISCARDABLE | INITIALIZED_DATA | READ |
Sample A has .pdata because it is x64 (exception directory 0x6000/0x1EC points straight at it); Sample B is 32-bit and has no .pdata at all — its exception directory is zero. Sample B also drops .pdata while keeping the same flags on the five sections both files share.
Linkers do add their own sections beyond the classic six. Verified on binaries in this very environment: OpenConsole.exe carries .didat and a populated Delay Import directory (RVA 0xE8CB8, size 0x40); dbghelp.dll carries .didat and .mrdata; rg.exe carries .fptable; libasyncProfiler.dll carries .idata, .tls and .00cfg. Every one of those was linked by MSVC 14.x — treat unfamiliar names as data-plus-flags to decode, never as assumptions about content.
.idata section — modern MSVC folds import descriptors, names and the IAT into .rdata (directory 12, the IAT, starts at RVA 0x3000 = the first byte of .rdata in Sample A). Dispatching on section names in code — the spec only promises the order the linker chose, and says tables need not begin at section starts. And assuming .reloc must stay resident — its MEM_DISCARDABLE bit tells the loader it may unmap those pages after rebasing.Decode .reloc's 0x42000040 by peeling bits off, highest first:
0x42000040 0x80000000 MEM_WRITE not set -- loader will map it read-only 0x40000000 MEM_READ set 0x20000000 MEM_EXECUTE not set -- data, not code 0x02000000 MEM_DISCARDABLE set -- safe to drop after relocations 0x00000040 CNT_INITIALIZED_DATA set -- initialized data class 0x00000020 CNT_CODE not set = read-only, discardable, initialized data
Then .data's 0xC0000040 — the only writable row in either sample:
0xC0000040 = 0x80000000 (MEM_WRITE) | 0x40000000 (MEM_READ) | 0x00000040 (CNT_INITIALIZED_DATA)
The consequences are visible without a debugger: with NX_COMPAT set in both samples (DllCharacteristics 0x8160 for A, 0x8140 for B), .text gets execute-plus-read and no write, .data gets read-plus-write and no execute, and a program that tries to store a variable into .text raises an access violation — the section table said so first.
Sample A's section table tail, where the flags live (last 8 bytes of each row are two zero dwords plus Characteristics):
00000228: 0000 0000 2000 0060 2e72 6461 7461 0000 .... ..`.rdata.. 00000278: 0000 0000 4000 00c0 2e70 6461 7461 0000 ....@....pdata.. 000002e8: 0000 0000 0000 0000 0000 0000 4000 0042 ............@..B
Read them backwards, little-endian: 20 00 00 60 = 0x60000020, 40 00 00 c0 = 0xC0000040, 40 00 00 42 = 0x42000040.
List the sections, then decode the flags yourself:
$ objdump -h cli-64.exe
Idx Name Size VMA File off
0 .text 000017bc 0000000140001000 00000400
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .rdata 0000132c 0000000140003000 00001c00
CONTENTS, ALLOC, LOAD, READONLY, DATA
...
5 .reloc 00000030 0000000140008000 00003600
CONTENTS, LOAD, READONLY, DATAA bit-decoder for any Characteristics value:
$ python3 -c "ch=0x42000040; flags=[(0x80000000,'WRITE'),(0x40000000,'READ'),(0x20000000,'EXECUTE'),(0x02000000,'DISCARDABLE'),(0x00000040,'INIT_DATA'),(0x00000020,'CODE')]; print([n for m,n in flags if ch & m])"
['READ', 'DISCARDABLE', 'INIT_DATA']Run it on 0x60000020, 0x40000040 and 0xC0000040 — you should get CODE/EXECUTE/READ, READ/INIT_DATA, and WRITE/READ/INIT_DATA respectively. Then try objdump -h cli-32.exe and notice .pdata is missing on the 32-bit build.
Without looking back: decode 0x42000040 into named flags, and say which section of Sample A carries it.
A program needs a global counter that several functions increment. Which section of Sample A will the linker place it in, and how do you know from the flags alone — without reading the name?
The pattern: name for humans, flags for the loader, directory index for finding structures. When they disagree, the flags and directories win.
You have seen the two sizes per section (0x17BC vs 0x1800) and the two rulers that produce them — content length versus padded disk length. The next lesson makes both rulers explicit: FileAlignment for the disk, SectionAlignment for memory, and the SizeOfImage / SizeOfHeaders arithmetic that ties them together.
Next: Alignment: FileAlignment vs SectionAlignment — why the file is 14336 bytes but the image claims 0x9000, and how to prove a file has no overlay.