Level 0 · Chapter 5

Ntdll & the user/kernel boundary

The thinnest layer in Windows: how a Win32 API call actually becomes a transition into kernel mode.

Every chapter so far has quietly assumed that user-mode code can eventually ask the kernel to do something — create a file, allocate memory, start a thread. This chapter is about the one narrow place where that actually happens: ntdll.dll, and the Nt*/Zw* functions it exports.

Kernel32 is not the bottom

When you call CreateFile from a C++ or C# program, you're calling a Win32 API exported by kernel32.dll. But kernel32.dll doesn't itself know how to talk to the kernel — almost nothing does, by design. Instead, CreateFile does some Win32-specific bookkeeping (translating Win32-style paths and error codes, for instance) and then calls straight into ntdll.dll's NtCreateFile.

ntdll.dll is loaded into every process automatically — it's the one DLL the loader can't skip, because it's the only bridge to the kernel that exists. Everything above it (Win32, .NET, COM, the GUI subsystem) is, from the kernel's point of view, optional scaffolding built on top of the same few hundred Nt* entry points.

What an Nt* function actually does

Take NtCreateFile as a concrete example. Its job is small and mechanical:

  1. Package the arguments — path, desired access, security descriptor, and so on — into the exact layout the kernel's system call dispatcher expects.
  2. Load a system call number into a register (eax/rax) — a small integer identifying which kernel routine to invoke. This number is a stable-per-build-but-unstable-across-versions detail; Microsoft has never documented or promised these numbers won't change, which is precisely why calling Nt* functions directly (bypassing ntdll.dll) is a technique associated with malware trying to evade hooks placed on the documented Win32 layer.
  3. Execute the transition instruction — syscall on x86-64 — which is what actually switches the processor from ring 3 to ring 0.
  4. Return the status. The kernel routine runs, and control returns to ntdll.dll with an NTSTATUS value, which kernel32.dll (if it was the caller) translates into the Win32-style error code (GetLastError()) most application code actually checks.

That's the entire job: marshal, transition, return. ntdll.dll implements no policy of its own — it doesn't decide whether you're allowed to open the file, whether the path exists, or how the access check works. It's a courier, not a decision-maker.

What "the transition" costs

The syscall instruction is not free, and understanding why is useful for anyone who's ever wondered why a chatty program (one that makes a very large number of small system calls) tends to run slower than expected on the same hardware:

  • The CPU has to save enough state to resume the user-mode thread later and to prevent user mode from seeing kernel-mode register contents.
  • The kernel has to switch to a kernel-mode stack — a user-mode stack cannot be trusted to be valid or non-corrupted.
  • On processors affected by speculative-execution vulnerabilities (Meltdown and its relatives, from 2018 onward), an additional page-table switch (Kernel Page Table Isolation) may happen on entry and exit specifically to stop user mode from ever being able to speculatively read kernel memory — a direct, permanent tax added after the fact, once the vulnerability class was understood.

None of this is visible to the calling program; it's simply why "minimize system calls" has been practical performance advice since long before anyone had heard of Meltdown.

Undocumented, but not unstable within a version

A recurring point of confusion: Nt*/Zw* functions are described as "the native API," and are genuinely undocumented as a public contract — Microsoft reserves the right to change system call numbers, add parameters, or restructure internal behavior between OS versions without notice. What is stable is the documented Win32 surface (kernel32.dll, advapi32.dll, and friends) — that's the actual, supported contract applications are meant to rely on. Debuggers, forensic tools, and security research routinely work at the Nt* layer anyway, because it's the layer closest to ground truth about what the kernel is actually being asked to do — but doing so means accepting that the exact entry points can shift under you release to release.

A common mistake

It's easy to think of kernel32.dll as "the real API" and ntdll.dll as an implementation detail underneath it that doesn't matter for understanding Windows. In practice it's the reverse: ntdll.dll is closer to what the operating system actually is, and kernel32.dll (along with the rest of Win32) is one particular, documented, stable way of presenting that underlying reality to application developers. Debugging tools that show call stacks bottoming out at ntdll!NtCreateFile aren't showing you an obscure implementation detail — they're showing you the actual boundary where your program stopped being "just code" and started asking the kernel for something.

Where this connects

  • Every one of the Executive's managers — Processes & threads, Memory management, the I/O Manager — is reached exclusively through this same Nt* boundary; there is no other front door into kernel-mode Windows from user mode.
  • WOW64 adds one extra hop before this same boundary for 32-bit processes, translating arguments before the real syscall happens.
  • Kernel mechanisms picks up exactly where this chapter leaves off — what happens on the kernel side of that same syscall instruction.