Skip to lesson
Game Hacking Academy
Preparing lesson text…
Exit reader mode
Voice and speed

Preparing read-aloud controls…

Lesson 12.5 Listening edition

From Win32 Calls to Kernel Services

A Windows API call crosses layers of exports, native APIs, and system calls before a kernel service acts on a request.

About 28 minutes

Section: One function call can cross several layers.

The course tool can now name a process, open and own a limited handle, inspect page metadata, and observe threads. Each operation was a call to a Windows A P I, application programming interface. The remaining question is where such a call goes when the tool asks the operating system to do something it cannot do directly.

A call to a Windows function such as VirtualQueryEx does not instantly jump into the deepest part of Windows. It usually passes through layers.

Alternative: Follow the call like a routed request: the public entry passes through implementation layers before a kernel service performs the privileged operation.

Lesson diagram

Use three separate ideas: contract, implementation path, and privilege transition. Win32 documents the contract application code may rely on. User-mode DLLs implement or forward that contract. A system-call instruction performs the controlled C P U, central processing unit, transition for work that requires kernel privilege.

The boxes are software layers, not necessarily four separate processes. kernel32.dll and ntdll.dll are mapped into the same user-mode process as our program. Microsoft may change the implementation path while preserving the Win32 contract, which is why “calling a deeper layer” is usually less stable rather than more capable.

A call passes through layers in one process before the kernel.

Section: User mode and kernel mode are permission levels.

Most game code, application code, and Windows D L L, dynamic link library, code runs in user mode. A user-mode mistake should not be able to overwrite the kernel or another process merely by using a bad pointer.

The Windows kernel and drivers run in kernel mode, where code can manage page tables, devices, scheduling, and other system-wide resources. A bug there can affect the whole computer. That is why the course does not turn ordinary game experiments into kernel-driver experiments.

The C P U enforces this border. A system call changes privilege through a defined entry point; it is not a magic function name and it is not the same thing as calling a normal function.

This design is often described as limited direct execution. The game normally runs directly on the C P U for speed, but in user mode and within its address-space permissions. Timers and interrupts let Windows regain control for scheduling. A system call lets the game request a restricted operation through a validated entry path.

The model resolves a common confusion: Windows is not interpreting every ordinary game instruction. Most instructions execute natively. Hardware privilege, address translation, interrupts, and controlled entries provide the limits that let the O S, operating system, remain in control.

Section: Win32 is the documented application interface.

The documented Win32 A P I is the part application developers are normally expected to call. Examples from this course include:

ReadProcessMemory for a read-only inspector with query-and-read rights;

VirtualQueryEx for page metadata;

CreateFileW for files and local named pipes;

CreateFileMappingW for a shared-memory object.

Microsoft can change how a supported function is implemented while keeping its documented behavior. On modern Windows, an export in kernel32.dll may forward to another D L L such as KernelBase.dll. Your source code still calls the stable Win32 name.

In brief: A documented Windows function can keep its contract while the DLLs and internal layers implementing it change.

A forwarded export is not a jump instruction the loader inserts. The export table entry itself can hold a string such as KERNELBASE.CreateFileW in place of a code address. When GetProcAddress resolves that entry, it reads the string, locates the named D L L, and looks up the function there instead — following another forward if that entry also happens to be one. Whoever asked for kernel32!CreateFileW never sees this redirection; they get back a usable address in KernelBase.dll either way, which is exactly why Microsoft can move an implementation between DLLs across Windows versions without breaking the caller’s import:

Lesson diagram

Section: Native APIs sit closer to the system call.

ntdll.dll exposes many functions whose names begin with Nt, such as NtQueryVirtualMemory. Some native functions are documented for limited uses and many are not stable application contracts.

For ordinary tools, prefer documented interfaces:

“Closer to the kernel” does not mean “better for normal application code.”

Calling an undocumented entry point or copying a system-call number from one Windows build can make a tool fragile. Numbers and internal structures can change. The course resolves one native export so you can see the layer, but it deliberately does not call it or build a direct-syscall bypass.

Section: D L L exports are names offered to other code.

A D L L can publish an export table. Each exported name maps to an entry that the loader can resolve. GetProcAddress asks a loaded module for one of those exports.

The returned address is a virtual address in the current process. Address Space Layout Randomization, or A S L R, address space layout randomization,, means the same D L L can be mapped at a different base address in another process or after a reboot. Store the name or an R V A, relative virtual address, when that is the stable identity; do not paste a displayed absolute address into tomorrow’s program.

Narration: A printed export address belongs to this process and mapping. Reusing its name or verified relative location avoids assuming tomorrow’s mapping uses the same base.

Section: Build the address inspector.

The lab gets handles to DLLs already loaded in its own process, resolves three names, and prints their process-local addresses. It never opens a game process and never calls the native function:

Lesson diagram

Additional detail: Complete lab source: api_layers.rs.

