SkillAgentSearch skills...

HelloSilicon

An introduction to ARM64 assembly on Apple Silicon Macs

Install / Use

/learn @below/HelloSilicon

README

HelloSilicon

An introduction to assembly on Apple silicon Macs.

Introduction

In this repository, I will code along with the book Programming with 64-Bit ARM Assembly Language, adjusting all sample code for Apple's ARM64 line of computers. While Apple's marketing material seems to avoid a name for the platform and talks only about the M1 processor, the developer documentation uses the term "Apple silicon". I will use this term in the following.

The original source code can be found here.

Prerequisites

While I pretty much assume that people who made it here meet most if not all required prerequisites, it doesn't hurt to list them.

  • You need Xcode 12.2 or later, and to make things easier, the command line tools should be installed. This ensures that the tools are found in default locations (namely /usr/bin). If you are not sure that the tools are installed, check Preferences → Locations in Xcode or run xcode-select --install.

  • All application samples also require at least macOS Big Sur, iOS 14 or their respective watchOS or tvOS equivalents. Especially for the later three systems it is not a necessity per-se (neither is Xcode 12.2), but it makes things a lot simpler.

  • Finally, while all samples can be adjusted to work on the iPhone and all other of Apple's ARM64 devices, for best results you should have access to an Apple silicon Mac.

Acknowledgments

I would like to thank @claui, @jannau, @jrosengarden, @m-schmidt, @saagarjha, and @zhuowei! They helped me when I hit a wall, or asked questions that let me improve the content.

Changes To The Book

With the exception of the existing iOS samples, the book is based on the Linux operating system. Apple's operating systems (macOS, iOS, watchOS and tvOS) are actually just flavors of the Darwin operating system, so they share a set of common core components.

Linux and Darwin, which were both inspired by AT&T Unix System V, are significantly different at the level we are looking at. For the listings in the book, this mostly concerns system calls (i.e. when we want the Kernel to do someting for us), and the way Darwin accesses memory.

This file is organized so that you can read the book, and read about the differences for Apple silicon side by side. The headlines in this document follow those in the book.

Chapter 1: Getting Started

Computers and Numbers

The default Calculator.app on macOS has a "Programmer Mode", too. You enable it with View → Programmer (⌘3).

CPU Registers

Apple has made certain platform specific choices for the registers:

  • Apple reserves X18 for its own use. Do not use this register.
  • The frame pointer register (FP, X29) must always address a valid frame record.

About the GCC Assembler

The book uses Linux GNU tools, such as the GNU as assembler. While there is an as command on macOS, it will invoke the integrated LLVM Clang assembler by default. And even if there is the -Q option to use the GNU based assembler, this was only ever an option for x86_64 — and is already deprecated as of this writing.

% as -Q -arch arm64
/usr/bin/as: can't specifiy -Q with -arch arm64

Thus, the GNU assembler syntax is not an option, and the code will have to be adjusted for the Clang assembler syntax.

Likewise, while there is a gcc command on macOS, this simply calls the Clang C-compiler. For transparancy, all calls to gcc will be replaced with clang.

% gcc --version
Configured with: --prefix=/Applications/Xcode-beta.app/Contents/Developer/usr --with-gxx-include-dir=/Applications/Xcode-beta.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/4.2.1
Apple clang version 12.0.0 (clang-1200.0.32.27)
Target: arm64-apple-darwin20.1.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin

Hello World

If you are reading this, I assume you already knew that the macOS Terminal can be found in Applications → Utilities → Terminal.app. But if you didn't I feel honored to tell you and I wish you lots of fun on this journey! Don't be afraid to ask questions.

To make "Hello World" run on Apple silicon, first the changes from page 78 (Chapter 3) have to be applied to account for the differences between Darwin and the Linux kernel. To silence the warning, I insert .align 4 (or .p2align 2), because Darwin likes things to be aligned on even boundaries. The books mentions this in Aligning Data in Chapter 5, page 114.

