Back to course

Exception Tables (.pdata)

15 minadvancedModule 7: Resources & Runtime TablesContinues from pe-tls
15 min · Module 7: Resources & Runtime Tables · Structures

Why This Matters

When a 64-bit Windows program faults, something has to rebuild the call stack: which function were we in, which registers were saved where, who called whom. The answer is not in the instructions — x64 code does not record frame pointers. It is in a side table the linker emits for every function that touches the stack or calls anything else.

That table is what debuggers walk when you hit a crash dump, what the operating system walks when it dispatches an exception, and what C++ uses to find destructors during unwinding. Read it and crash analysis stops being guesswork: you can map a faulting address to a function and see exactly how big its stack frame is.

Data directory index 3 (Exception Table) points at it. In cli-64.exe it holds RVA 0x6000 with size 0x1EC — the .pdata section.

A Simple Model

Think of it as a table of contents for the stack:

  1. One entry per function — a 12-byte RUNTIME_FUNCTION: where the function starts, where it ends, where its unwind recipe lives. All three are RVAs.
  2. One recipe per entry — an UNWIND_INFO record: how many bytes the prolog took, and a short list of operations (push this register, subtract that many bytes from the stack) that an unwinder can run backwards.

The model's missing piece: lookups are binary searches. The spec requires the entries be sorted by function address before the image is emitted, so the unwinder can find the function containing a faulting instruction in one pass.

The Actual Detail

The PE/COFF specification, chapter "The .pdata Section", defines the x64 and Itanium entry as three 4-byte fields — Begin Address, End Address, Unwind Information — and states plainly: these are RVAs, and the entries must be sorted according to the function addresses before being emitted into the final image. (The older MIPS format stores VAs instead; the x64 layout is the one that matters on modern machines.)

Microsoft's x64 exception handling documentation adds the operational rules:

StructureContentsRule
RUNTIME_FUNCTION (12 bytes)Function start, function end, unwind info addressAll addresses image-relative; DWORD aligned; entries sorted; stored in .pdata of a PE32+ image
UNWIND_INFOVersion (3 bits), Flags (5 bits), SizeOfProlog, CountOfCodes, FrameRegister and offset, then the unwind codes arrayVersion is currently 1; the codes array always holds an even number of entries
UNWIND_CODE (2 bytes)Offset in prolog, unwind operation (4 bits), operation info (4 bits)Array sorted by descending prolog offset

The dispatch algorithm is documented too: search for a RUNTIME_FUNCTION covering the faulting RIP; if there is none, the code is a leaf function and the return address is simply at [RSP]; if RIP sits in the prolog or epilog, undo or simulate it; otherwise call the language-specific handler if UNW_FLAG_EHANDLER is set, then unwind frame by frame. JIT-generated code must register its own tables with RtlAddFunctionTable or RtlInstallFunctionTableCallback, or exception handling and debugging become unreliable.

Common mistake. End Address is exclusive: it is the first byte after the function, not the last byte of it. Padding after a ret instruction belongs to neither side of the boundary — yet a debugger that treats End as inclusive will misattribute exactly those bytes.

A Real Example

Forty-eight bytes from the start of .pdata in cli-64.exe (file offset 0x3200) — three complete entries:

00003200: 1010 0000 3410 0000 c038 0000 4010 0000  ....4....8..@...
00003210: 8510 0000 8038 0000 a010 0000 fc11 0000  .....8..........
00003220: a438 0000 0012 0000 d012 0000 8c38 0000  .8...........8..

The first entry decodes to Begin 0x1010, End 0x1034, UnwindInfo 0x38C0. Follow it end to end:

  1. At image base 0x140000000 the function is 0x140001010 to 0x140001034. It opens with sub rsp, 0x28 and the last instruction at 0x140001033 is ret — the byte at 0x140001034 is int3 padding, outside the function, exactly as an exclusive End predicts.
  2. Unwind info RVA 0x38C0 lives in .rdata (RVA 0x3000, file 0x1C00), so file offset 0x24C0. First 8 bytes:
000024c0: 0104 0100 0442 0000                      .....B..
  1. Byte 0: version 1, flags 0. Byte 1: SizeOfProlog = 4 — matches the 4-byte sub rsp instruction. Byte 2: CountOfCodes = 1. Byte 3: no frame register. Then one UNWIND_CODE: offset 0x04, operation 0x2 (UWOP_ALLOC_SMALL), info 0x4 — and the spec formula for that operation is info * 8 + 8 = 40 bytes = 0x28. The recipe and the machine code agree.
  2. The section holds 41 entries (0x1EC / 12), sorted by Begin. conpty.dll has 354 (0x1098 / 12). Its first entry reads Begin 0x10C0, End 0x1105, UnwindInfo 0x14B50 — same layout, bigger program.

The imports tell you who consumes these tables: cli-64.exe pulls RtlLookupFunctionEntry, RtlVirtualUnwind, and RtlCaptureContext from KERNEL32.dll, and __C_specific_handler from VCRUNTIME140.dll — the very APIs the unwinder and structured exception handling are built on.

Try It

Read the first table entry straight from the file (0x3200 is .pdata for our sample):

$ xxd -s 0x3200 -l 48 cli-64.exe
00003200: 1010 0000 3410 0000 c038 0000 4010 0000  ....4....8..@...
00003210: 8510 0000 8038 0000 a010 0000 fc11 0000  .....8..........
00003220: a438 0000 0012 0000 d012 0000 8c38 0000  .8...........8..

Then look at the code the first entry covers (command output, operand syntax reformatted for width):

$ objdump -d --start-address=0x140001010 --stop-address=0x140001036 cli-64.exe
140001010: 48 83 ec 28           sub    rsp, 0x28
14000102a: b8 01 00 00 00        mov    eax, 1
14000102f: 48 83 c4 28           add    rsp, 0x28
140001033: c3                    ret
140001034: cc                    int3

What to look for: sub and add of the same 0x28 pair up with the single UWOP_ALLOC_SMALL code, and End lands on the first int3 after ret. If those three numbers agree, your parser is reading the table correctly.

Check Your Understanding

Without looking back: a RUNTIME_FUNCTION reads Begin 0x1010 and End 0x1034. Which byte is the last one inside the function?

Apply It

An unwind code reads 04 42: offset 0x04, operation 0x2, info 0x4. How much stack does the prolog allocate?

The pattern: identify the operation first, then apply its formula — every unwind opcode packs meaning into those four info bits differently.

Connect

You have now met the three runtime tables: resources, TLS, and exceptions. They all describe data the rest of the system consumes. The final module turns to the consumer itself — the loader that turns this file into a running process.

Next: How the Windows Loader Maps a PE — from MZ probe to mapped sections and bound imports.