Section: Memory is managed in pages.
A correctly owned process handle with read rights answers who may ask for memory. It does not say whether any particular address can be read. The target process has a changing map of pages, and each page has its own state and protection. We need both checks before interpreting a scanner failure.
A process sees memory as a huge range of numbered virtual addresses. Windows does not manage that range one byte at a time. It groups adjacent addresses into fixed-size pages, commonly 4 KiB on x86 and x86-64 systems.
Pages that share the same state and protection are reported together as a region. A 200 KiB read-only region is therefore a run of many pages with matching state and protection, not one enormous hardware page.
This matters to every scanner and patcher in the course. An address can be a perfectly valid integer while the page at that address is absent, guarded, read-only, or non-executable.
🛡️ Permission check: a believable address is not enough. Ask Windows whether the page exists and whether your exact operation is allowed before touching it.
Section: A virtual address is part of a process’s map.
The address printed by a debugger is a virtual address. It names a location inside one process’s address space; it does not directly name a R A M, random access memory, chip cell. Windows and the C P U, central processing unit, use page tables to translate the virtual page into a physical frame when the page is present.
For any page size, an address can be split into two pieces:
Alternative: Split an address like a book reference: the virtual page selects the mapped page, and the offset picks a byte within it.
Optional: practise separating the page number from the byte offset without dividing by zero.
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
With a 4 KiB page, address shown here has offset hexadecimal A B C inside its page. The remaining upper part identifies the virtual page. The translation changes the page part; the offset stays the same because it selects one byte inside that page.
Two games can both use virtual address shown here while those addresses map to different physical memory. One process can also have no mapping there at all. ReadProcessMemory takes a process handle precisely so Windows knows which process map should interpret the address.
Committed also does not mean “currently sitting in physical R A M.” Windows may bring a committed page back from its backing storage when accessed. That event is a page fault handled by the operating system; it is not automatically a bug in the game or scanner.
Section: State, type, and protection answer different questions.
Windows describes a region using three groups of facts — and they are independent axes, not a hierarchy:
In brief: Region state, mapping type, and access protection describe different facts; check all relevant fields before reading.
State says whether storage is ready:
MEM_FREE: no address range is reserved there;
MEM_RESERVE: the address range is set aside, but physical/page-file storage is not committed;
MEM_COMMIT: Windows has committed storage and the process may use it according to its protection.
Type says where committed pages came from:
MEM_IMAGE: a mapped P E, portable executable, image such as an EXE or D L L, dynamic link library;
MEM_MAPPED: a mapped file or shared section;
MEM_PRIVATE: private process memory such as many heap allocations.
Protection says which operations are allowed. The course prints the three permissions as R, W, and X:
PAGE_GUARD is an extra flag. The first access raises an exception and clears the guard flag. Windows uses guard pages for mechanisms such as growing thread stacks. A scanner should skip them instead of treating the exception as ordinary data.
Section: Why writable plus executable deserves attention.
Normal compiled code is usually readable and executable. Writable data is usually non-executable. Keeping those roles separate follows the write xor execute idea: memory should normally be writable or executable, not both at once.
Some legitimate runtimes generate code dynamically, and old software may use broad permissions. An RWX region is therefore a question, not a conviction:
Which component created it?
Is it inside the expected image or private memory?
Does it exist in a clean run of the same build?
Does its lifetime match a known feature?
This lab reports RWX regions but never writes to them or starts a thread there.
Section: VirtualQueryEx returns one matching run.
The shared Process::regions wrapper begins at an address, calls VirtualQueryEx, records the returned MEMORY_BASIC_INFORMATION, and advances by RegionSize:
Optional: practise the named operations and their inputs with a few whole-token blanks.
Code example. The code’s comments explain: 🔍 Ask about the run containing `current`; this describes pages but does not copy bytes from them or grant permission to read/write them. ✅ Zero ends the walk. No stale MEMORY_BASIC_INFORMATION is consumed. 📏 Move from the returned base—not merely from `current`—to the first address after this entire run of pages with identical attributes.
The checked addition prevents an invalid region from wrapping to a low address. The progress check prevents an endless loop if the reported range does not move forward.
The fields of MEMORY_BASIC_INFORMATION that the walk and the scanner rely on:
What VirtualQueryEx reports about one run
BaseAddresspointer
Where the run starts. Pages are 4 KiB, so this is the queried address rounded down to a page boundary. The walk advances from here, not from the address it asked about.
RegionSizebytes
How long the run is. Every page from BaseAddress for this many bytes shares one state, one type, and one protection, and a new run begins wherever any of the three changes.
StateMEM_COMMIT, MEM_RESERVE, or MEM_FREE
Whether storage is ready. Only committed pages can hold bytes a scanner may read.
TypeMEM_IMAGE, MEM_MAPPED, or MEM_PRIVATE
Where committed pages came from: a P E image, a mapped file or shared section, or private process memory.
ProtectPAGE_* flags
Which operations the pages allow. The course prints the read, write, and execute bits as R, W, and X, and treats PAGE_GUARD as a reason to skip the run.
AllocationBasepointer
Where the whole allocation begins. Several runs with different protections can belong to one allocation, so they share this value.
Section: Build a read-only module mapper.
The complete tool opens the process without write rights, finds one module, clips every returned region to that module, and prints an access summary.
Additional detail: Complete lab source: memory_map.rs.
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
false in open_by_name(&process_name, false) is important. It means the wrapper requests query and read rights without PROCESS_VM_OPERATION or PROCESS_VM_WRITE.
The original helper opens a writable process handle even though it only lists module regions. In this course wrapper, true requests write-related rights; print_regions only formats the returned metadata.
Before: open a writable handle to inspect pages
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
Request observation rights with false. Also use checked arithmetic for the module end, so an invalid size cannot wrap the region boundary.
Improved code
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
Show the diff
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
Takeaway: an inspection tool should request inspection rights and reject an overflowing range.
Section: Why this version?.
The lab is answering “what permissions exist?” It does not need permission to change them. The handle should match the question.
Section: Run it against the course games.
Start each game before running its command. The first argument names the process to open; the second names the module whose live pages to report. The output groups regions by state, type, and protection so you can compare a mapped image with its section flags on disk.
Terminal window
Code example. The surrounding prose explains this example; the full lesson contains the exact code.
Compare the regions with the P E section flags printed by pe_inspector. The on-disk section table describes intended mapping characteristics; VirtualQueryEx describes the live pages in this run.
Keep these address ideas separate:
A normal user-mode pattern scanner needs the live virtual address and lets Windows perform translation. It should not guess physical addresses. The offline D M A, direct memory access, chapter introduces page-table translation separately because a raw capture file has no running Windows process to translate addresses for it.
Section: Why a scanner skips some pages.
Predict first
VirtualQueryEx reports a region as MEM_COMMIT with protection PAGE_READWRITE | PAGE_GUARD. The scanner's handle includes PROCESS_VM_READ.
What does the course's scanner do with that region?
Answer: Skips it. The scanner reads only committed, readable, unguarded regions. PAGE_GUARD on top of RW- fails the third test, so ReadProcessMemory is never called for this region.

The memory scanner visits only committed, readable, non-guarded regions. That avoids predictable failures and reduces wasted work. A later ReadProcessMemory can still fail because the process continues running and page state can change between the query and read.
Narration: A page can become unreadable between the map query and the read. A scanner needs to handle that ordinary race even after careful filtering.
This is another time-of-check/time-of-use race. A robust scanner treats a failed region read as a normal event, reports useful context, and continues when safe.
The buildable tool is memory_map.rs, and the shared query implementation is in process.rs.
References: VirtualQueryEx and memory protection constants.