Show off on Apple M1 with assembly

Sat, 8 Apr. 2023     Thomas Bendler     ~ 7 min to read

Recently, I was a little bit bored on a grey and rainy November weekend without MotoGP or the like on television. So, I zapped through tons of YouTube videos and, out of a sudden, I stumbled over a video of a nerdy guy with glasses at whatever university talking about assembly and how cool and useful it is. Instantly I started to swallow in memories when I was a kid at the age of 15 or so, doing my first steps in “something with a computer”. It was a time when no one cared about computers and when self-learning was the only way. Although it was a very small group, at least a few other guys were showing some interest in this new, futuristic thing. And what should I say, most of them looked like the nerds everyone was thinking of at this time. But there was one exception, a friend of mine, 2/ 3 years older, 1.90m, bodybuilder, professional swimmer and, what a shame, on top extremely smart. He was the first one with a 32-bit computer and he was a developer who programmed graphical stuff … in assembly. It was a mixture of being jealous and impressed at the same point in time but I had to admit, he was far better than me. I tried to understand what he was programming but I was simply failing. So, I focused on what I was good at and forgot about that mysterious assembly.

But as said in the introduction, I then saw the video of this nerdy guy and I thought, for god sake, why not? Second try so to speak, 35 years later. But better late than never. First I read through some tutorials and the first thing I recognized was, that assembly is so low-level, that it even depends on the processor. This already brought some challenges, I have a bunch of different machines at home, so what to use for my first steps? My decision in the end was my MacBook Air with the M1 CPU. The primary reason was that this is my development workstation and, as this exercise is only for educational reasons, I can compile and run my code directly on my laptop. And let’s be frank, how can you show off more than programming in assembly on a modern MacBook? How cool is this to cite the nerdy guy … smile

The first program

Ok, it’s time to start coding. As usual, let’s start with the classical “Hello World!” or in this case “Hello ARM64!”. Some remarks first, comments are written with two slashes at the beginning of a line. The program starting point, in this case _main needs to be defined in the linker section as well (see Makefile). So if you would like to derive from this naming convention, you need to adjust the Makefile as well. Having this mentioned, let’s have a look at the complete program first:

// HelloWorld.s
//
// Author:      Thomas Bendler <code@thbe.org>
// Date:        Sun Nov 12 12:08:01 CET 2023
//
// Description: Assembler program to print "Hello World!" to stdout
//
// Note:        X0-X2 - Parameters to UNIX system calls
//              X16   - Mach system call function number
//

// Provide program starting address to linker
.global _main
.align 2

// Setup the parameters to print hello world and execute it
_main:  mov X0,  #1          // Use 1 for StdOut
        adr X1,  msg         // Print Hello World string
        mov X2,  msg_len     // Length of Hello World string
        mov X16, #4          // MacOS write system call
        svc #0               // Call MacOS to output the string

// Setup the parameters to exit the program and execute it
        mov X0,  #0          // Use 0 for return code
        mov X16, #1          // MacOS terminate program
        svc #0               // Call MacOS to terminate the program

// Define the Hello World string and calculate length
msg:    .ascii  "Hello, ARM64!\n"
        msg_len = . - msg

As this code significantly differs from other programming languages like, for example, Python, let’s dig deeper into this example and analyze it step by step. The first directive “.global _main” defines the starting point of the assembly program. The second directive is an optimization directive. This is “.align X” where X is a number that must be a power of 2. For example 2, 4, 8, 16, and so on. The directive allows you to enforce alignment of the instruction or data immediately after the directive, on a memory address that is a multiple of the value X. The reason is that some instructions are simply executed faster if they are aligned on a byte boundary.

As said, the real program starts at “_main:”. First, we move the number 1 into the first parameter/ result register named X0. There are 8 of them in total (X0 till X7), but we only use X0, X1 and X2 for the example program. The 1 in the first register tells the machine to output the following registers. In the second register, the text string defined in msg is addressed whereas in the third register, the length of the text string is stored. The output text and the length of the test are defined in the section “msg:”. The first line is the text, the second line is the calculation of the text length. The last register that is used, is X16. This is a temporary intra-procedure-call register which contains the action that should be executed. The number 16 that is written to the X16 register means, to write a system call to stdout as defined in X0. With the information in the four registers, the registers are then executed with the “svc” command.

The second block terminates the program. First, the return code of the program after termination is written into X0. The 1 written to X16 is the number that terminates the program. As already seen in block one, the “svc” command executes the program block.

So, that’s effectively it. This is the program that outputs “Hello, ARM64!” using assembly and the way, you can show off … smile

The build

Although one could enter the compiler and linker commands manually every time, it’s much more handy to create a Makefile that does the required steps for you. To build the HelloWorld example in the chapter above, the following Makefile do the trick:

# Makefile
#
# Author:      Thomas Bendler <code@thbe.org>
# Date:        Sun Nov 12 12:08:01 CET 2023
#
# Description: GNU Makefile that compiles and link the HelloWorld Assembler program
#
# Note:        You need to use [tab], space is not allowed
#

HelloWorld: HelloWorld.o
	ld -o HelloWorld HelloWorld.o \
		-macos_version_min 14.0.0 \
		-lSystem \
		-syslibroot `xcrun -sdk macosx --show-sdk-path` \
		-e _main \
		-arch arm64

HelloWorld.o: HelloWorld.s
	as -o HelloWorld.o HelloWorld.s

Remark: Within the Makefile, it is necessary to use tabs for any kind of indention. Using spaces will cause “make” to fail.

To use the Makefile, the make command is required like this:

make -B


Share on: