Lesson 13.1 left us with a concrete mismatch: a debugger reports a game’s virtual address, while an offline R A M, random access memory, capture is indexed by physical address. We need the captured page tables to connect the two. If we mix them up, a read may return plausible bytes from the wrong place, so the types and bounds in this tool are part of the explanation, not just coding style.
Section: Give address spaces different types.
A physical address and a virtual address are both u64 values, but mixing them is a serious logic error. Newtypes make the compiler help:
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
Now read_physical cannot accidentally accept a VirtualAddress without an explicit conversion.
Section: Bounds-check every physical read.
The capture is untrusted binary input. A page-table entry can point beyond the end of the file, so the reader checks the complete range:
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
Three checks handle three different failures: the address may not fit usize, addition may overflow, or the final range may be outside the capture.
Section: Read one table entry.
Each table entry is eight little-endian bytes. Its low present bit must be set before the address bits are trusted:
Optional: practise locating one eight-byte table entry and refusing an absent mapping.
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
The error includes the failed level and physical entry address. That context is far more useful than “read failed.”
Here is one real entry: the first PML4 entry of the synthetic capture used in the tests later in this lesson, stored at physical address hexadecimal 1 0 0 0:
Section: Walk all four levels.
In brief: Each index selects one entry. Its address bits lead to the next table. Only the last entry supplies the data page.
Before reading the code, it pays to see where its constants come from, because none of them are arbitrary. A 64-bit virtual address is cut into six fields, and two sizes fix every boundary:
A page is 4,096 bytes, and 4,096 equals 2^12, so the lowest 12 bits, 0–11, pick a byte inside the page.
A table holds 512 entries, and 512 equals 2^9, so each table index takes 9 bits. Stacking four indices above the offset puts the page-table index at bits 12–20 (the next field starts at 12 + 9 equals 21), the page-directory index at 21–29, the PDPT index at 30–38, and the PML4 index at 39–47.
That accounts for 12 + 4 times 9 equals 48 bits. The remaining 16 bits, 48–63, carry no information of their own: they must repeat bit 47, which is what makes an address canonical.
Take the address from the page-crossing example later in this lesson, this value. Every hex digit is exactly four bits, so its low twelve digits spell out bits 47 down to 0:
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
Cutting those 48 bits at the boundaries above, working from the right (12 bits, then four groups of 9), gives the six fields:
Converting each field to decimal is ordinary place value: hexadecimal 0 F F equals 15 times 16 + 15 equals 255; hexadecimal 1 D 8 equals 1 times 256 + 13 times 16 + 8 equals 472; hexadecimal 0 9 1 equals 9 times 16 + 1 equals 145; hexadecimal 1 4 0 equals 1 times 256 + 4 times 16 equals 320; and hexadecimal F F 8 equals 15 times 256 + 15 times 16 + 8 equals 4,088. This address therefore uses PML4 entry 255, PDPT entry 472, page-directory entry 145, page-table entry 320, and byte 4,088 of the final page.
The same layout accounts for every mask in the walk below:
The shifts of 39, 30, 21, and 12 slide each index field down to bit zero so it can be used as a table index. For the example address, (value >> 39) & hexadecimal 1 F F is exactly the PML4 field, 255.
& hexadecimal 1 F F keeps nine bits, because hexadecimal 1 F F is 511 and each table holds 512 entries. Nine bits is not a coincidence: 512 entries of eight bytes each comes to exactly 4,096 bytes, so every page table is itself exactly one page.
& hexadecimal F F F keeps twelve bits — the offset within a 4 KiB page, since hexadecimal F F F is 4,095.
& this value keeps bits 12 through 51 of an entry. The low twelve bits are cleared because an entry stores flags there (present, writable, user-accessible, large-page), and the high bits are cleared because they hold further flags rather than address. What remains is the physical address of the next table — which is page-aligned, and therefore always had twelve spare zero bits for those flags to live in.
The complete implementation masks flag bits from each next-table address:
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
The real file also checks the large-page bit, bit 7 of an entry (1 << 7 equals hexadecimal 8 0), at the PDPT and page-directory levels. When it is set, the walk stops early, and the index fields it skipped become part of the offset. A 1 GiB page is 2^30 bytes, so its offset is the low 30 bits, value & this value. A 2 MiB page is 2^21 bytes, so its offset is the low 21 bits, value & hexadecimal 1 F F F F F. Each mask is one less than its page size: this value equals 4 times 16^7 equals 2^2 times 2^28 equals 2^30, and hexadecimal 2 0 0 0 0 0 equals 2 times 16^5 equals 2 times 2^20 equals 2^21.
Every entry in the chart is read through read_physical, so an entry address beyond the end of the file stops the walk with OutOfRange at whichever level it occurs.
Section: A read can cross a page boundary.
Narration: A neighboring virtual page can lead somewhere else in physical memory. Translate again at the boundary.
Translating only the starting address is a subtle bug. Virtual memory looks contiguous; the physical memory behind it is not. Two virtual pages sitting side by side can be backed by physical pages nowhere near each other.
Suppose a 32-byte read begins 8 bytes before the end of a page:
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
This is the worked address from the walk above. Its page offset is hexadecimal F F 8, and the page ends at offset hexadecimal 1 0 0 0, so hexadecimal 1 0 0 0 minus hexadecimal F F 8 equals 4,096 minus 4,088 equals 8 bytes remain in the first page. Byte 9 sits at this value, where the page-table index is hexadecimal 1 4 1 equals 321 instead of 320 and the offset is 0. The two halves of the read are translated by neighbouring entries of the same page table, at byte offsets 320 times 8 equals 2,560 and 321 times 8 equals 2,568, and nothing requires those two entries to hold neighbouring physical addresses.
Nothing warns you when this happens. Translating once and then reading 32 consecutive physical bytes quietly returns 24 bytes belonging to whatever follows the first page in physical memory — a completely unrelated allocation, another process’s data, or nothing mapped at all. The bytes come back looking perfectly ordinary.
read_virtual therefore loops:
translate the current virtual address;
calculate bytes remaining in its page;
copy the smaller of that amount and the requested remainder;
translate again after crossing the boundary.
This is why the public method returns a new byte vector rather than a borrowed slice of one physical region.

