When a Windows program calls something innocuous like CreateFileW, an
impressive amount of machinery springs into action between your line of C++ and the
disk controller. Understanding that machinery — even at a high level — pays off the
first time you have to debug a strange hang, write a driver, or audit a piece of
low-level code. This post walks through the layers, from the friendly Win32 API
you already use to the native NT system calls underneath.
The three layers you actually touch
For practical purposes there are three layers between user code and the kernel:
- The Win32 API —
kernel32.dll,user32.dll,advapi32.dll. This is the documented surface most code targets. - The Native API — exported by
ntdll.dll. Functions here are prefixedNt*orZw*, and most Win32 calls eventually translate into one of them. - The system call boundary — a
syscallinstruction that traps into the kernel, wherentoskrnl.exetakes over.
Following CreateFile down the stack
Let's trace a single call. In Win32, you'd write:
// Win32 — the layer most apps target.
HANDLE h = CreateFileW(
L"C:\\temp\\hello.txt",
GENERIC_READ,
FILE_SHARE_READ,
nullptr,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
nullptr);
Inside kernel32.dll, that path is normalized (drive letters resolved,
\\?\ prefix added) and the call is forwarded to
NtCreateFile in ntdll.dll:
// Native API — what kernel32 actually calls under the hood.
extern "C" NTSTATUS NTAPI NtCreateFile(
PHANDLE FileHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PIO_STATUS_BLOCK IoStatusBlock,
PLARGE_INTEGER AllocationSize,
ULONG FileAttributes,
ULONG ShareAccess,
ULONG CreateDisposition,
ULONG CreateOptions,
PVOID EaBuffer,
ULONG EaLength);
NtCreateFile sets up a handful of registers and executes a
syscall. Control transfers into the kernel, the I/O manager builds an
IRP (I/O Request Packet), routes it to the file system driver, which in turn talks
to a storage driver. The handle you get back is just an index into your process's
handle table.
Calling the Native API directly
You can skip the Win32 layer entirely. There's almost never a reason to do so in
application code, but it's instructive to try once. Here's a minimal sample that
reads the system's process list using NtQuerySystemInformation:
// Resolve the function from ntdll at runtime.
using NtQuerySystemInformation_t = NTSTATUS(NTAPI*)(
ULONG, PVOID, ULONG, PULONG);
HMODULE ntdll = GetModuleHandleW(L"ntdll.dll");
auto NtQuerySystemInformation =
(NtQuerySystemInformation_t)GetProcAddress(
ntdll, "NtQuerySystemInformation");
ULONG needed = 0;
NtQuerySystemInformation(5 /* SystemProcessInformation */,
nullptr, 0, &needed);
How handles really work
A handle isn't a pointer — it's an opaque index into a per-process handle table
managed by the Object Manager. When you call CloseHandle, the kernel
decrements a reference count on the underlying object. When it hits zero, the
object is freed. This is why leaking handles is so easy: nothing in your address
space looks broken when it happens.
Tools that make this visible
- Process Explorer — see every handle, every loaded module, in real time.
- WinDbg with the
!handleand!objectextensions. - API Monitor — log every Win32 and Native API call your process makes.
- ETW — system-wide tracing for performance investigations.
Where to go next
A few directions if this whetted your appetite: write a tiny tool with the Native
API, walk the PEB to enumerate loaded modules, or read the Windows Internals
book series. Once you've seen the full path from CreateFile to the
disk, every weird platform bug becomes a little less mysterious.
Want help going deeper? Our coaching tracks include a Windows internals module with hands-on exercises in WinDbg. Get in touch to join a cohort.