Assembler programming on the Raspberry Pi
Assembler on Pi
Talk to your Raspberry Pi in its native assembler language.
Assembler programs run directly on the computer's hardware, which means they can reach nearly the maximum achievable speed of execution. Because assembler program code is very low level, writing the code is more complicated, but it is still the best choice for some tasks, especially on a computer such as the Raspberry Pi with its limited resources. Before you can start creating programs, however, you need to plumb the depths of the CPU and peripheral architecture.
Machine Code
To begin, it makes sense to clarify some terms. The CPU only understands machine code – zeros and ones or, more precisely, voltage levels that represent zeros and ones. Each command in machine code has a human-readable abbreviation that is easy to remember. These abbreviations are known as mnemonics and act as assembler commands. Assembler code is specific to a CPU architecture, which means that code for a Raspberry Pi (ARM) will not run on a PC (x86).
Programming in assembler on the Raspberry Pi can be approached in two ways: First, you can create an image in which you package the code and then boot the small-board computer (SBC) from that image to run the program. In other words, you degrade the Raspberry Pi to a microcontroller. With this method, the Pi runs without an operating system. Although you have full access to everything, you don't even get a shell.
The second way is to run the assembler program on the Raspberry Pi itself, which gives you the luxury of an operating system with everything that entails; however, you are limited in terms of direct access to the hardware. The second method was used for the example in this article.
Setup
A Raspberry Pi 3 with the current Raspberry Pi OS Lite provides the basis for my experiments. I will use Raspberry Pi Imager [1] to prepare the SD card, after which, I can boot from the card and get started right away, because all the tools needed for coding in assembler are included in the image. That said, an additional action provides more convenience and flexibility (see the "Activating SSH" box).
Activating SSH
To remove the need for an additional monitor and keyboard, I recommend working on the Raspberry Pi over SSH. To get the service running correctly on first boot requires some minor intervention. To begin, create an empty /boot/ssh
file on the SD card; the SSH daemon will then launch automatically at boot time.
If needed, redirect the output of the X server over SSH from the Raspberry Pi to the desktop PC with the -X
option. This works best if you are also using Linux on the desktop computer. If your router supports local name resolution, use the
ssh -X pi@raspberrypi@local
command to open the connection. All graphical output from programs then end up on the desktop computer. If the local DNS does not work, use the IP address of the Raspberry Pi, which you can look up from the list of connected devices on the router.
No Hello World
When you start working with a new programming language, the traditional approach is create a "Hello World" program; however, it takes a fair amount of assembler code and some understanding of strategies to generate even this simple output. Therefore, the first small assembler program only outputs the return code on the console, which indicates the status of a program on exiting. Bash stores this value in the $?
variable, which you read with echo $?
.
A return code of 0 means that the previously executed command ran without error; a value greater than 0 indicates an error. Listing 1 shows the example program 42.s
, so named because the return code value 42 is the result. (Note that the title of this article is 42 in binary-coded decimal (BCD) encoding, which represents each decimal section from 0 to 9 as 4 bits (i.e., half a byte – or a nibble, if you prefer.)
Listing 1
42.s
.global main /* Entry point for the program */ main: mov r0, #42 /* Move value 42 to register r0 */ bx lr /* Return to calling program */
Assembler comprises relatively simple commands that do nothing more than move bytes back and forth, manipulate them, or react to a status bit, which makes it extremely important to document the code thoroughly and to use mnemonic identifiers where possible.
Labels are used in programming languages to mark points in the source code that serve as jump targets. The compiler exchanges the label for a physical memory address at build time, which clarifies the massive advantage of using labels: You do not need to calculate laboriously where in memory a particular command is located. Moreover, with each additional command you insert, all the addresses below it would move.
As in many other programming languages, you need to specify the starting point for a program in assembler. In Java and C the corresponding function is main()
; in assembler you define the global label main
. The label must precede the first line of code you want to execute, as shown in the assembler program in Listing 1.
The first line defines main
globally so that the linker can find it. That label is then used in the second line, immediately followed by the first command, the mov
command (short for move), which is used to move values (e.g., to store constant values in a register or to transfer the content of one register to another). To move values from registers into RAM or load them from RAM, you need the str
(store register) and ldr
(load register) commands instead. A register is a memory location on the CPU. An overview of the registers on the Raspberry Pi is shown in Table 1.
Table 1
ARM CPU Registers
Register | Mnemonic | Function |
---|---|---|
r0-r10 |
– |
General registers without a special function |
r11 |
fp |
Frame pointer register |
r12 |
ip |
General register without a special function |
r13 |
sp |
Stack pointer |
r14 |
lr |
Link register |
r15 |
pc |
Program counter |
The program status register acts as the CPU's internal control register. The states of the individual bits tell you what results specific CPU register operations return. The commands for conditional jumps react to these bits. One well-known control bit is the zero bit (bit 30), which indicates that the value of an arithmetic logic unit (ALU) of a CPU, wherein all calculations and comparisons are performed, is 0.
In the example in Listing 1, the mov
command stores a value of 42 in the CPU register r0
. When the program ends, the operating system reads the value of register r0
and stores it in shell variable $?
.
The bx
command in the last line causes the CPU to continue the program at a different memory address – in this case, the address found in register lr
. This register stores the address for program calls that the computer has to make after the program terminates.
The only question that now remains is how to generate an executable program from the assembler code. The workflows required to do this are very similar to the process of compiling C programs:
$ as -o 42.o 42.s $ gcc 42.o $ ./a.out $ echo $? 42
The first line creates an object file from the assembler source code, which is then bound in the next line to the operating system to obtain an executable file. Unless you specify otherwise, this file is named a.out
. You can execute the a.out
program as usual. The final command shows that the program returns the value 42.
Buy this article as PDF
(incl. VAT)