Skip to content
Tech News
← Back to articles

Writing Portable ARM64 Assembly (2023)

read original get ARM64 Assembly Programming Book → more articles
Why This Matters

As ARM-based computers like Apple's Macs become more prevalent, developers are increasingly writing assembly code optimized for these devices. However, many are neglecting the portability of their code across different ARM64 platforms, which can limit its usability on servers, SBCs, and other Linux-based systems. This article emphasizes the importance of understanding ABI differences and syntax nuances to create truly portable assembly code that works seamlessly across diverse ARM64 environments.

Key Takeaways

An unfortunate side effect of the rising popularity of Apple’s ARM-based computers is an increase in unportable assembly code which targets the 64-bit ARM ISA. This is because developers are writing these bits of assembly code to speed up their programs when run on Apple’s ARM-based computers, without considering the other 64-bit ARM devices out there, such as SBCs and servers running Linux or BSD.

The good news is that it is very easy to write assembly which targets Apple’s computers as well as the other 64-bit ARM devices running operating systems other than Darwin. It just requires being aware of a few differences between the Mach-O and ELF ABIs, as well as knowing what Apple-specific syntax extensions to avoid. By following the guidance in this blog, you will be able to write assembly code which is portable between Apple’s toolchain, the official ARM assembly toolchain, and the GNU toolchain.

Differences between the ELF and Mach-O ABIs

Modern UNIX systems, including Linux-based systems largely use the ELF binary format. Apple uses Mach-O in Darwin instead for historical reasons. This is not a requirement for Apple imposed by their use of Mach, indeed, OSFMK, the kernel that Darwin, MkLinux and OSF/1 are all based on, supports ELF binaries just fine. Apple just decided to use the Mach-O format instead.

When it comes to writing assembly (or, really, just linking code in general) targeting Darwin, the main difference to be aware of is that all symbols are prefixed with a single underscore. For example, if you have a function that would be declared in C like:

extern void unmask ( const char * payload, const char * mask, size_t len);

On Darwin, the function in your assembly code must be defined as _unmask .

The other major difference is that ELF defines different classes of data, for example STT_FUNC and STT_OBJECT . There is no equivalence in Mach-O, and thus the .type directive that you would use when writing assembly for ELF targets is not supported.

A brief note on Platform ABIs

You will also need to be aware of minor differences between the Darwin ABI and other platform ABIs. A notable example is that the x18 register is reserved by the Darwin ABI and is explicitly zeroed on context switches in some cases. This register is also reserved on Android, but not on GNU/Linux or Alpine.

... continue reading