Build and run

Your first darwinOS program

Cross-compile a "hello, world" binary on your host, copy it into a running VM, and run it under the scheduler you just built.

Now that you have darwinOS built and running in a VM, the smallest meaningful thing you can do is run a userland program against it. This guide walks through a hello-world in C, links it against the darwinOS SDK, gets it into the guest, and runs it.

The point isn’t the program itself — it’s to confirm your build, your SDK, and your ABI are all aligned.

Write the program

Anywhere on your host, create hello.c:

#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    printf("Hello from PID %d on darwinOS.\n", getpid());
    return 0;
}

Nothing Darwin-specific — it’s plain POSIX. The ABI you link against does all the work.

Cross-compile against the SDK

The darwinOS build produces an SDK tarball alongside the rootfs:

build/arm64-debug/sdk.tar.gz

Extract it once:

mkdir -p ~/.darwinos-sdk
tar -xzf build/arm64-debug/sdk.tar.gz -C ~/.darwinos-sdk

Then compile with the bundled wrapper:

~/.darwinos-sdk/bin/darwinos-clang \
  --target=arm64-darwin \
  -o hello \
  hello.c

The wrapper sets --sysroot, -isysroot, and the library search paths so your binary links against the darwinOS libSystem — not your host’s libc.

Confirm the result:

file hello
# hello: Mach-O 64-bit executable arm64

Copy the binary into the guest

The simplest path is to mount the guest’s root filesystem on the host and drop the binary in:

sudo mount -o loop build/arm64-debug/rootfs.img /mnt/darwinos
sudo cp hello /mnt/darwinos/usr/local/bin/
sudo umount /mnt/darwinos

Then boot the VM as usual. For repeated iteration, this gets tedious — see Next steps for faster options.

Run it

Inside the guest:

hello
# Hello from PID 47 on darwinOS.

If you see that line, you have a working toolchain, a working libSystem, a working dyld, and a working launchd + shell chain. Everything else is an extension of this.

What just happened

In sequence:

  1. darwinos-clang produced a Mach-O binary with a LC_LOAD_DYLIB entry for /usr/lib/libSystem.B.dylib
  2. The shell forked a child process and invoked the kernel’s execve implementation
  3. The kernel loaded the Mach-O, mapped dyld into the child, and jumped to dyld’s entry point
  4. dyld resolved libSystem and wired up printf, getpid, and friends via lazy binding
  5. dyld jumped to main, which syscalled into the BSD personality for the write that printf eventually issued

Next steps

  • For faster iteration, set up a shared 9p filesystem between host and guest so you can recompile and run without copying:

    -virtfs local,path=./share,mount_tag=host0,security_model=passthrough,id=host0

    Mount it in the guest with mount -t 9p host0 /mnt.

  • Debug the kernel — attach lldb when something goes wrong.

  • Write a kext — the kernel-space equivalent of this guide.