Section: Test with synthetic page tables.
The unit test constructs a tiny fake physical image:
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
The fixture is hexadecimal 7 0 0 0 bytes: 7 times hexadecimal 1 0 0 0 equals 7 times 4,096 equals 28,672 bytes, or seven 4 KiB pages. The lab’s capture format has no header. File offset N holds physical address N, so each page of the file is one physical page:
With c r 3 equals hexadecimal 1 0 0 0, virtual address hexadecimal 0 1 2 3 should translate to physical hexadecimal 5 1 2 3. Tests also cover a missing present bit and a noncanonical virtual address.
Synthetic tests are powerful because every byte is known. A failure means the translator is wrong, not that an unknown capture changed.
Section: Run the complete tool.
The checked implementation is in advanced-memory-labs/src/dma.rs; the command-line wrapper is src/bin/dma_capture.rs.
Terminal window
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
Arguments are:
path to an offline physical capture with recorded provenance;
physical c r 3 value in hexadecimal;
virtual address in hexadecimal;
byte count from 1 through 4096.
The maximum is deliberately small. A learning tool should make unexpectedly huge reads impossible by default.
The output says which bytes an offline page-table mapping points to. It does not certify that a live user-mode read or write would pass C P U, central processing unit, and Windows permission checks. The present bit and the file bounds are necessary for this walk; effective access also depends on flags at multiple table levels and on the kind of access requested.
Section: Why a translation may fail.
Never “fix” an error by masking more bits until output appears. Compare the entry format with the architecture manual, confirm the capture layout, and keep the failure visible.
The next lesson asks a harder question than “did translation return bytes?”: do those bytes belong to the intended process, object, build, and capture moment? That requires provenance and independent checks, not another mask.