The Running Game as a Process
An executable becomes a process with threads, modules, handles, and Windows-managed state; the PEB and TEB reveal useful structure without being a safe API.
Cheatsheet for Lesson 6.1
A one-page summary of βThe Running Game as a Processβ you can keep beside your notes. Plain Markdown.
Lesson 6.1 β The Running Game as a Process: cheatsheet
- Remember: The executable supplies starting code and data; a running match also depends on live memory, threads, handles, and other process state.
- Also useful: Picture one process-wide information hub with a separate thread desk for every execution path: each TEB leads back to the shared PEB.
Key terms
- program file:
wesnoth.exeon disk is a program file. - PEB: The PEB is a user-mode structure Windows uses to keep process-wide information.
dwSize:dwSizeis important: it tells Windows which version and size of the output structure the caller prepared.
Formulas and patterns
program on disk process in memory;-------------------------- --------------------------------------;wesnoth.exe bytes β image mapped at a live module base;section RVAs β virtual addresses after base + RVA;entry-point RVA β first user-code address Windows calls
Review
- Q: Which item exists only after Windows creates a running process? A: A virtual address space with threads, handles, stacks, and mapped modules
- Q: Why is the first code run at a program's entry point usually not the developer's own main function? A: Startup code prepares the runtime first, then it calls main
A program file is not a running process
Section titled βA program file is not a running processβThe previous chapters followed executable files, saved data, mods, and scripts. All of them meet in a running Windows process. The executableβs bytes help create that process, but the running game contains more than that file.
wesnoth.exe on disk is a program file. When Windows starts it, Windows
creates a process: an isolated running instance with its own address space,
handles, threads, security information, and loaded modules.
That distinction matters. The PE chapter reads the file on disk. A debugger or memory tool studies the running process after Windows has mapped that file and its DLLs into virtual memory.
The questions now come in a useful order: what runtime owns the gameβs objects, which exact build is running, what access Windows grants, and how pages and threads change while the game runs. A dump at the end of the chapter will preserve a slice of that state for later inspection.
program on disk process in memory-------------------------- --------------------------------------wesnoth.exe bytes β image mapped at a live module baseimport names β live function pointers in the IATsection RVAs β virtual addresses after base + RVAentry-point RVA β first user-code address Windows callsThe process is not simply a copy of the file. Windows may place sections at different virtual addresses, fill zero-initialized data, apply relocations, resolve imports, and create private memory that never existed in the EXE.
1 On disk: the file wesnoth.exe
Disk
Fileswesnoth.exeThe file holds code, data and headers, and does nothing by itself.
2 Mapped into a running process
Disk
Fileswesnoth.exeRunning process
maps
Main imageimage mapped from wesnoth.exeWindows maps the file into a new process as its main image.
3 Runtime resources that no file holds
Running process
From the fileimage mapped from wesnoth.exeLoadedDLLsprivate heapPer threadthread stackthread contextHeld by the processhandlessecurity stateEverything else exists only while the process runs.
wesnoth.exe on disk is only the starting point. A running process adds resources that no file contains.
A process groups process-wide resources and a set of independently scheduled threads. Each thread adds its own execution state inside that shared container:
Threads share the process address space and handles, but each thread needs its own registers, stack, and thread bookkeeping so Windows can pause and resume it.
The PE entry point is also not automatically the gameβs main function. Windows
transfers control to startup code chosen by the executable and its runtime. That code
can initialize security state, global constructors, thread-local storage, and library
support before it calls the developerβs main function. Reverse the startup chain as
a sequence of responsibilities rather than labeling the first code address βmain.β
What Windows creates for a running process
Section titled βWhat Windows creates for a running processβA normal game process contains several kinds of state:
- the virtual address space, which gives the process its own numbered memory locations;
- one or more threads, which are the paths of CPU execution;
- modules, including the main EXE and loaded DLLs;
- heaps and stacks, used for dynamic objects and function calls;
- a handle table, containing Windows-managed references to files, events, sockets, and other objects;
- an access token, describing the process security context;
- user-mode bookkeeping structures, including the PEB and one TEB per thread.
Windows creates and manages these resources, checks access to protected objects, and schedules threads. A process can read much of its own user-mode state, but an internal Windows field can change between versions even when its address is readable. Prefer a documented API when one provides the information you need.
A process is machine state plus managed resources
Section titled βA process is machine state plus managed resourcesβThe useful description is an inventory of everything required to stop this running program and later continue it:
address space + thread register contexts + scheduling state+ open handles + security context + operating-system bookkeepingThe EXE supplies code and initial data, but the process is this larger live state.
That explains why copying wesnoth.exe does not copy a running match and why a
crash dump can reveal much more than the original file.
Keep mechanism separate from policy. A context switch is a mechanism for stopping one thread and restoring another. Scheduling policy decides which ready thread receives a CPU next. A handle is a mechanism for naming a Windows object; access checks are policy decisions based on the requested rights and security context. Mixing the two makes an implementation detail sound like a guarantee.
PEB means Process Environment Block
Section titled βPEB means Process Environment BlockβThe PEB is a user-mode structure Windows uses to keep process-wide information. It includes or leads to information such as:
- the process image base;
- loader bookkeeping for DLLs;
- process parameters such as the command line and environment;
- heap-related state;
- flags used by parts of Windows and the loader.
Model the PEB as one root in a graph of process metadata. Its fields lead to other structures such as process parameters and loader records; those records contain more links and values. Walking the graph requires the correct architecture and structure layout at every step. It is internal implementation data, so a field offset copied from an old 32-bit article is not automatically valid for a current 64-bit process.
The loader lists connected to the PEB describe modules known to the Windows loader. They are why low-level debuggers can teach you a lot about how DLL loading works. They are also live linked lists: another thread may load or unload a module while you are walking them.
TEB means Thread Environment Block
Section titled βTEB means Thread Environment BlockβEach thread has its own TEB. It contains thread-specific user-mode state, including a route to the PEB, stack boundaries, thread-local storage information, and error-related fields.
Each threadβs TEB points to the one PEB shared by the whole process, which in turn fans out into the structures described above:
On 32-bit x86 Windows code, the fs segment register helps locate thread state. On x86-64 Windows code, gs fills that role. That is why you may see instructions such as these in low-level Windows disassembly:
; 32-bit Windows: obtain the current process's PEB pointer.mov eax, dword ptr fs:[0x30]
; 64-bit Windows: obtain the current process's PEB pointer.mov rax, qword ptr gs:[0x60]This is not ordinary game data. fs: and gs: tell the CPU to use a
segment-relative address associated with the current thread. 0x30 and
0x60 are offsets in different Windows layouts, not an instruction to double every structure offset when moving from 32 to 64 bits. Wider pointers and
alignment help explain why layouts change; the actual offsets come from the
layout being examined. Treat these internal structures as an observation in
a particular architecture and Windows build, not as a public API contract.
0x30 bytes into the TEB.0x60 instead, roughly twice as far in.Why the course tool uses ToolHelp
Section titled βWhy the course tool uses ToolHelpβWalking internal loader pointers is useful when you are learning in a debugger. For a beginner-facing tool, Windows already provides a documented, read-only alternative: a ToolHelp snapshot.
CreateToolhelp32Snapshot asks Windows for a moment-in-time copy of a process, thread, or module list. Module32FirstW fills the first MODULEENTRY32W, and Module32NextW advances through the copy.
The shared wrapper keeps that unsafe boundary in one reviewed function. Here is the order of Windows calls it makes before it ever returns a module base and size:
A snapshot is owned evidence from one moment. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
copy the current module list
own the snapshot handle
read the saved base and size
optionally unload the live module
close the owned snapshot handle
report saved evidence and live validityResult: the module remains in this schedule; the snapshot still describes an earlier capture.
Final values: unload = no; handle = closed; saved_base = 0x400000; saved_size = 4096; record_read = true; live_module = still loaded; live_valid = true.
Read every step of the lessonβs example
Step 1, line 1. Start with the selected inputs and follow how the next operation changes the state.
unload = no.
Step 2, line 2. The snapshot owns its own copied record.
unload = no; handle = open; saved_base = 0x400000; saved_size = 4096.
Step 3, line 3. Reading the record does not retain the live module.
unload = no; handle = open; saved_base = 0x400000; saved_size = 4096; record_read = true.
Step 4, line 4. The target can change after capture.
unload = no; handle = open; saved_base = 0x400000; saved_size = 4096; record_read = true; live_module = still loaded.
Step 5, line 5. Scope exit closes the snapshot handle.
unload = no; handle = closed; saved_base = 0x400000; saved_size = 4096; record_read = true; live_module = still loaded.
Step 6, line 6. Result: the module remains in this schedule; the snapshot still describes an earlier capture.
unload = no; handle = closed; saved_base = 0x400000; saved_size = 4096; record_read = true; live_module = still loaded; live_valid = true.
A live module record is copied into a ToolHelp snapshot. OwnedHandle owns the snapshot handle, Module32FirstW fills MODULEENTRY32W, and a matching name returns its base and size. The snapshot handle closes at scope exit.
Read the steps
- CreateToolhelp32Snapshot copies the targetβs module list at this moment. This successful tour follows the first entry, whose name is the requested name.
- OwnedHandle takes responsibility for closing the snapshot handle. Keeping the wrapper alive keeps the snapshot available during enumeration.
- The correctly sized MODULEENTRY32W is filled by Module32FirstW. The copied entry contains szModule, modBaseAddr, and modBaseSize.
- The wrapper compares szModule with wanted_name without regard to letter case. This name matches. A nonmatching entry would advance with Module32NextW.
- The successful branch returns modBaseAddr and modBaseSize. On leaving the function, OwnedHandle closes the snapshot; the returned values do not keep the module loaded.
The field names are symbolic: this example does not invent a module address. A snapshot lookup observes a range without owning the moduleβs lifetime.
pub fn module(&self, wanted_name: &str) -> anyhow::Result<(usize, usize)> { let flags = TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32; // π‘οΈ SAFETY: the PID is a value and OwnedHandle closes the returned snapshot. let snapshot = unsafe { CreateToolhelp32Snapshot(flags, self.id) }?; let snapshot = OwnedHandle::from_raw(snapshot)?; let mut entry = MODULEENTRY32W { dwSize: size_of::<MODULEENTRY32W>() as u32, ..Default::default() };
// π SAFETY: entry has the size Windows requires and remains writable. unsafe { Module32FirstW(snapshot.raw(), &mut entry) }?; loop { if wide_text(&entry.szModule).eq_ignore_ascii_case(wanted_name) { return Ok((entry.modBaseAddr as usize, entry.modBaseSize as usize)); } // π Module32NextW either replaces entry or reports the end of the list. match unsafe { Module32NextW(snapshot.raw(), &mut entry) } { Ok(()) => {} Err(error) if no_more_files(&error) => break, Err(error) => return Err(error.into()), } } anyhow::bail!("module {wanted_name:?} was not found")}dwSize is important: it tells Windows which version and size of the output structure the caller prepared. Forgetting it is like handing someone a form without saying which form it is.
Snapshot does not mean frozen process
Section titled βSnapshot does not mean frozen processβThe list is stable, but the process is still running. A DLL could unload after the snapshot was created. A snapshot therefore has a time limit:
Enumeration tells you what existed during the snapshot. A later read must still be allowed to fail.
Result makes that race visible. The tool should report a normal error instead of assuming a previously seen address must remain valid.
Lab: connect the file map to the live process
Section titled βLab: connect the file map to the live processβStart one offline course game, then run both existing tools. 3CCD91 is the
sample Wesnoth RVA examined in Lessons 5.1 and 5.2; it is a position relative
to the module base, not a live address to reuse after a restart. Replace it
with an RVA you verified for the exact local build if your file differs:
cd windows-labscargo build --release --target i686-pc-windows-msvc ` --bin pe_inspector --bin module_inspector
.\target\i686-pc-windows-msvc\release\pe_inspector.exe ` "C:\Games\Wesnoth 1.14.9\wesnoth.exe"
.\target\i686-pc-windows-msvc\release\module_inspector.exe ` wesnoth.exe wesnoth.exe 3CCD91pe_inspector prints the preferred base stored in the file, and module_inspector prints the live base where the loader placed the module in this run. If the two differ, ASLR or relocation moved the whole module, and every byte inside it moved by the same amount. The RVA is measured from the moduleβs start, so it still names the same position, and module_inspector adds it to the live base to find where that position sits in this run.
Repeat with ac_client.exe and Quake3-UrT.exe. Do not copy one gameβs module name, pointer width, or address into another game.
What not to change
Section titled βWhat not to changeβDo not edit PEB flags, unlink modules from loader lists, or disguise process parameters. Those changes can break loader assumptions or conceal activity from diagnostic tools. This lesson uses the structures to explain Windows, then uses documented APIs for the actual program.
The buildable implementations are module_inspector.rs and the ToolHelp wrapper in process.rs.
module_inspector.rs
Show the codeHide the code
#[cfg(windows)]
fn main() -> anyhow::Result<()> {
use anyhow::Context;
use gha_windows_labs::Process;
let mut arguments = std::env::args().skip(1);
let process_name = arguments.next().unwrap_or_else(|| "wesnoth.exe".to_owned());
let module_name = arguments.next().unwrap_or_else(|| process_name.clone());
let requested_rva = arguments
.next()
.map(|text| usize::from_str_radix(text.trim_start_matches("0x"), 16))
.transpose()
.context("RVA must be hexadecimal, such as 7CCD91")?;
let process = Process::open_by_name(&process_name, false)?;
let (base, size) = process.module(&module_name)?;
let end = base.checked_add(size).context("module range overflowed")?;
println!("{} (PID {})", process.name(), process.id());
println!(" module: {module_name}");
println!(" live base: {base:#010x}");
println!(" size: {size:#010x}");
println!(" end: {end:#010x}");
if let Some(rva) = requested_rva {
anyhow::ensure!(rva < size, "RVA {rva:#x} is outside the module");
let address = base.checked_add(rva).context("base + RVA overflowed")?;
println!(" {base:#010x} + {rva:#010x} = {address:#010x}");
}
Ok(())
}
#[cfg(not(windows))]
fn main() {
eprintln!("This live module inspector uses ToolHelp and must run on Windows.");
}process.rs
Show the codeHide the code
use std::{ffi::c_void, mem::size_of};
use anyhow::{Context, Result};
use windows::Win32::{
Foundation::ERROR_NO_MORE_FILES,
System::{
Diagnostics::{
Debug::{FlushInstructionCache, ReadProcessMemory, WriteProcessMemory},
ToolHelp::{
CreateToolhelp32Snapshot, MODULEENTRY32W, Module32FirstW, Module32NextW,
PROCESSENTRY32W, Process32FirstW, Process32NextW, TH32CS_SNAPMODULE,
TH32CS_SNAPMODULE32, TH32CS_SNAPPROCESS,
},
},
Memory::{
MEM_COMMIT, MEMORY_BASIC_INFORMATION, PAGE_EXECUTE, PAGE_EXECUTE_READ,
PAGE_EXECUTE_READWRITE, PAGE_EXECUTE_WRITECOPY, PAGE_GUARD, PAGE_NOACCESS,
PAGE_PROTECTION_FLAGS, PAGE_READONLY, PAGE_READWRITE, PAGE_WRITECOPY, VirtualProtectEx,
VirtualQueryEx,
},
SystemInformation::{
IMAGE_FILE_MACHINE, IMAGE_FILE_MACHINE_I386, IMAGE_FILE_MACHINE_UNKNOWN,
},
Threading::{
IsWow64Process2, OpenProcess, PROCESS_ACCESS_RIGHTS, PROCESS_QUERY_INFORMATION,
PROCESS_VM_OPERATION, PROCESS_VM_READ, PROCESS_VM_WRITE,
},
},
};
use super::OwnedHandle;
#[derive(Clone, Debug)]
pub struct ProcessEntry {
pub id: u32,
pub name: String,
}
#[derive(Clone, Copy, Debug)]
pub struct MemoryRegion {
pub base: usize,
pub size: usize,
pub readable: bool,
pub writable: bool,
pub executable: bool,
}
#[derive(Debug)]
pub struct Process {
handle: OwnedHandle,
id: u32,
name: String,
}
fn wide_text(buffer: &[u16]) -> String {
let end = buffer
.iter()
.position(|unit| *unit == 0)
.unwrap_or(buffer.len());
String::from_utf16_lossy(&buffer[..end])
}
fn no_more_files(error: &windows::core::Error) -> bool {
error.code() == ERROR_NO_MORE_FILES.to_hresult()
}
impl Process {
/// Lists every running process with ToolHelp.
pub fn list() -> Result<Vec<ProcessEntry>> {
// SAFETY: no borrowed pointers are passed; the returned snapshot is owned below.
let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }?;
let snapshot = OwnedHandle::from_raw(snapshot)?;
let mut raw = PROCESSENTRY32W {
dwSize: size_of::<PROCESSENTRY32W>() as u32,
..Default::default()
};
// SAFETY: `raw` is correctly sized and writable for the duration of the call.
unsafe { Process32FirstW(snapshot.raw(), &mut raw) }?;
let mut processes = Vec::new();
loop {
processes.push(ProcessEntry {
id: raw.th32ProcessID,
name: wide_text(&raw.szExeFile),
});
// SAFETY: `raw` remains a valid PROCESSENTRY32W output buffer.
match unsafe { Process32NextW(snapshot.raw(), &mut raw) } {
Ok(()) => {}
Err(error) if no_more_files(&error) => break,
Err(error) => return Err(error.into()),
}
}
Ok(processes)
}
pub fn find(name: &str) -> Result<ProcessEntry> {
Self::list()?
.into_iter()
.find(|entry| entry.name.eq_ignore_ascii_case(name))
.with_context(|| format!("process {name:?} is not running"))
}
pub fn open_by_name(name: &str, allow_write: bool) -> Result<Self> {
let entry = Self::find(name)?;
Self::open(entry, allow_write)
}
pub fn open(entry: ProcessEntry, allow_write: bool) -> Result<Self> {
let mut access: PROCESS_ACCESS_RIGHTS = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ;
if allow_write {
access |= PROCESS_VM_OPERATION | PROCESS_VM_WRITE;
}
Self::open_with_access(entry, access)
}
pub fn open_with_access(entry: ProcessEntry, access: PROCESS_ACCESS_RIGHTS) -> Result<Self> {
// SAFETY: the PID is a value; inheritance is disabled; `OwnedHandle` owns the result.
let handle = unsafe { OpenProcess(access, false, entry.id) }?;
Ok(Self {
handle: OwnedHandle::from_raw(handle)?,
id: entry.id,
name: entry.name,
})
}
pub fn is_32_bit(&self) -> Result<bool> {
let mut process_machine = IMAGE_FILE_MACHINE::default();
let mut native_machine = IMAGE_FILE_MACHINE::default();
// SAFETY: both output pointers remain valid for the call.
unsafe {
IsWow64Process2(
self.handle.raw(),
&mut process_machine,
Some(&mut native_machine),
)?;
}
Ok(process_machine == IMAGE_FILE_MACHINE_I386
|| (process_machine == IMAGE_FILE_MACHINE_UNKNOWN
&& native_machine == IMAGE_FILE_MACHINE_I386))
}
pub fn id(&self) -> u32 {
self.id
}
pub fn name(&self) -> &str {
&self.name
}
pub fn raw_handle(&self) -> windows::Win32::Foundation::HANDLE {
self.handle.raw()
}
pub fn read_exact(&self, address: usize, output: &mut [u8]) -> Result<()> {
let mut bytes_read = 0_usize;
// SAFETY: `output` is writable for its length. Windows validates the remote range.
unsafe {
ReadProcessMemory(
self.handle.raw(),
address as *const c_void,
output.as_mut_ptr().cast(),
output.len(),
Some(&mut bytes_read),
)?;
}
anyhow::ensure!(bytes_read == output.len(), "short read at {address:#x}");
Ok(())
}
pub fn read_bytes(&self, address: usize, count: usize) -> Result<Vec<u8>> {
let mut bytes = vec![0_u8; count];
self.read_exact(address, &mut bytes)?;
Ok(bytes)
}
pub fn read_u32(&self, address: usize) -> Result<u32> {
let bytes: [u8; 4] = self
.read_bytes(address, 4)?
.try_into()
.expect("the read length is exactly four");
Ok(u32::from_le_bytes(bytes))
}
pub fn read_f32(&self, address: usize) -> Result<f32> {
Ok(f32::from_bits(self.read_u32(address)?))
}
pub fn write_exact(&self, address: usize, bytes: &[u8]) -> Result<()> {
let mut bytes_written = 0_usize;
// SAFETY: `bytes` is readable for its length. Windows validates the remote range.
unsafe {
WriteProcessMemory(
self.handle.raw(),
address as *const c_void,
bytes.as_ptr().cast(),
bytes.len(),
Some(&mut bytes_written),
)?;
}
anyhow::ensure!(bytes_written == bytes.len(), "short write at {address:#x}");
Ok(())
}
pub fn write_u32(&self, address: usize, value: u32) -> Result<()> {
self.write_exact(address, &value.to_le_bytes())
}
/// Writes machine code, flushes the CPU instruction cache, and restores protection.
pub fn write_code(&self, address: usize, bytes: &[u8]) -> Result<()> {
let mut old = PAGE_PROTECTION_FLAGS::default();
// SAFETY: Windows validates the target range and writes `old` before returning.
unsafe {
VirtualProtectEx(
self.handle.raw(),
address as *const c_void,
bytes.len(),
PAGE_EXECUTE_READWRITE,
&mut old,
)?;
}
let write_result = self.write_exact(address, bytes);
if write_result.is_ok() {
// SAFETY: the process handle is live and the written range is the one above.
unsafe {
FlushInstructionCache(
self.handle.raw(),
Some(address as *const c_void),
bytes.len(),
)?;
}
}
let mut ignored = PAGE_PROTECTION_FLAGS::default();
// SAFETY: this restores the protection returned by the first call.
let restore_result = unsafe {
VirtualProtectEx(
self.handle.raw(),
address as *const c_void,
bytes.len(),
old,
&mut ignored,
)
};
write_result?;
restore_result?;
Ok(())
}
pub fn module(&self, wanted_name: &str) -> Result<(usize, usize)> {
// Include SNAPMODULE32 so a 64-bit tool can list a 32-bit game module.
// SAFETY: the PID is valid and the returned snapshot is owned below.
let snapshot =
unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, self.id) }?;
let snapshot = OwnedHandle::from_raw(snapshot)?;
let mut raw = MODULEENTRY32W {
dwSize: size_of::<MODULEENTRY32W>() as u32,
..Default::default()
};
// SAFETY: `raw` is correctly sized and writable.
unsafe { Module32FirstW(snapshot.raw(), &mut raw) }?;
loop {
if wide_text(&raw.szModule).eq_ignore_ascii_case(wanted_name) {
return Ok((raw.modBaseAddr as usize, raw.modBaseSize as usize));
}
// SAFETY: `raw` remains a valid output buffer.
match unsafe { Module32NextW(snapshot.raw(), &mut raw) } {
Ok(()) => {}
Err(error) if no_more_files(&error) => break,
Err(error) => return Err(error.into()),
}
}
anyhow::bail!("module {wanted_name:?} was not found in {}", self.name)
}
pub fn regions(&self, start: usize, end: usize) -> Result<Vec<MemoryRegion>> {
let mut current = start;
let mut regions = Vec::new();
while current < end {
let mut raw = MEMORY_BASIC_INFORMATION::default();
// SAFETY: `raw` is a valid output structure; Windows validates the address.
let returned = unsafe {
VirtualQueryEx(
self.handle.raw(),
Some(current as *const c_void),
&mut raw,
size_of::<MEMORY_BASIC_INFORMATION>(),
)
};
if returned == 0 {
break;
}
let protection = raw.Protect;
let blocked = protection.contains(PAGE_GUARD) || protection.contains(PAGE_NOACCESS);
let readable = !blocked
&& raw.State == MEM_COMMIT
&& [
PAGE_READONLY,
PAGE_READWRITE,
PAGE_WRITECOPY,
PAGE_EXECUTE_READ,
PAGE_EXECUTE_READWRITE,
PAGE_EXECUTE_WRITECOPY,
]
.iter()
.any(|flag| protection.contains(*flag));
let writable = readable
&& [
PAGE_READWRITE,
PAGE_WRITECOPY,
PAGE_EXECUTE_READWRITE,
PAGE_EXECUTE_WRITECOPY,
]
.iter()
.any(|flag| protection.contains(*flag));
let executable = !blocked
&& [
PAGE_EXECUTE,
PAGE_EXECUTE_READ,
PAGE_EXECUTE_READWRITE,
PAGE_EXECUTE_WRITECOPY,
]
.iter()
.any(|flag| protection.contains(*flag));
let base = raw.BaseAddress as usize;
let size = raw.RegionSize;
regions.push(MemoryRegion {
base,
size,
readable,
writable,
executable,
});
let next = base.checked_add(size).context("memory map overflowed")?;
anyhow::ensure!(next > current, "memory map did not advance");
current = next;
}
Ok(regions)
}
}References: Microsoft ToolHelp snapshots, Module32FirstW, and Microsoft PE format.
Separate file from execution
Which item exists only after Windows creates a running process?
A program file is input to process creation. Windows maps it, resolves imports, creates execution state, and adds private memory not present on disk.
Finished Lesson 6.1?
Mark it done and the lesson list shows a beside it. A chapter whose lessons are all done is marked done too. Progress is saved in this browser only.
Plain Markdown, saved in this browser.
This book grew out of the original Game Hacking Academy by attilathedud and has been rewritten as a beginner-first course. Like the original repository, its source is published under the Apache License 2.0.