HelloSilicon
An introduction to ARM64 assembly on Apple Silicon Macs
Install / Use
/learn @below/HelloSiliconREADME
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 runxcode-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:
-lSystemtells the linker to link our executable withlibSystem.dylib. We do that to add theLC_MAINload command to the executable. Generally, Darwin does not support statically linked executables. It is possible, if not especially elegant to create executables without usinglibSystem.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 replacedLC_UNIXTHREADas of MacOS X 10.7.-sysroot: In order to findlibSystem.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 usexcrun -sdk macosx --show-sdk-pathto 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_startand tell the linker that this is the entry point we want to use-arch arm64for 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
