TL;DR

Recently, I start learning NASM and following this tutorial. Even though this is an example for Linux, I plan to run this on my Mac.

          global    _start

          section   .text
_start:   mov       rax, 1                  ; system call for write
          mov       rdi, 1                  ; file handle 1 is stdout
          mov       rsi, message            ; address of string to output
          mov       rdx, 13                 ; number of bytes
          syscall                           ; invoke operating system to do the write
          mov       rax, 60                 ; system call for exit
          xor       rdi, rdi                ; exit code 0
          syscall                           ; invoke operating system to exit

          section   .data
message:  db        "Hello, World", 10      ; note the newline at the end

And compile everything on mac,

$ nasm -f macho64 -o hello.o hello_64.asm
$ ld -v -macosx_version_min 10.13 -e _start -static hello.o
$ ./a.out
[1]    48225 segmentation fault  ./hello

magic number 0x2000000

why segfault? It turns out I’m using Linux syscall table here. 1 is referring Linux system call write but not exactly on macOS, where syscall number 1 means exit in [Mac syscall table][mac-syscall-table].

From the XNU kernel source in osfmk/mach/i386/syscall_sw.h. Searching SYSCALL_CLASS_SHIFT:

/*
 * Syscall classes for 64-bit system call entry.
 * For 64-bit users, the 32-bit syscall number is partitioned
 * with the high-order bits representing the class and low-order
 * bits being the syscall number within that class.
 * The high-order 32-bits of the 64-bit syscall number are unused.
 * All system classes enter the kernel via the syscall instruction.

In XNU kernel, syscall is partitioned:

#define SYSCALL_CLASS_NONE  0   /* Invalid */
#define SYSCALL_CLASS_MACH  1   /* Mach */
#define SYSCALL_CLASS_UNIX  2   /* Unix/BSD */
#define SYSCALL_CLASS_MDEP  3   /* Machine-dependent */
#define SYSCALL_CLASS_DIAG  4   /* Diagnostics */

The partition tag for BSD syscall is 2. That’s how magic number 0x2000000 is constructed.

// 2 << 24 + syscall number
#define SYSCALL_CONSTRUCT_UNIX(syscall_number) \
            ((SYSCALL_CLASS_UNIX << SYSCALL_CLASS_SHIFT) | \
             (SYSCALL_NUMBER_MASK & (syscall_number)))

Why it uses BSD tag in the end instead of mach? The reason is Apple switches from mach kernel to BSD kernel. Check XNU for more information.

XNU

Putting together

Change the Linux syscall number to Mac’s,

          global    _start

section   .text

_start:   mov       rax, 0x2000004          ; system call for write
          mov       rdi, 1                  ; file handle 1 is stdout
          mov       rsi, msg                ; address the string message
          mov       rdx, msg.len            ; number of bytes
          syscall                           ; invoke operating system to do the write

          mov       rax, 0x2000001          ; system call for exit
          mov       rdi, 0                  ; exit code 0
          syscall                           ; invoke operating system to exit

section   .data

 msg:     db        "Hello, World!", 10      ; note the newline at the end
.len:     equ        $ - msg

Compile and run:

$ nasm -f macho64 -o hello.o hello_64.asm
$ ld -v -macosx_version_min 10.13 -e _start -static hello.o
$ ./a.out
Hello, World!

Instead of fixing ld command by adding various flags in Mac if it doesn’t work for you, use below,

cc -e _start -Wl,-no_pie hello.o -o hello

cc will figure out the right flags for ld.

_start can be replaced to _main so that you don’t need to specify the entry point for the executable. One more thing, I made another mistake by setting the entry point to start, instead of _start, shown above. For mac the entry point is start which will call the entry point. Thus, you cannot override start.