Code example. The code’s comments explain: SAFETY: module is loaded in this process and name is a terminated, static A S C I I string. We only print the returned address. SAFETY: these D L L names are terminated, static UTF-16 strings. Windows loads both modules before a normal console program reaches main. SAFETY: ntdll.dll is likewise a terminated, static UTF-16 name for a module that is already loaded in every normal Windows process.

Section: Why GetModuleHandleW instead of LoadLibraryW?.

Both DLLs are already present in a normal Windows console process. GetModuleHandleW borrows their existing module handles; it does not request another load or add a loader reference that we would later need to release.

Section: Why does GetProcAddress use an A S C I I name?.

P E, portable executable, export names are byte strings. The s! macro creates a terminated static PCSTR. D L L file names use Windows wide strings, so GetModuleHandleW receives values from the w! macro instead.

This original attempt mixes byte strings and wide strings. It is intentionally a non-compiling example: the Windows bindings reject the wrong parameter types before any call runs.

Before: rejected string widths

Code example. The code’s comments explain: SAFETY: static terminated strings outlive lookup; widths must match the A P I. SAFETY: the borrowed module stays loaded during this export lookup.

Use w! for the wide D L L filename and s! for the byte-encoded export name. Both strings are static and terminated; the D L L handle is borrowed for the lookup.

In this version, FARPROC already means an optional generic function pointer. ok_or_else removes that Option wrapper; the helper returns its address as usize for display. It does not call that pointer as VirtualQueryEx. The version-pinned type and lookup signature explain this return-type choice.

Improved code

Optional: practise matching the full A P I names, module argument and address type to their jobs.

Code example. The code’s comments explain: SAFETY: static terminated strings outlive lookup; widths must match the A P I. SAFETY: the borrowed module stays loaded during this export lookup.

Show the diff

Code example. The code’s comments explain: SAFETY: static terminated strings outlive lookup; widths must match the A P I. SAFETY: the borrowed module stays loaded during this export lookup.

Takeaway: the A P I’s string type determines the encoding, even when both arguments look like ordinary names.

The types make the two encodings visible instead of silently mixing them.

Section: Read the Windows signature before translating it.

Microsoft’s C documentation uses names such as DWORD, BOOL, and HANDLE to describe the A P I’s binary contract. Match that contract rather than choosing a Rust type just because its name sounds similar:

Lesson table

These distinctions come from Windows coding conventions and Windows data types. Read each function’s return-value section too: its success and failure rules can be more specific than its return type.

The lab’s string-width mistake is one instance of a broader convention. In paired string APIs, W accepts UTF-16 code units and A accepts 8-bit text in the A P I’s ANSI encoding; A is not a promise of A S C I I or UTF-8. C headers may select an unsuffixed name using the UNICODE macro. Calling the explicit W function keeps the choice visible in Rust. See Working with Strings.

The A B I, application binary interface,, or application binary interface, also specifies how arguments and results pass between caller and callee. WINAPI and CALLBACK in C signatures mark a calling convention, not an access right. Generated bindings supply the matching declarations. For a callback declared with that convention, match its exact signature and Rust’s extern "system" A B I: its underlying convention depends on the target, including stdcall on 32-bit Windows for non-variadic functions.

In brief: A handle’s size does not tell you who owns it. Check the A P I that produced it and its documented release rule; this lab’s borrowed module handle must not be released with FreeLibrary.

The GetModuleHandleW contract is the ownership rule behind that note. Likewise, a string pointer must stay valid for as long as the receiving A P I uses it; a pointer wrapper alone does not own its storage.

Section: Which windows-rs crate does what?.

The crate names describe different jobs in the Rust bindings. They do not change which Windows layer runs a call or what the caller may access.

Lesson table

The generation tools have separate jobs: windows-bindgen turns Windows metadata into Rust bindings; windows-metadata reads and writes that metadata; windows-clang extracts C/C++ header declarations using libclang. The lab consumes pre-generated bindings. Add support or generation crates only when your own task needs them; many support types are already re-exported by the A P I crate.

One similarly named crate, windows-reference, boxes a value for a WinRT signature that requires IReference<T>. That is a Windows interface object, distinct from ordinary Rust borrowing with &T. The address inspector needs no such boxed value.

Section: Run it and compare two launches.

The program prints exported function names beside their addresses in this process. Its first run establishes a local observation; the second run tests whether those addresses are safe to treat as permanent identifiers.

Terminal window

Code example. The surrounding prose explains this example; the full lesson contains the exact code.

Run it again after restarting the virtual machine and compare the addresses. A difference is expected. The names and behavior are the meaningful pieces.

Then use module_inventory against wesnoth.exe, ac_client.exe, or Quake3-UrT.exe. You are looking at two sides of the same loader idea:

module_inventory shows which D L L images the game loaded;

api_layers shows how one process asks a loaded D L L for an exported name.

The complete program is api_layers.rs.

References: libraries and native-system-service entry points, using Nt and Zw routines, GetModuleHandleW, and GetProcAddress.