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
.