System calls in Linux and macOS have several differences due to the unique conventions of each system. Here are some key distinctions:

  • Function Number: The function numbers differ between the two systems, with Linux using 64 and macOS using 4. The table for Darwin (Apple) system calls can be found at this link: Darwin System Calls.

[!CAUTION] Darwin function numbers are considered private by Apple, and are subject to change. They are provided here for educational purposes only

  • Address for Storing Function Numbers: The address used for storing function numbers also varies. In Linux, it’s on X8, while in macOS, it’s on X16.
  • Interruption Call: The call for interruption is 0 in Linux, whereas it’s 0x80 on Apple Silicon.

To make the linker work, a little more is needed, most of it should look familiar to Mac/iOS developers. These changes need to be applied to the makefile and to the build file. The complete call to the linker looks like this:

ld -o HelloWorld HelloWorld.o \
	-lSystem \
	-syslibroot `xcrun -sdk macosx --show-sdk-path` \
	-e _start \
	-arch arm64

We know the -o switch, let's examine the others:

  • -lSystem tells the linker to link our executable with libSystem.dylib. We do that to add the LC_MAIN load command to the executable. Generally, Darwin does not support statically linked executables. It is possible, if not especially elegant to create executables without using libSystem.dylib. I will go deeper into that topic when time permits. For people who read Mac OS X Internals I will just add that this replaced LC_UNIXTHREAD as of MacOS X 10.7.
  • -sysroot: In order to find libSystem.dylib, it is mandatory to tell our linker where to find it. It seems this was not necessary on macOS 10.15 because "New in macOS Big Sur 11 beta, the system ships with a built-in dynamic linker cache of all system-provided libraries. As part of this change, copies of dynamic libraries are no longer present on the filesystem.". We use xcrun -sdk macosx --show-sdk-path to dynamically use the currently active version of Xcode.
  • -e _start: Darwin expects an entrypoint _main. In order to keep the sample both as close as possible to the book, and to allow it's use within the C-Sample from Chapter 3, I opted to keep _start and tell the linker that this is the entry point we want to use
  • -arch arm64 for good measure, let's throw in the option to cross-compile this from an Intel Mac. You can leave this off when running on Apple silicon.

Reverse Engineering Our Program

While the objdump command line program works just as well on Darwin and produces the expected output, also try the --macho (or -m) option, which causes objdump to use the Mach-O specific object file parser.

Chapter 2: Loading and Adding

The changes from Chapter 1 (makefile, alignment, system calls) have to be applied.

Register and Shift

The gcc assembler accepts MOV X1, X2, LSL #1, which is not defined by the ARM Compiler User Guide. Instead, LSL X1, X2, #1 (etc) is used.

Register and Extension

Clang requires the source register to be 32-bit. This makes sense because with these extensions, the upper 32 Bit of a 64-bit register will never be touched:

ADD X2, X1, W0, SXTB

The GNU Assembler seems to ignore this and allows you to specifiy a 64-bit source register.

Chapter 3: Tooling Up

Beginning GDB

On macOS, gdb has been replaced with the LLDB Debugger lldb of the LLVM project. The syntax is not always the same as for gdb, so I will note the differences here.

To start debugging our movexamps program, enter the command

lldb movexamps

This yields the abbreviated output:

(lldb) target create "movexamps"
Current executable set to 'movexamps' (arm64).
(lldb)

Commands like run or list work just the same, and there is a nice GDB to LLDB command map.

To disassemble our program, a slightly different syntax is used for lldb:

disassemble --name start

Note that because we are linking a dynamic executable, the listing will be long and include other start functions. Our code will be listed under the line movexamps`start.

Likewise, lldb wants the breakpoint name without the underscore: b start

To get the registers on lldb, we us

View on GitHub
GitHub Stars4.9k
CategoryDevelopment
Updated10h ago
Forks325

Languages

Assembly

Security Score

100/100

Audited on Mar 21, 2026

No findings