Loading learning content...
Every operating system must provide a controlled mechanism for user-mode applications to request services from the kernel. In Windows, this mechanism—the system call interface—is both sophisticated and carefully hidden from typical application developers.
Unlike Unix-like systems where system calls are well-documented and frequently called directly, Windows deliberately obscures its system call interface. The official API (Win32/Win64) is implemented in user-mode DLLs that internally use system calls, but developers are strongly discouraged from calling system calls directly.
This architectural choice has profound implications for Windows programming, security, and compatibility. Understanding how Windows system calls work—even if you never call them directly—provides crucial insight into Windows internals and helps explain behaviors that might otherwise seem mysterious.
By the end of this page, you will understand how Windows transitions between user mode and kernel mode, the role of ntdll.dll as the system call gateway, how system call numbers work, the actual x64 system call instruction sequence, and why Microsoft deliberately keeps this interface undocumented.
A system call (syscall) is a request from a user-mode process to the operating system kernel. Because the kernel operates in a protected execution environment with elevated privileges, user-mode code cannot simply jump into kernel code—there must be a controlled transition mechanism.
Why System Calls Exist:
Privilege isolation: User applications run in restricted mode (Ring 3 on x86/x64) and cannot directly access hardware, kernel memory, or execute privileged CPU instructions
Resource protection: The kernel manages all system resources (memory, files, network, devices) and must mediate access to prevent conflicts and enforce security
Abstraction: System calls provide a stable interface that decouples applications from hardware specifics
Security boundary: Every transition from user mode to kernel mode is a potential attack surface, so it must be carefully controlled
The Windows System Call Architecture:
Unlike Unix systems where applications can call syscalls via simple wrapper functions, Windows has a multi-layered architecture:
┌─────────────────────────────────────────────────────────────┐
│ Application │
│ ↓ (Win32 API call) │
├─────────────────────────────────────────────────────────────┤
│ kernel32.dll / user32.dll / etc. │
│ ↓ (documented layer) │
├─────────────────────────────────────────────────────────────┤
│ ntdll.dll (Native API) │
│ ↓ (undocumented layer - syscall stubs) │
├─────────────────────────────────────────────────────────────┤
│ ═══════════ User Mode / Kernel Mode Boundary ══════════════│
├─────────────────────────────────────────────────────────────┤
│ ntoskrnl.exe (Windows Kernel) │
│ System Service Dispatcher │
│ ↓ │
│ System Service Routines (NtCreateFile, NtReadFile, ...) │
└─────────────────────────────────────────────────────────────┘
This layered design serves multiple purposes:
Unlike libc wrappers in Linux that are relatively stable, Windows system call numbers change between versions—sometimes even between service packs. Code that calls system calls directly will likely break on different Windows versions. This is intentional: Microsoft wants applications to use the documented Win32 API layer.
The Native API is the true interface between user mode and kernel mode in Windows. It's implemented in ntdll.dll, which is automatically loaded into every Windows process. While largely undocumented, understanding the Native API is essential for security research, malware analysis, and advanced Windows development.
Two Categories of ntdll.dll Functions:
Nt/Zw Functions: System call stubs that transition to kernel mode
NtCreateFile, NtReadFile, NtQuerySystemInformation, etc.Rtl Functions: Runtime library functions that execute entirely in user mode
RtlInitUnicodeString, RtlCopyMemory, RtlCompareUnicodeString, etc.123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
// Native API structures and functions// Note: These are typically NOT declared in standard headers // Native string type used by Native APItypedef struct _UNICODE_STRING { USHORT Length; // Current length in BYTES (not characters) USHORT MaximumLength; // Maximum length in BYTES PWSTR Buffer; // Pointer to wide-character string} UNICODE_STRING, *PUNICODE_STRING; // Object attributes for native object creationtypedef struct _OBJECT_ATTRIBUTES { ULONG Length; HANDLE RootDirectory; PUNICODE_STRING ObjectName; ULONG Attributes; PVOID SecurityDescriptor; PVOID SecurityQualityOfService;} OBJECT_ATTRIBUTES, *POBJECT_ATTRIBUTES; // I/O status block - returned by native I/O operationstypedef struct _IO_STATUS_BLOCK { union { NTSTATUS Status; // Final status PVOID Pointer; }; ULONG_PTR Information; // Bytes transferred or other info} IO_STATUS_BLOCK, *PIO_STATUS_BLOCK; // Native file creation functionNTSTATUS 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); // Example: Using Native API to create a fileNTSTATUS CreateFileNative(LPCWSTR filePath, PHANDLE pHandle) { UNICODE_STRING nativePath; OBJECT_ATTRIBUTES objAttr; IO_STATUS_BLOCK ioStatus; NTSTATUS status; // Native API requires NT path format: \??\C:\path\file.txt WCHAR ntPath[MAX_PATH + 4]; swprintf_s(ntPath, MAX_PATH + 4, L"\\??\\%s", filePath); // Initialize UNICODE_STRING - Native API doesn't use null-terminated strings RtlInitUnicodeString(&nativePath, ntPath); // Initialize object attributes InitializeObjectAttributes( &objAttr, &nativePath, OBJ_CASE_INSENSITIVE, // Case-insensitive path lookup NULL, // No root directory NULL // No security descriptor ); // Call the Native API function status = NtCreateFile( pHandle, GENERIC_READ | SYNCHRONIZE, &objAttr, &ioStatus, NULL, // No allocation size FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ, FILE_OPEN, // Equivalent to OPEN_EXISTING FILE_SYNCHRONOUS_IO_NONALERT, // Synchronous I/O NULL, // No extended attributes 0 ); return status;}Native API functions return NTSTATUS values, not Win32 error codes. NTSTATUS is a 32-bit value where the high bits indicate severity: 0x00000000 is success, 0x80000000 is informational/warning, 0xC0000000 is error. Use RtlNtStatusToDosError() to convert NTSTATUS to Win32 error codes.
How Win32 Functions Call Native API:
The relationship between Win32 and Native API is fundamental. Consider the journey of a CreateFile call:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
// What happens when you call CreateFileW(): HANDLE CreateFileW( LPCWSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile) { // 1. Validate parameters if (!lpFileName) { SetLastError(ERROR_INVALID_PARAMETER); return INVALID_HANDLE_VALUE; } // 2. Convert Win32 path to Native path format // "C:\file.txt" → "\??\C:\file.txt" UNICODE_STRING ntPath; RtlDosPathNameToNtPathName_U(lpFileName, &ntPath, ...); // 3. Convert Win32 flags to Native flags ACCESS_MASK ntAccess = MapWin32AccessToNtAccess(dwDesiredAccess); ULONG ntDisposition = MapCreationDisposition(dwCreationDisposition); ULONG ntOptions = MapFlagsToOptions(dwFlagsAndAttributes); // 4. Build OBJECT_ATTRIBUTES OBJECT_ATTRIBUTES objAttr; InitializeObjectAttributes(&objAttr, &ntPath, ...); // 5. Call Native API HANDLE hFile; IO_STATUS_BLOCK ioStatus; NTSTATUS status = NtCreateFile( &hFile, ntAccess, &objAttr, &ioStatus, NULL, dwFlagsAndAttributes & FILE_ATTRIBUTE_MASK, dwShareMode, ntDisposition, ntOptions, NULL, 0 ); // 6. Convert NTSTATUS to Win32 error and return if (!NT_SUCCESS(status)) { SetLastError(RtlNtStatusToDosError(status)); return INVALID_HANDLE_VALUE; } return hFile;}| Win32 Function | Native API Function | Notes |
|---|---|---|
| CreateFile | NtCreateFile | Full parameter translation required |
| ReadFile | NtReadFile | Relatively direct mapping |
| WriteFile | NtWriteFile | Relatively direct mapping |
| CloseHandle | NtClose | Direct wrapper |
| CreateProcess | NtCreateUserProcess | Complex: creates process + thread |
| VirtualAlloc | NtAllocateVirtualMemory | Similar parameters |
| OpenProcess | NtOpenProcess | Requires CLIENT_ID structure |
| RegOpenKeyEx | NtOpenKey | Registry functions use native directly |
Now let's examine the actual mechanism by which user-mode code transitions into the kernel. This varies by processor architecture, but the x64 mechanism is the most important for modern Windows.
x64 System Call Sequence:
On x64 processors, Windows uses the syscall instruction for user-to-kernel transitions. This is a fast, specialized instruction designed specifically for system calls:
12345678910111213141516171819202122232425
; Typical ntdll.dll syscall stub for x64 Windows 10/11; This is what NtCreateFile looks like in ntdll.dll: NtCreateFile: ; Move the system call number into EAX mov r10, rcx ; Save first parameter (rcx is clobbered by syscall) mov eax, 55h ; System call number for NtCreateFile ; (This number varies between Windows versions!) ; Test if running inside the Windows hypervisor test byte ptr ds:[7FFE0308h], 1 jne UseHypercall ; Use different path for Hyper-V ; Execute the system call syscall ; CPU transitions to kernel mode ; Jumps to LSTAR MSR (KiSystemCall64) ; Return - we're back in user mode ret UseHypercall: ; When running in Hyper-V, use INT 2E for system calls ; (This is a security feature called Virtual Secure Mode) int 2Eh retWhat Happens During the syscall Instruction:
The syscall instruction performs several critical operations atomically:
The IA32_LSTAR Model-Specific Register (MSR) contains the address of the kernel's system call entry point—KiSystemCall64 in ntoskrnl.exe.
Windows x64 syscalls use a modified calling convention: the first argument is in R10 (not RCX as in normal x64 ABI), because RCX is used by syscall to save the return address. Arguments 2-4 are in RDX, R8, R9, and additional arguments are on the stack.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
// What happens in the kernel when a syscall arrives// (Simplified view of KiSystemCall64 and KiSystemServiceStart) KiSystemCall64: ; We're now in kernel mode (Ring 0) ; SWAPGS: Exchange kernel and user GS base addresses ; GS points to KPCR (Kernel Processor Control Region) swapgs ; Switch to kernel stack ; User stack pointer is saved in TSS mov rsp, gs:[KPCR.CurrentThread.InitialStack] ; Build trap frame on kernel stack ; Save all user-mode state push user_ss push user_rsp push user_rflags push user_cs push user_rip ; return address from syscall push error_code ; ... push all general registers ; Validate system call number cmp eax, MAX_SYSCALL_NUMBER jae InvalidSyscall ; Look up system service routine in SSDT lea rbx, [KeServiceDescriptorTable] mov rdi, qword ptr [rbx + SSDT.ServiceTable] ; Get the routine address (relative offset in table) movsxd r10, dword ptr [rdi + rax*4] sar r10, 4 ; Remove argument count from low bits add r10, rdi ; Validate and copy arguments from user stack ; (Critical security boundary!) call CopyUserModeArguments ; Call the actual system service routine call r10 ; e.g., calls NtCreateFile in ntoskrnl.exe ; Restore user state and return ; ... restore registers from trap frame swapgs sysretq ; Return to user modeThe System Service Descriptor Table (SSDT):
The SSDT is the kernel data structure that maps system call numbers to their implementations:
KeServiceDescriptorTable:
ServiceTable: Pointer to array of function offsets
CounterTable: Pointer to usage counters (debug builds)
NumberOfServices: Number of system calls in this table
ArgumentTable: Pointer to argument byte counts
In modern Windows, there are actually two SSDTs:
The system call number's high bit determines which table to use:
Historically, security software would modify the SSDT to intercept system calls. Modern Windows prevents this via Kernel Patch Protection (PatchGuard) on x64 systems. Any attempt to modify the SSDT triggers a CRITICAL_STRUCTURE_CORRUPTION bugcheck.
A critical aspect of Windows system calls is that system call numbers are not stable across Windows versions. This is a deliberate design decision with significant implications.
Why System Call Numbers Change:
| Windows Version | Syscall Number (x64) | Notes |
|---|---|---|
| Windows XP SP2 | 0x0025 | x64 introduced |
| Windows Vista | 0x0025 | Same as XP |
| Windows 7 | 0x0042 | Significant renumbering |
| Windows 8 | 0x0053 | Another major change |
| Windows 8.1 | 0x0054 | Minor adjustment |
| Windows 10 1507 | 0x0055 | Minor adjustment |
| Windows 10 1903 | 0x0055 | Stable |
| Windows 11 21H2 | 0x0055 | Still 0x55 |
Researchers determine current syscall numbers by disassembling ntdll.dll on each Windows version. Several projects maintain databases of syscall numbers, including 'Windows X86-64 System Call Table' by j00ru. However, relying on these in production code is strongly discouraged.
Direct Syscall Techniques (and Why They're Used):
Despite Microsoft's discouragement, some scenarios call for direct syscalls:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
// Direct syscall example - FOR EDUCATIONAL PURPOSES ONLY// This code is fragile and will break across Windows versions #include <windows.h> // Syscall stub - must be in assembly// This is the same code pattern that ntdll.dll usesextern NTSTATUS DirectNtClose(HANDLE Handle); // In separate .asm file:// DirectNtClose:// mov r10, rcx// mov eax, 0Fh ; NtClose syscall number for Win10/11// syscall// ret // Finding syscall number at runtime (more portable approach)DWORD GetSyscallNumber(LPCSTR functionName) { // Get ntdll base address HMODULE hNtdll = GetModuleHandleA("ntdll.dll"); if (!hNtdll) return 0; // Get function address PBYTE pFunction = (PBYTE)GetProcAddress(hNtdll, functionName); if (!pFunction) return 0; // Parse the syscall stub // First instruction: mov r10, rcx = 4C 8B D1 // Second instruction: mov eax, <syscall#> = B8 XX XX XX XX if (pFunction[0] == 0x4C && pFunction[1] == 0x8B && pFunction[2] == 0xD1 && pFunction[3] == 0xB8) { // Extract the syscall number return *(DWORD*)(pFunction + 4); } // Stub pattern may be different (hooked or different Windows version) return 0;} void ExampleUsage() { DWORD syscallNumber = GetSyscallNumber("NtClose"); printf("NtClose syscall number: 0x%X\n", syscallNumber); // You could then use this number with a dynamic syscall stub // But again - this is fragile and not recommended for production}Modern EDR (Endpoint Detection and Response) solutions are aware of direct syscall techniques. They employ kernel-level hooks and ETW (Event Tracing for Windows) tracing that cannot be bypassed from user mode. Direct syscalls are not a reliable evasion technique against sophisticated security software.
The transition between user mode and kernel mode is one of the most security-critical operations in an operating system. Let's examine this process in detail.
Security Boundaries Crossed:
When a syscall occurs, multiple security boundaries are crossed:
The Transition Sequence (x64):
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
┌──────────────────────────────────────────────────────────────────┐│ USER MODE (Ring 3) ││ ││ 1. Application calls kernel32!CreateFileW() ││ - User-mode code, normal function call ││ ││ 2. kernel32!CreateFileW validates parameters ││ - Still user mode ││ - Converts path format, maps flags ││ ││ 3. kernel32!CreateFileW calls ntdll!NtCreateFile ││ - Still user mode ││ - Normal function call into ntdll.dll ││ ││ 4. ntdll!NtCreateFile syscall stub executes ││ - mov r10, rcx ; Save first parameter ││ - mov eax, 55h ; Load syscall number ││ - syscall ; ← TRANSITION POINT ││ │╠══════════════════════════════════════════════════════════════════╣│ CPU OPERATIONS ││ ││ The SYSCALL instruction atomically: ││ - Saves RIP to RCX (return address) ││ - Saves RFLAGS to R11 ││ - Loads CS from STAR MSR (kernel code segment) ││ - Loads SS implicitly ││ - Masks RFLAGS (clears IF, TF, etc.) ││ - Loads RIP from LSTAR MSR (KiSystemCall64) ││ - Privilege level changes to Ring 0 ││ │╠══════════════════════════════════════════════════════════════════╣│ KERNEL MODE (Ring 0) ││ ││ 5. KiSystemCall64 runs ││ - swapgs ; Switch to kernel GS ││ - Switch to kernel stack ││ - Build KTRAP_FRAME on stack ││ ││ 6. KiSystemServiceStart ││ - Validate syscall number ││ - Look up in SSDT ││ - Copy user-mode arguments ││ - Probe user-mode buffers for validity ││ ││ 7. NtCreateFile executes (in ntoskrnl.exe) ││ - Full kernel privileges ││ - Calls file system drivers ││ - Returns NTSTATUS ││ ││ 8. KiSystemServiceExit ││ - Places return value in RAX ││ - Restores user state from trap frame ││ - swapgs ; Switch to user GS ││ - sysretq ; Return to user mode ││ │╠══════════════════════════════════════════════════════════════════╣│ USER MODE (Ring 3 - after sysret) ││ ││ 9. ntdll!NtCreateFile returns ││ - NTSTATUS in RAX ││ ││ 10. kernel32!CreateFileW handles result ││ - Converts NTSTATUS to Win32 error ││ - Returns HANDLE or INVALID_HANDLE_VALUE │└──────────────────────────────────────────────────────────────────┘Performance Implications:
System call transitions are expensive due to:
Typical syscall overhead on modern x64:
Applications can minimize syscall overhead by: (1) Batching operations where possible, (2) Using memory-mapped files instead of read/write syscalls, (3) Using I/O completion ports for async I/O, (4) Using the vDSO-like mechanism in Windows for time queries (SharedUserData).
Understanding how Windows system calls differ from Unix/POSIX system calls illuminates design philosophy differences between the operating systems.
| Aspect | Windows | POSIX/Linux |
|---|---|---|
| Documentation | Undocumented (Native API) | Fully documented |
| Stability | Changes between versions | Stable across versions |
| Official API | Win32 (user-mode DLLs) | libc wrappers + direct calls |
| Syscall numbers | Version-dependent | Fixed (within architecture) |
| String handling | Unicode (UNICODE_STRING) | UTF-8 null-terminated |
| Error reporting | NTSTATUS (32-bit codes) | errno (integer values) |
| Object model | Handle-based kernel objects | File descriptor integers |
| Path format | NT paths (\??\C:\...) | POSIX paths (/home/...) |
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
// Equivalent operations: Windows vs POSIX // ==== File Open ====// POSIX (Linux):int fd = open("/path/to/file", O_RDONLY);if (fd < 0) { perror("open failed");} // Windows Win32:HANDLE hFile = CreateFileW(L"C:\\path\\to\\file", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);if (hFile == INVALID_HANDLE_VALUE) { // GetLastError() for error code} // Windows Native API (internal):UNICODE_STRING path;OBJECT_ATTRIBUTES objAttr;IO_STATUS_BLOCK ioStatus;HANDLE hFile;RtlInitUnicodeString(&path, L"\\??\\C:\\path\\to\\file");InitializeObjectAttributes(&objAttr, &path, OBJ_CASE_INSENSITIVE, NULL, NULL);NTSTATUS status = NtCreateFile(&hFile, GENERIC_READ, &objAttr, &ioStatus, ...); // ==== Process Creation ====// POSIX:pid_t pid = fork();if (pid == 0) { execve("/bin/program", argv, envp);} // Windows Win32:STARTUPINFO si = { sizeof(si) };PROCESS_INFORMATION pi;CreateProcessW(L"C:\\program.exe", NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi); // ==== Memory Allocation ====// POSIX:void* mem = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); // Windows:LPVOID mem = VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);Windows system calls represent the fundamental mechanism by which applications request kernel services, but unlike POSIX systems, this interface is deliberately abstracted away from developers.
What's Next:
With understanding of how Windows system calls work, we'll next examine the handle-based access model that Windows uses for all kernel objects. Handles are the tokens that represent kernel resources in user space, and understanding how they work is fundamental to Windows programming.
You now understand how Windows implements system calls, from the high-level Win32 API through ntdll.dll and into the kernel. This knowledge is essential for understanding Windows security, performance, and the architectural differences from Unix-like systems.