Running ARM Code on X86 Linux


Categories: IoT Tags: linux arm systemd

Recently I was engaged in a project that penetrated a niche which we use to call “IoT” these days. Or, in simpler terms, there was some software running on low-power devices, collecting sensor data and sending JSONs to the cloud. I was responsible for packaging the device software. Cross-compiling was not an issue thanks to the great tooling on Debian (which was a target platform), but I had to be sure that the package installation, removal and upgrading works smoothly. Part of the task was to test that systemd-managed services start and stop as expected.

As you may have guessed, those low-power devices were not x86-based. They were the boxes running full Debian distro on ARM CPUs. Debian calls this port “armhf” (for “ARM hardfloat”, which simply means that when gcc compiles your code, the resulting binary will use a hardware floating point unit).

So my jenkins pipeline was building and packaging the software, but I had no idea how to make sure that this software will actually work as intended when delivered to the wild world outside. Unfortunately, access to the very ARM boxes was quite limited: we had one sample on-site, in use almost all the time by our embedded developer.

Of course, the solution was to use an emulator. I needed something that will execute those ARM binaries on my x86 laptop.

To illustrate the approach I took, let me first step back and show you an old quiz for system administrators.

Imagine someone accidentally removed all the execution bits from the chmod utility on your system. How would you make your /bin/chmod functional again?

Being a good quiz, it has several solutions. Let me demonstrate one of them – the most relevant to the topic of this article:

As you can see, there is a special kind of interpreter that can execute binaries.

In fact, both a script with a shebang and a compiled binary use the same kernel mechanics to run. Linux reads file’s “header” to determine its format and then calls a corresponding interpreter. There are several binary formats supported out of the box:

(ignis@ignis-pc|linux-4.19.2) find ./ -name 'binfmt*.c'

Notice binfmt_elf.c (ELF is a binary format used on linux) and binfmt_script.c.

But the most exciting thing here is the binfmt_misc. Think of it as of a plug-in system for binary formats.

With binfmt_misc it is possible to do


and have dosbox interpreting the exe-file.

In a similar fashion we can call a special interpreter for ARM binaries.

Qemu is a very powerful and feature-rich opensource software that emulates a ton of different CPU architectures using dynamic binary translation (this is what Java VM also does). Among those supported architectures is, of course, ARM as well. So in order to build a playground for ARM software on my x86 linux, I had to marry qemu with binfmt_misc magic. The good thing is: this is already a solved problem.

In my case (I use Manjaro distro) this is how I installed the neccessary packages:

yaourt -S qemu-arm-static binfmt-qemu-static

qemu-arm-static is, as the name suggests, a statically compiled build of the QEmu emulator for ARM. The other package simply configures binfmt_misc to use QEmu for ARM binaries.

All the funny binary formats supported by the binfmt_misc on your system can be listed like so:

(ignis@ignis-pc|~) ls -l /proc/sys/fs/binfmt_misc/
total 0
-rw-r--r-- 1 root root 0 30. Nov 14:58 aarch64
-rw-r--r-- 1 root root 0 30. Nov 14:58 appimage-type1
-rw-r--r-- 1 root root 0 30. Nov 14:58 appimage-type2
-rw-r--r-- 1 root root 0 30. Nov 14:58 arm
-rw-r--r-- 1 root root 0 30. Nov 14:58 armeb
-rw-r--r-- 1 root root 0 30. Nov 14:58 CLR
-rw-r--r-- 1 root root 0 30. Nov 14:58 qemu-aarch64
-rw-r--r-- 1 root root 0 30. Nov 14:58 qemu-alpha
-rw-r--r-- 1 root root 0 30. Nov 14:58 qemu-arm
-rw-r--r-- 1 root root 0 30. Nov 14:58 qemu-armeb
-rw-r--r-- 1 root root 0 30. Nov 14:58 qemu-cris
-rw-r--r-- 1 root root 0 30. Nov 14:58 qemu-m68k
-rw-r--r-- 1 root root 0 30. Nov 14:58 qemu-microblaze
-rw-r--r-- 1 root root 0 30. Nov 14:58 qemu-mips
-rw-r--r-- 1 root root 0 30. Nov 14:58 qemu-mipsel
-rw-r--r-- 1 root root 0 30. Nov 14:58 qemu-ppc
-rw-r--r-- 1 root root 0 30. Nov 14:58 qemu-ppc64
-rw-r--r-- 1 root root 0 30. Nov 14:58 qemu-ppc64abi32
-rw-r--r-- 1 root root 0 30. Nov 14:58 qemu-s390x
-rw-r--r-- 1 root root 0 30. Nov 14:58 qemu-sh4
-rw-r--r-- 1 root root 0 30. Nov 14:58 qemu-sh4eb
-rw-r--r-- 1 root root 0 30. Nov 14:58 qemu-sparc
-rw-r--r-- 1 root root 0 30. Nov 14:58 qemu-sparc32plus
-rw-r--r-- 1 root root 0 30. Nov 14:58 qemu-sparc64
--w------- 1 root root 0 30. Nov 14:58 register
-rw-r--r-- 1 root root 0 30. Nov 14:58 status

