Back to course

VA, RVA, and File Offset

15 minintermediateModule 3: Addressing & Data DirectoriesContinues from pe-data-directories
15 min · Module 3: Addressing & Data Directories · Foundations

Why This Matters

A PE file has one set of bytes but three different ways to name a position inside it. xxd shows file offsets. The data directories and the entry point store RVAs. Disassemblers, crash dumps, and the CPU itself deal in full virtual addresses. Confusing them is the single most common PE bug: a tool adds ImageBase to something already relative, or treats an RVA as a byte position on disk and reads the wrong row of the section table.

The three are related by exactly two arithmetic operations — subtract ImageBase to get an RVA, add it to get a VA — plus a per-section translation between RVA and file offset that has no closed form. Get the vocabulary straight here and every later lesson is arithmetic instead of guesswork.

A Simple Model

Picture one instruction living in two places at once — its bytes on disk, and its mapped copy in the process — with labels in different coordinate systems:

  ON DISK                              IN MEMORY (loaded at 0x140000000)
  +-----------------------------+      +-----------------------------------+
  | file offset 0x1140          | map  | VA 0x140001D40                   |
  | bytes: 48 83 ec 28         | ===> | RVA 0x1D40                       |
  | (sub rsp, 0x28)            |      | bytes: 48 83 ec 28  (same code)  |
  +-----------------------------+      +-----------------------------------+
        ^                                     ^
        | read with xxd                      | executed by the CPU,
        | shown by tools that seek           | VA = ImageBase + RVA
        | the file directly                  |

The model's missing piece: ImageBase is only a preferred base. Sample A asks for 0x140000000, Sample B for 0x00400000 — and both carry the DYNAMIC_BASE flag, so ASLR may load them somewhere else entirely.

The Actual Detail

The spec's own definitions, in its words: an RVA is "the address of an item after it is loaded into memory, with the base address of the image file subtracted from it," and it "almost always differs from its position within the file on disk." A VA is "same as RVA, except that the base address of the image file is not subtracted" — and because "the loader might not load the image at its preferred location," a VA is unpredictable while an RVA is not.

Address kindSample A exampleStored inStable under ASLR?
File offset0x1140PointerToRawData, e_lfanew, Security directory, anything xxd printsYes — the file never moves
RVA0x1D40AddressOfEntryPoint, data directories 0-3 and 5-15, section VirtualAddress, import/export/reloc recordsYes — image-relative
VA0x140001D40CPU registers, objdump VMA column, .pdata BeginAddress/EndAddress, crash dumpsNo — moves when the loader rebases

The two samples show the range: Sample A is PE32+ with an 8-byte ImageBase 00 00 00 40 01 00 00 00 at file 0x130 = 0x140000000, the usual high 64-bit base for EXEs. Sample B is PE32 with a 4-byte ImageBase 00 00 40 00 at file 0x134 = 0x00400000, the classic 32-bit default the spec documents. Same rule, different field width — PE32+ widened ImageBase to 8 bytes at optional-header offset 24.

Where each one shows up in practice: data directories hold RVAs (except Security); objdump -h prints VMA = ImageBase + RVA (Sample A .text at 0x140001000) and file off separately; objdump -p prints the entry as an RVA (0x1d40) but the function table as VAs (0x140001401, 0x14000164c, ...).

Common mistakes. "The VA is fixed because ImageBase is in the header" — no: with DYNAMIC_BASE set (Sample A DllCharacteristics 0x8160, Sample B 0x8140) the loader may place the image elsewhere, and only rebasing via base relocations keeps it correct. And "RVA is just a file offset" — the spec says it almost always differs: Sample A's entry RVA 0x1D40 sits at file 0x1140, a gap of 0xC00 that changes per section.

A Real Example

Take Sample A's entry point. The optional header stores RVA 0x1D40. At runtime, if the image loads at its preferred base, the CPU fetches from VA 0x140001D40. On disk you must translate through .text (VA 0x1000, raw pointer 0x400): file offset = 0x400 + (0x1D40 - 0x1000) = 0x1140. The bytes there are the program's first instructions:

00001140: 4883 ec28 e8d7 0300 0048 83c4 28e9 72fe  H..(.....H..(.r.
00001150: ffff cccc 4053 4883 ec20 488b d933 c9ff  ....@SH.. H..3..

48 83 ec 28 is sub rsp, 0x28 — a function prologue. Three names for those same four bytes: offset 0x1140 (disk), RVA 0x1D40 (header), VA 0x140001D40 (running process).

Sample B repeats the trick in 32-bit: entry RVA 0x1B87, ImageBase 0x00400000, so VA = 0x00401B87; .text (VA 0x1000, raw 0x400) gives file 0xF87, where the bytes are e8 c5 03 00 00 — a relative call.

Try It

Read ImageBase and the entry point, then compute all three coordinates for any PE:

$ python3 -c "import struct; d=open('cli-64.exe','rb').read(); e=struct.unpack_from('<I',d,0x3C)[0]; o=e+24; ib=struct.unpack_from('<Q',d,o+24)[0]; ep=struct.unpack_from('<I',d,o+16)[0]; print('ImageBase',hex(ib)); print('entry RVA',hex(ep)); print('entry VA ',hex(ib+ep))"
ImageBase 0x140000000
entry RVA 0x1d40
entry VA  0x140001d40

$ xxd -s 0x1140 -l 16 cli-64.exe
00001140: 4883 ec28 e8d7 0300 0048 83c4 28e9 72fe  H..(.....H..(.r.

Swap in cli-32.exe and change '<Q' to '<I' at offset 28 — PE32 stores ImageBase as 4 bytes after a BaseOfData field that PE32+ dropped.

What to look for: RVA and VA always differ by exactly ImageBase (when loaded at base), while the file offset differs by a per-section amount that no single formula covers.

Check Your Understanding

Without looking back: which of the three address kinds survives an ASLR rebase unchanged, and which structures keep using it while everything else moves?

Apply It

Sample A's import directory has RVA 0x3A04. One day Windows loads the image at 0x7FF600000000 instead of 0x140000000. What is the import directory's VA then, and what stays the same?

The pattern: convert VA to RVA by subtracting whatever base is actually in use, then use the section table to reach the file. Both steps are reversible; neither is optional.

Connect

You can now name any position three ways. What you cannot yet do mechanically is the middle step — turning an RVA from a directory into the byte offset you feed xxd. That translation is exactly the section table's job, but the algorithm comes first.

Next: Converting RVAs to File Offsets — the per-section algorithm, why no constant offset works, and the span check that catches the classic bug.