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.
![]()
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.