The files here are the actual configuration interface for binfmt_misc.

(ignis@ignis-pc|~) cat /proc/sys/fs/binfmt_misc/qemu-arm
interpreter /usr/bin/qemu-arm-static
offset 0
magic 7f454c4601010100000000000000000002002800
mask ffffffffffffff00fffffffffffffffffeffffff
(ignis@ignis-pc|~) file /usr/bin/qemu-arm-static 
/usr/bin/qemu-arm-static: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, stripped

This setup is already enough to run a statically-linked ARM binary. It’s easy to prove with this tiny go program:

package main

import (

func main() {
	fmt.Printf("Hello from %s on %s!\n", runtime.GOARCH, runtime.GOOS)

Let’s cross-compile this piece of code and try to run it:

(ignis@ignis-pc|running-arm-code-on-x86) GOOS=linux GOARCH=arm go build hello_arm.go 
(ignis@ignis-pc|running-arm-code-on-x86) file hello_arm
arm: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, Go BuildID=EU8gFAuRaqyiFNHS24Mv/uyvsrKy_JSIa89DepWmG/fOI57lcKmfyalb7oGKtG/ynlSiaZtt4pSVD6q-QcO, with debug_info, not stripped
(ignis@ignis-pc|running-arm-code-on-x86) ./hello_arm 
Hello from arm on linux!

Heck, that was easy! But for a full experience (e.g. when we need shared libraries like most of the software does), we need a linux distro built for ARM.

You can use docker for that but in my case I needed more of a VM-experience to test how systemd services for the ARM software behave, so I opted for systemd-nspawn.

systemd-spawn is a true gem (and you don’t even have to install anything if your distro is systemd-based). systemd-spawn is extremely useful for local development and testing. Unlike docker, it doesn’t need any special images, it is more like chroot: we can just install debian in any directory and run from it.

In this example the directory in which we install debian distribution is called debian_jessie.

yaourt -S debootstrap
sudo debootstrap --foreign --arch=armhf jessie debian_jessie
sudo mount -t proc /proc debian_jessie/proc
sudo cp /usr/bin/qemu-arm-static debian_jessie/usr/bin/
sudo cp /etc/resolv.conf debian_jessie/etc
sudo PATH=/bin:/sbin:/usr/bin:/usr/sbin chroot debian_jessie /bin/bash

Once you’re in a chroot environment, finish the installation:

/debootstrap/debootstrap --second-stage

As this command concludes, it is time to set the root password:

sudo systemd-nspawn -D debian_jessie

At this point debian_jessie directory contains a fully workable debian system.

When I was testing new builds of our IoT software, I used to copy this initial installation into a separate directory to have a clean environment

sudo cp -r debian_jessie debian

and then dropped the files I needed inside

sudo cp ~/builds/*.deb ./debian/var/tmp

Starting an armhf port of debian jessie was then just a matter of

sudo systemd-nspawn -D debian /sbin/init

After a normal systemd boot process and a terminal login I had a fresh linux system ready for play.

It is just amazing how flexible linux as an ecosystem is and how much fun you can have by digging deeper in it. I hope you had fun too reading this article. Enjoy your day!

For some more interesting use cases for systemd-nspawn, check out this article