'Hello World!' in ARM assembly

03 Feb 2019

Categories: Android NDK Tags: Android ARM Assembly NDK


(Originally written 2012-01-14; updated 2019-02-03)

Over the last few weeks, in an effort to port a small C library to the platform, I’ve been doing a fair bit of tinkering around with the Android NDK. The NDK is primarily intended to allow Android developers to write performance-critical portions of their apps in native C or C++, which interface with the Android Java API through JNI. As the C library in question required porting some x86 SIMD assembly, I figured it would be helpful for me to get to know the bare bones of the ARM architecture. As a means to this end, we can use the NDK’s cross-compiler as a standalone tool to write a simple ‘Hello World!’ console app in ARM assembly. As Android is effectively Linux under the hood, we can apply our Linux assembly programming skills to this task.

Update: this guide also works if you are targetting an ARM device running Linux, such as a Raspberry Pi.

The structure of a minimal ‘Hello World!’

To make our app as simple as possible, we will (perhaps counter-intuitively) forgo use of the C standard library in favour of a lower-level interface to the Linux kernel: system calls (syscalls). Our syscall ‘Hello, World!’ app in C looks like:

#include <unistd.h>

void main() {
  const char msg[] = "Hello, ARM!\n";
  write(0, msg, sizeof(msg));
  exit(0);
}

Note the call to exit: in C, this normally happens behind the scenes when control returns from main. We get no such convenience in assembly: we must explicitly tell the kernel when to terminate our app.

Invoking syscalls

At a high level, our app needs to do the following in order to invoke a syscall:

  1. Load syscall arguments into registers.
  2. Tell the kernel which syscall to invoke.
  3. Pass control to the kernel.

The ABI (application binary interface) is the protocol that we follow to achieve these tasks. The ARM ‘EABI’ calling convention is described vaguely in these patch notes. We can glean that each system call has a unique identifier that is passed in register R7, arguments are passed in R0-R6 (respecting “EABI arrangement” where appropriate, i.e. 64-bit arguments), and control is passed to the kernel with the SWI 0 instruction.

To find the system call identifiers corresponding to write and exit we refer to the Linux kernel source: $LINUX_SOURCE_ROOT/arch/arm/include/asm/unistd.h. Note that these identifiers are not guaranteed to be the same on each platform.

To assembly

Our ARM assembly code, in GAS syntax, therefore looks like (see inline comments for details):

.data

/* Data segment: define our message string and calculate its length. */
msg:
    .ascii      "Hello, ARM!\n"
len = . - msg

.text

/* Our application's entry point. */
.globl _start
_start:
    /* syscall write(int fd, const void *buf, size_t count) */
    mov     %r0, $1     /* fd := STDOUT_FILENO */
    ldr     %r1, =msg   /* buf := msg */
    ldr     %r2, =len   /* count := len */
    mov     %r7, $4     /* write is syscall #4 */
    swi     $0          /* invoke syscall */

    /* syscall exit(int status) */
    mov     %r0, $0     /* status := 0 */
    mov     %r7, $1     /* exit is syscall #1 */
    swi     $0          /* invoke syscall */

Assembling

Save the above as hello.S and run it through the GNU cross-assembler provided with the NDK. I will assume that you have the prebuilt NDK toolchain directory in your PATH (in my case /Users/peterdn/android-ndk/toolchains/arm-linux-androideabi-4.4.3/prebuilt/darwin-x86/bin):

arm-linux-androideabi-as -o hello.o hello.S
arm-linux-androideabi-ld -s -o hello hello.o

Update: if running natively on an ARM device with binutils installed, the arm-linux-androideabi- probably won’t work: simply use as and ld instead.

Deploying to Android

For many, the easiest way to test the above binary is by deploying it to an Android device with an ARM processor. This also means we can take advantage of the insanely useful adb tool. If you happen to be running vanilla Linux on an ARM device, the binary should still run, providing your kernel supports the newer EABI (I believe 2.6.15 and above).

To deploy and test on Android, simply run:

adb push hello /data/local/tmp/hello
adb shell /data/local/tmp/hello

It is also possible to run the binary locally on your device using the Android Terminal Emulator, as below:

Hello ARM on Android Terminal Emulator

Enjoy!