Back to course

The Section Table

15 minintermediateModule 4: SectionsContinues from pe-rva-conversion
15 min · Module 4: Sections · Structures

Why This Matters

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.

A Simple Model

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
  1. Where it starts: immediately after the optional header. You do not get a pointer to it — you compute it: e_lfanew + 4 + 20 + SizeOfOptionalHeader. In Sample A that is 0x100 + 4 + 20 + 0xF0 = 0x208.
  2. How many rows:NumberOfSections in the COFF header — 6 in Sample A, 5 in Sample B. The loader caps it at 96.
  3. What one row means: name, memory size, memory base, disk size, disk base, and the Characteristics flags.

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

The spec's IMAGE_SECTION_HEADER, 40 bytes per entry:

OffsetSizeFieldWhat it says
08NameNull-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
84VirtualSizeTotal size of the section once loaded — the true in-memory length
124VirtualAddressFirst byte of the section relative to ImageBase (the section's RVA base)
164SizeOfRawDataBytes present on disk, rounded up to FileAlignment; may exceed or fall below VirtualSize
204PointerToRawDataFile offset of the section's first byte; a multiple of FileAlignment
244PointerToRelocationsZero in images — COFF relocations belong to object files only
284PointerToLinenumbersZero in images; COFF line numbers are deprecated
322NumberOfRelocationsZero in images (images use base relocations in .reloc instead)
342NumberOfLinenumbersZero in images
364CharacteristicsSection flags: code/data class plus READ/WRITE/EXECUTE/DISCARDABLE
Common mistakes. Assuming Name is always terminated — eight-character names fill the field completely, and the spec says so explicitly; read at most 8 bytes and stop at a null only if you find one. Assuming VirtualSize equals SizeOfRawData — Sample A .text is 0x17BC in memory but 0x1800 on disk (alignment padding), while .data is 0x648 in memory but only 0x200 on disk (zero-filled tail). And reading the four middle fields at all: in an image they are zero by specification.

A Real Example

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:

  1. Bytes 0-7: 2e 74 65 78 74 00 00 00 — ASCII .text plus three null pads.
  2. Bytes 8-11: bc 17 00 00 = 0x17BC — VirtualSize.
  3. Bytes 12-15: 00 10 00 00 = 0x1000 — VirtualAddress, the section's RVA base.
  4. Bytes 16-19: 00 18 00 00 = 0x1800 — SizeOfRawData, padded to 0x200.
  5. Bytes 20-23: 00 04 00 00 = 0x400 — PointerToRawData, where code starts in the file.
  6. Bytes 24-35: twelve zero bytes — the four deprecated COFF fields, all zero as the spec requires for images.
  7. Bytes 36-39: 20 00 00 60 = 0x60000020 — Characteristics, decoded next lesson.

All six rows decoded (names, memory sizes, memory bases, disk sizes, disk bases):

NameVirtualSizeVirtualAddressSizeOfRawDataPointerToRawDataCharacteristics
.text0x17BC0x10000x18000x04000x60000020
.rdata0x132C0x30000x14000x1C000x40000040
.data0x06480x50000x02000x30000xC0000040
.pdata0x01EC0x60000x02000x32000x40000040
.rsrc0x01E00x70000x02000x34000x40000040
.reloc0x00300x80000x02000x36000x42000040

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.

Try 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  00001c00

Walk 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 0x42000040

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

Check Your Understanding

Without looking back: how do you compute where the section table begins, and how many rows does Sample A have?

Apply It

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.

Connect

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.