Why do Windows applications enter the kernel through stubs in ntdll.dll instead of issuing the syscall instruction themselves, and what does that imply about Windows system call numbers?
answer
- the contract sits one level up
- subsystems over a native API
- the number is a build artefact
- stubs are the monitored chokepoint
- Nt and Zw differ in the kernel
basics
~20 sOn Windows the supported contract is the Win32 API and the native routines ntdll.dll exports, not the instruction-level interface. Microsoft renumbers system services freely between builds, so ntdll.dll is the only stable syscall boundary and hardcoded service numbers break on the next update.
solid answer
~50 sNT was designed with a *native* API — the `Nt*` routines — and environment subsystems layered on top; Win32 is simply the subsystem that survived. So the layering is deliberate: `kernel32.dll` implements documented Win32 semantics, calls the matching `ntdll.dll` stub, and only that stub loads a system service number and executes `syscall`. Because the boundary is the DLL and not the instruction, Microsoft is free to add, remove and renumber system services in any build, and does — the number behind `NtCreateFile` differs across Windows versions. That is the opposite arrangement from Linux, where syscall numbers are a committed ABI and a static binary can trap directly. Practically: never hardcode service numbers, always call through `ntdll.dll`, and expect security products to hook `ntdll.dll` precisely because it is the chokepoint — which is also why evasive code rebuilds the stubs from the on-disk copy.
go deeper
Know that Windows programs call documented APIs in DLLs like kernel32.dll, and that ntdll.dll sits beneath them as the last user-mode step before the kernel.
Describe the chain from a Win32 function to an ntdll.dll Nt* stub to the syscall instruction, and explain that a system service number selects the kernel routine from a dispatch table.
Argue why the supported boundary is deliberately the DLL rather than the instruction, and connect it to consequences: service numbers churn across builds so hardcoding them is a time bomb, and ntdll.dll hooking is the monitoring chokepoint attackers route around.
Own the ABI-stability decision itself: where you place a supported boundary determines how freely the layer beneath it may change, and site your telemetry where untrusted code cannot bypass it rather than inside the process being watched.
## The native API and the subsystem above it Windows NT was built with two API layers by design. The **native API** is the set of `Nt*` routines the kernel actually implements — `NtCreateFile`, `NtOpenProcess`, `NtQueryInformationProcess` and several hundred more. Above it were meant to sit several **environment subsystems**, each presenting a different personality to applications: Win32, OS/2 and POSIX all shipped in early NT. Win32 is the one that survived and became synonymous with Windows, but the architecture beneath it never collapsed. That history explains the call chain. `CreateFileW` in `kernel32.dll`/`kernelbase.dll` is a Win32 personality function: it parses a DOS path, applies Win32 flag semantics and defaults, builds an `OBJECT_ATTRIBUTES` structure, and calls `NtCreateFile` in `ntdll.dll`. Only the `ntdll.dll` stub touches the CPU's mode-switch instruction. ## What the stub does A stub is a handful of instructions: move a **system service number** into `EAX`, put the argument pointer where the kernel expects it, execute `syscall` (on x64; older 32-bit builds used `sysenter` or `int 2Eh`), return. On the kernel side the dispatcher uses that number as an index into the system service descriptor table, which yields the kernel routine's address and its argument count, then copies and validates arguments before calling it. The number is *not* part of any published contract. It is generated per build from the ordering of entries in the table, so inserting one new service can shift everything after it. ``` ; conceptual shape of an ntdll.dll x64 stub mov r10, rcx ; syscall clobbers rcx, so the first argument is preserved mov eax, <service #> ; index into the system service table for THIS build syscall ret ``` ## Nt versus Zw `ntdll.dll` exports both `NtCreateFile` and `ZwCreateFile`, and in user mode they are the same stub. The distinction is meaningful inside the kernel, where `ntoskrnl.exe` also exports both: the `Zw` form sets the *previous mode* to `KernelMode`, telling the service its caller is trusted, so buffer addresses are not validated as user pointers and access checks may be skipped; the `Nt` form leaves the previous mode as it is. That is why kernel-mode drivers are told to call the `Zw` variants when acting on their own behalf, and why a driver that carelessly calls a `Zw` routine on a pointer that came from user mode has just created a privilege-escalation bug. ## Why the indirection is worth the layer Three things fall out of putting the boundary in a DLL: 1. **Freedom to evolve the kernel.** Services can be split, merged, renumbered or removed between releases because the only thing that must stay in step is the `ntdll.dll` shipped with the same kernel. Microsoft has used that freedom for decades. 2. **A place to put user-mode work.** `ntdll.dll` also holds the loader, the heap, thread startup and exception dispatch — plenty of work that need not run in kernel mode happens there, which keeps the kernel smaller. 3. **A single, patchable chokepoint.** Compatibility shims and instrumentation can be applied at the DLL boundary without kernel changes. ## What breaks when you route around it Because the stubs are trivially regenerable, some software builds its own: it reads service numbers out of the on-disk `ntdll.dll`, or sorts the exported `Nt*` addresses to infer them, and issues `syscall` directly. Endpoint security products hook the in-memory `ntdll.dll` stubs to observe native calls, so "direct syscall" and "stub unhooking" techniques exist specifically to evade that monitoring. The defensive answer is not to hook harder in user mode but to observe from a place user-mode code cannot skip — kernel callbacks and kernel event-tracing providers. For ordinary engineering the rule is blunt: call the documented Win32 API; reach for native `Nt*` exports only when Win32 genuinely cannot express what you need, and then only through `ntdll.dll`'s exported names; never bake a service number into a binary. A build that ships hardcoded numbers works on the machine it was tested on and fails, often catastrophically and with no useful error, after the next feature update. ## The contrast an interviewer is fishing for On Linux the numeric syscall interface is itself a stability promise, so a statically linked binary trapping directly is supported practice. On Windows the promise lives one level up, in the DLLs. Both are coherent designs; the mistake is assuming Windows made the same promise and reasoning about `ntdll.dll` as though it were merely a convenience wrapper.
- What is the practical difference between the Nt and Zw exports?In `ntdll.dll` they are the same stub. Inside the kernel they differ: the `Zw` form sets previous mode to `KernelMode`, so the service treats pointers as trusted and can bypass access checks, while the `Nt` form honours the caller's actual mode. Drivers call `Zw` for their own work; calling it on data that originated in user mode is a classic privilege-escalation bug.
- Why do security products hook ntdll.dll, and why is that hooking fragile?It is the last user-mode point every native call passes through, so a hook there sees file, process and memory operations with full argument context. It is fragile because the hooks live in the target's own writable memory: code can restore the original bytes from the on-disk image, or build fresh stubs and issue `syscall` itself. Durable visibility has to come from kernel callbacks or kernel tracing providers instead.
- Can you call native NtXxx routines from an application, and should you?You can — resolve them from `ntdll.dll` with `GetProcAddress`, and some are documented. You mostly should not: the semantics of many are undocumented, argument structures change, and the Win32 layer above exists to give you stable behaviour. Reach for a native routine only when Win32 cannot express what you need, and never by hardcoding a service number instead of a name.
saying these in an interview costs you the question
- Assumes Windows syscall numbers are a stable ABI like Linux's
- Thinks kernel32.dll executes the syscall instruction itself
- Says Nt and Zw are different kernel implementations
- Believes hardcoded service numbers are safe once tested
- Calls ntdll.dll just a thin convenience wrapper