Hello World in Assembly on a Raspberry Pi
The following is a practical guide as to where to begin writing assembly code for the ARM CPU on a Raspberry Pi. It is also possible this guide will work on other ARM based systems running Linux as nothing specific to the Pi is assumed.
Hello World is traditionally the first program someone writes when learning a new programming language and simply prints this text on the screen. The process that goes on to print something on screen in a modern computer is a fairly complex operation but luckily these days we have an operating system to help us out. You may recall one of the features of operating systems is they come bundled with useful software libraries to simplify common operations for the programmer. The Raspberry Pi (usually) runs the Linux operating system and printing something on the screen is certainly something Linux can help us out with. So what we’re actually going to do is use assembly language to tell the operating system to do it all for us.
Linux has a number of built-in software routines called “system calls” or syscalls for short. If you have spent much time using the Linux command line many of these syscalls will seem very familiar. There is one called “mkdir” which makes a directory. There is another called “chmod” which changes the permissions of a file. Many Linux command line tools end up using these syscalls. The library functions available in high level programming languages like Python also use them too.
There is a syscall called “write” which writes data to a file. In Linux many operations are implemented as fake files. These are files that do not really exist on disk but when you read from or write specific values to them they control aspects of the machine. One of these is “stdout” which behaves like a file, although it isn’t really. What it does is when you write to it, it displays the text written to the terminal. This is the behaviour we need to print out hello world.
In Linux, system calls can be accessed through an interrupt. The way this works is you place the parameters of the system call into CPU registers and then call the interrupt. This causes the operating system to stop what it is doing and run a specific interrupt routine. Each syscall has an assigned code number which we place into a specific register. The Raspberry Pi has an ARM CPU and on this processor register w8 is used to specify which syscall we require. The interrupt code looks at the w8 register, determines which syscall is being requested, looks up the location of the relevant code and then jumps there. The syscall routine uses values stored in the other registers as parameters to the syscall.
At the time of writing (2024) Raspberry Pis have started transitioning to using 64-bit operating systems by default. The below code will only work on the newer 64-bit editions of the Raspberry Pi O/S (see here to determine if you have the 64-bit version). The 32-bit editions used on older Pis will need different assembly language code referring to different registers. If you are looking at other tutorials on the web and see references to ARM registers beginning with “r”, you are looking at 32-bit assembly code.
In Linux on 64-bit ARM the write syscall has code 64 and so that number needs to go into register w8. We need to put the memory location of the data we want to write to the file into register x1. The length of the data in bytes goes into register x2. Then we need to specify which file we want to write to as a “file number” in register x0. The file number is a code that references a specific file currently open on the machine. Normally this value is returned when you open a file. However, the fake stdout file is always number 1.
In 64-bit ARM assembler, the w8 register is actually the same register as x8 but only refers to half (32-bits) of it.
The mnemonics for the assembly language instructions available for ARM that we will need are:
- ldr - copy a value from memory to a register
- mov - place a value into a register
- svc - causes an interrupt
ARM is a RISC style of CPU. As is typical for this design, most instructions have very few addressing modes compared with other types of CPU. The ldr instruction is the only one that can move data from memory into a register. The mov instruction can’t access memory but can store a specific constant value into a register (immediate mode addressing).
One of the conveniences of using assembly language instead of machine code is we can get the assembler to automatically figure out where to place variables in memory for us. We’re going to define the string “Hello World”, but we don’t need to bother specifying where to store it, the assembler can do that for us. What we do is give the string a label and then refer to it by the label. In this case we will call the string “greeting”. When the assembler sees that label it will replace it with the location of whatever memory address it has decided to place the string at.
When a program finishes running, Linux requires us to return an exit status code to the operating system. This indicates if the program ran successfully. If we don’t do this the program will crash. Exit status zero means the program was successful. Returning the exit status is done with the “exit” syscall which is number 93 in 64-bit ARM.
The complete assembly language code is as follows. Save the code in a file called hello.s
// Assembly for 64-bit ARM Linux
.data
greeting: .ascii "Hello World\n"
.text
.global _start
_start:
//Print the greeting message to the terminal
ldr x1, =greeting // Load the location in memory of the greeting message into register x1
mov x2, #12 // Store the length of the greeting message into register x2 (it's 12 bytes long)
mov x0, #1 // Load 1 into register x0. This tells the write syscall to send the greeting to stdout (the terminal)
mov w8, #64 // The write syscall is number 64. This is placed into the w8 register
svc #0 // Tell Linux to run the syscall
// Exit cleanly from the program by setting the exit status
mov x0, #0 // Set the program exit status to 0 (set in the x0 register). Zero means the program ran correctly.
mov w8, #93 // Exit is syscall number 93 which is placed into the w8 register
svc #0 // Tell Linux to run the syscall
We need to convert the assembly into machine code. This is done using an assembler. The Raspberry Pi O/S usually includes an assembler by default called “as” which is free and open-source.
as -o hello.o hello.s
Then we need to link it. The linker takes the assembled code (the machine code) and merges it together with any libraries we have used, then produces a final executable. In this case we haven’t used any libraries.
ld -o hello hello.o
This produces an executable called hello
. To run it we do:
./hello
and we get the expected hello world output to the terminal:
pi@mypi:~/dev/assembly $ ./hello
Hello World
We can actually reverse the machine code in the executable back into the assembly using a disassembler utility like this and find out what the assembler did with our code:
objdump -s -d hello
This gives us:
Contents of section .data:
4100d8 48656c6c 6f20576f 726c640a Hello World.
Disassembly of section .text:
00000000004000b0 <_start>:
4000b0: 58000101 ldr x1, 4000d0 <_start+0x20>
4000b4: d2800182 mov x2, #0xc // #12
4000b8: d2800020 mov x0, #0x1 // #1
4000bc: 52800808 mov w8, #0x40 // #64
4000c0: d4000001 svc #0x0
4000c4: d2800000 mov x0, #0x0 // #0
4000c8: 52800ba8 mov w8, #0x5d // #93
4000cc: d4000001 svc #0x0
4000d0: 004100d8 .word 0x004100d8
4000d4: 00000000 .word 0x00000000
It’s pretty much exactly the assembly we entered, except it swapped out the “greeting” label for a memory address (4000d0). Here we find another memory address (4100d8) and that’s where the “Hello World” text is stored.