Take a binary compiled on Linux and copy it to a Windows machine. Try to run it. Nothing happens — or worse, an unhelpful error. The source code might be identical. The logic is the same. The CPU understands the same instruction set. But the program refuses to run.
This is not a licensing issue or a deliberate restriction. It is a consequence of how programs talk to the operating system — and every operating system speaks a different dialect.
The Interface a Binary Depends On
When a program does anything real — reads a file, opens a network connection, allocates memory — it cannot do it alone. It asks the operating system through a system call: a controlled crossing from user space into the kernel, where the actual work happens.
That crossing has a protocol. The program places a number in a specific CPU register to identify which operation it wants. It places arguments in other registers. It executes a single instruction that hands control to the kernel. The kernel reads the registers, does the work, and returns.
A compiled binary has this protocol baked in. The system call numbers, the register layout, the instruction used — all of it is fixed at compile time, specific to the operating system and CPU architecture the binary was built for. When you move that binary to a different OS, the protocol no longer matches. The kernel on the other side does not recognise the request.
Linux vs Windows
Linux defines around 300 to 400 system calls depending on the architecture. On x86-64, write is system call number 1. read is 0. open is 2. These numbers are defined in the kernel source at arch/x86/entry/syscalls/syscall_64.tbl and are stable across kernel versions — software depends on them not changing.
Windows defines its own set, internally called NT system calls — NtReadFile, NtWriteFile, NtCreateProcess. The numbers are different, the names are different, and the design philosophy is different.
Linux follows the Unix model: almost everything is a file descriptor. A socket, a pipe, a timer — once you have one, you read and write it with the same read and write calls you use for files. The kernel handles the difference underneath.
Windows does not follow this model. It has separate handle types for files, sockets, threads, and registry keys, each with its own API family. WriteFile for files, send for sockets. Neither model is wrong — they reflect different design decisions made in the 1970s and 1980s that every subsequent version has had to maintain for compatibility.
The calling convention also differs. Both Linux and Windows on x86-64 use the syscall instruction at the hardware level, but the register assignments are different and the entry points are different. A Linux binary that executes syscall with rax=1 is asking Linux for write. The same instruction on Windows means something entirely different — or maps to nothing at all.
Linux vs macOS
macOS shares Unix heritage. It is built on XNU, a kernel combining Mach and parts of BSD. Many macOS system calls have the same names as Linux ones — open, read, write, fork all exist, and the semantics are often similar.
But the numbers are different — and the difference goes deeper than counting from a different starting point. macOS groups its system calls into classes using the upper bits of the syscall number. BSD calls, inherited from FreeBSD, use a class offset of 0x2000000. The BSD write call is number 4, so the actual value placed in rax on macOS is 0x2000004. Linux write is 1 with no class offset. A Linux binary placing 1 in rax on macOS hits a Mach trap, not a write operation — a completely different subsystem.
On Apple Silicon (ARM), the instruction for a system call is svc, not syscall. A Linux binary compiled for x86-64 would not even contain the right instruction, let alone the right number.
Why WSL Exists
Windows Subsystem for Linux is the direct answer to this incompatibility.
WSL 1 intercepted Linux system calls at runtime and translated them into Windows kernel calls. When a Linux binary executed syscall with rax=1, WSL caught it, translated it into NtWriteFile, and returned the result in the format Linux expected.
This translation layer proved too difficult to maintain accurately. Some Linux syscalls had no clean Windows equivalent, and edge cases accumulated. WSL 2 abandoned translation entirely. It runs a real Linux kernel inside a lightweight Hyper-V virtual machine. Linux binaries make real Linux syscalls to a real Linux kernel — no translation, near-perfect compatibility, because it is genuine.
What Cross-Platform Actually Means
When software is described as cross-platform, it almost always means the source code compiles on multiple operating systems — not that the same binary runs on all of them.
The same C source file that calls write() will compile and run on Linux, macOS, and Windows because the C library on each platform provides a write() function that calls the local kernel correctly. glibc on Linux, libSystem on macOS, the Windows CRT on Windows — each translates the same function call into the right syscall for its OS. The source is portable. The compiled output is not.
A distributed binary is tied to a specific OS and architecture. A Linux x86-64 binary contains Linux syscall numbers for x86-64. It will not run on macOS, Windows, or Linux on ARM without a translation layer, a compatibility shim, or a virtual machine running the right kernel underneath.
The abstraction that enables portability is not the syscall interface itself — it is the C library sitting above it, speaking the right dialect on each platform so the source code does not have to.
Verify It Yourself
The syscall table for your running Linux kernel is visible directly:
ausyscall --dump 2>/dev/null | head -20
If ausyscall is not available, the numbers are in the kernel headers directly:
grep "#define __NR_" /usr/include/asm/unistd_64.h | head -20
Each line maps a syscall name to its number — the integers a Linux binary on this machine uses when it crosses into the kernel.