Adding a syscall to darwinOS touches three layers: the syscall table (where its number lives), the handler (what it does in the kernel), and libSystem (how userspace calls it). This guide walks all three for a trivial example: a hello_syscall(const char *name) that logs hello, <name> from kernel space.
It’s a toy — the point is the plumbing. Real syscalls look the same, they just do more work.
Prerequisites
- darwinOS built from source
- A way to debug or observe the kernel while you test
Step 1: Assign a syscall number
Edit xnu/bsd/kern/syscalls.master. Each line is <num> <abi> <ret> <name>(<args>); append yours at the end of the BSD namespace:
540 ALL STD int hello_syscall(const char *name);540is the next free number in the BSD syscalls range (check the file — don’t reuse).ALLmeans every architecture gets it.STD= standard syscall (no special treatment).
The build system regenerates syscall_numbers.h, syscall_dispatch.c, and the companion libSystem stubs from this file, so don’t edit those by hand.
Step 2: Write the handler
Handlers go in xnu/bsd/kern/. By convention, name the file after the syscall or its subsystem — kern_hello.c:
#include <sys/systm.h>
#include <sys/kauth.h>
#include <sys/sysproto.h>
struct hello_syscall_args {
const char *name;
};
int
hello_syscall(proc_t p, struct hello_syscall_args *uap, int *retval)
{
char buf[64];
size_t copied = 0;
int err = copyinstr((user_addr_t)uap->name, buf, sizeof(buf), &copied);
if (err != 0) {
return err;
}
printf("hello, %s (from PID %d)\n", buf, proc_pid(p));
*retval = 0;
return 0;
}Key points:
proc_t pis the calling process’s kernel handle.proc_pid(p)gives the PID.uapis the argument struct — same layout as insyscalls.master.- Never dereference userspace pointers directly. Use
copyin,copyout, orcopyinstrto marshal across the boundary. - Return
0on success, aerrno-style value on failure.
Add the new file to xnu/bsd/conf/MASTER or whatever your tree’s equivalent file-list is, then rebuild:
make xnuStep 3: Call it from userspace
Once the kernel is built, the libSystem stubs include _hello_syscall automatically. Headers are a separate step — add the prototype to libsystem/include/sys/hello.h:
#pragma once
int hello_syscall(const char *name);And from your userland program:
#include <sys/hello.h>
int main(void) {
hello_syscall("world");
return 0;
}Build, copy into the guest, run. The string you passed shows up on the kernel console.
Step 4: Testing
Unit tests for syscalls live under tests/syscalls/. Add hello.c:
#include <assert.h>
#include <sys/hello.h>
int main(void) {
assert(hello_syscall("test") == 0);
return 0;
}Register it in tests/syscalls/Makefile. Then:
make testNegative paths — what does your syscall do if the pointer is NULL? Misaligned? A kernel pointer? Tests for each.
Common pitfalls
“Unknown syscall” at runtime
The libSystem stub wasn’t rebuilt after you edited syscalls.master. make world (not just make xnu) regenerates both sides.
Kernel panic on invocation
Almost always a missed copyin. Trying to read userspace memory directly in a kernel context panics on arm64 (SMAP-equivalent) or returns stale/wrong data on x86_64.
Syscall number collision
Two PRs adding syscalls at the same number race at merge time. The number you chose became wrong the moment someone else’s PR landed. Re-run the build, let the codegen pick the next free number, and update your handler’s args accordingly.
Next steps
- Trace a syscall — observe your new syscall with DTrace once it’s working.
- Debug the kernel — set a breakpoint in your handler and inspect what userspace is sending.