ARM64 development on x86_64
April 10, 2025
I would like to add an ARM64 backend to my compiler project, but a prerequisite for that is learning ARM64 assembly sufficiently well that I can get something basic stood up.
My personal machines are all x86_64
(and some old 32-bit ARM Raspberry Pis), and while I’ll want to test on a real ARM machine reasonably often, for everyday development work I’d prefer something I can quickly iterate with.
Furthermore, even though I would like my compiler to generate ARM64 directly, and not need a seperate toolchain, I still need (for now) a linker, and an assembler and C compiler to test and experiment with.
This post aggregates information from a couple of other tutorials and sites to bootstrap an ARM64 linux development environment on Debian (12, to be specific), on a x86_64 host. It allows ARM64 binaries to be built, run, and debugged from a single x86_64 machine.
What isn’t covered here is getting userspace libraries (to link and load) setup.
Debian multiarch addresses this, or alternatively a chroot can be setup with an ARM64 userspace (using e.g. debootstrap
).
Perhaps I’ll document one of these in a later post.
Toolchain
Installing assemblers a cross compilers, and binary utilities
$ sudo apt install gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu binutils-aarch64-linux-gnu-dbg
This installs the GNU toolchain with prefix aarch64-linux-gnu
, e.g. aarch64-linux-gnu-as
is the ARM64 assembler.
Userspace emulation with qemu
QEMU can emulate userspace binaries, intercepting system calls and mapping them to the host.
Using binfmt
, a feature of the linux kernel that recognizes signatures of a binary and figures out how to run them (wonder how WSL launches Windows binaries?), this allows an x86_64 linux host to run ARM64 linux binaries.
$ sudo apt install qemu-user-static
There is also a non-statically linked variant qemu-user
.
If you do install qemu-user
, you might want to add qemu-user-binfmt
to register the binfmt
handlers, something that is done automatically with qemu-user-static
.
qemu-user-static
historically worked better with chroot
environments where it could be explicitly added to the chroot
filesystem.
On newer versions of Debian, the binary on the host is used, and any dependent libraries can be loaded as normal, so there is no need to add the binary to the chroot.
Which you prefer to use is therefore now just a matter of preference and use case.
The commands that follow might need to be tweaked slightly if you use the dynamically linked version – in particular the -static
suffix on some commands will need removing.
Debugger
For now on linux gdb
is the most useful debugger, which does at least have a multi-arch version
$ sudo apt install gdb-multiarch
Perhaps sometime soon we’ll have something much better.
Hello, world
Let’s assemble and link the hello world of ARM64 binaries.
I typed this out from a tutorial, just to get started.
Put this in arm64hello.s
.
.section .text
.global _start
_start:
mov x0, #1
adr x1, msg
mov x2, len
mov w8, #64 /* syscall 64 is write */
svc #0
mov x0, #0
mov w8, #93 /* syscall 93 is exit */
svc #0
msg:
.asciz "Hello, world\n"
len = . - msg
Build with
$ aarch64-linux-gnu-as -o arm64hello.o arm64hello.s
$ aarch64-linux-gnu-ld -o arm64hello arm64hello.o
By virtue of binfmt
, this can then be run with
$ ./arm64hello
where, behind the scenes, qemu
does the translation work.
You can verify this is a real ARM64 binary with file
.
Debugging
It’s useful to be able to step through code with a debugger.
gdb
’s remote debugging facilities in combination with qemu
enable this.
$ qemu-aarch64-static -g PORTNUMBER ./arm64hello
starts the binary we built in qemu
, and pauses, waiting for gdb (or something using the gdb protocol) to attach.
$ gdb-multiarch
starts the debugger, and then we can connect with
(gdb) target remote localhost:PORTNUMBER
and start stepping through the program.
References
This condenses some instructions from this tutorial and this post.
The Debian page on QEMU user emulation is also useful.