The First Program
Our first program will initially do nothing; it will simply run an infinite loop. We use it as a foundational building block to create all further experiments. I will first describe how such a program is created, what peculiarities there are, how it is compiled, and then how it is executed.
Creating the Source Code
First, we open our text editor to write our first assembly program.
/*
The first program
13.10.2020 www.satyria.de
*/
.section .init @This shows the linker what to consider
.globl _start @Generates a global label
_start: @The label _start (entry address)
b main @Branch to "main"
.section .text @Instruction to the linker that code is coming
main: @Create the label "main"
mov sp,#0x8000 @Create a stack of 32768 bytes
MainLoop: @Infinite loop
b MainLoop
Save this file in the default installation directory on Windows as follows: C:\msys64\home\xxx, where "xxx" usually refers to your name. On Linux, it can be saved in the "Home" directory. Name it erstes.s. The suffix .s designates the file as assembly source code.
First, I will describe the program and the commands used, and in the end, we will compile it.
Comments
In this example, I have highlighted the comments in orange so that they can be better recognized. In the following examples, these will be shown in olive.
There are two types of comments. Comments that span multiple lines start with /* and end with */. This can be seen in lines 1-4 in the example.
In the following lines, @ is used to create a line comment. The comment starts from the @ character and ends at the end of the line.
Sections
Sections in assembly are needed to tell the linker how to handle the following lines.
Our first .section command (line 5) generates an initiation of the program. This code is executed first but should not be called again afterwards. In this section, we jump to our main program.
In line 11, we use the .text section. This information tells the linker that it is executable code. This part is also write-protected.
Here is a selection of possible .section commands:
| SECTION NAME | DESCRIPTION |
|---|---|
| .text | Executable program code
This section contains read-only data and cannot be written to |
| .data | Data
Within this section, data can be read and written. The data is not executable |
| .rdata | Similar to .data, but the data cannot be written. This is used, for example, for constants. |
| .init | Generates code that is only executable at the start of the program. |
.globl
In line 6, the label _start is defined as global. This makes the linker aware of this label. In our system, which we must program here, we need to define this label as global so the linker can place our code in memory at address 0x8000. For 32-bit bare-metal applications, the Raspberry Pi expects the program to be exactly there. This is due to the peculiarity of the Raspberry Pi. The Raspberry Pi initially doesn't start with the CPU, but with its graphics chip. A firmware is started there, the Raspberry Pi is initialized, and only then handed over to the CPU. During this time, this memory space is reserved, and the CPU is then assigned to look at memory location 0x8000 and execute the code there. For 64-bit bare-metal applications, the address is 0x80000.
The First Assembly Commands
In this example, only two assembly instructions have been used so far, b and mov, highlighted in blue.
b (Branch)
b stands for Branch and means that the program should branch.
b main @Branch to "main"
The first Branch command (line 9) means it should jump to the label main, thereby starting our program.
MainLoop: @Infinite loop
b MainLoop
The second Branch command used here means it should jump to the label MainLoop. If you look at the code closely, you will see that a constant jump to the same code section occurs. This creates an infinite loop. We must do this here because otherwise, the program would do something undefined, possibly things we do not want.
A Note on Labels
All labels that are jumped to must be concluded with : in the code. All alphanumeric characters plus a few special characters are allowed. Ideally, one should use meaningful labels so that the code remains readable later on.
mov (Move)
mov sp,#0x8000 @Create a stack of 32768 bytes
The command mov means that information should be moved. In our case, we write the address 0x8000 into the stack pointer (sp) register. We need the stack later in our programs to store temporary data.
A stack operates in a way that the last number placed on it is the first one taken off (LIFO). The stack pointer (sp) negates its position, meaning the first number is stored at memory location 0x8000, and the next at 0x7FFF.
In our case, we reserve memory from 0 to 0x8000 for the stack.
I will describe the mov command in more detail later on; for now, let's just see it in action.
Number Formats
In the example mov, I used the "number" 0x8000. In the comment, the number 32768 is shown. These two numbers are identical. The prefix 0x is used to indicate a hexadecimal number. Without specifying a format, the compiler interprets the number as a decimal. Besides these two formats, there are a few others. Here’s a list:
| NUMBER TYPE | BASE | PREFIX | ALLOWED CHARACTERS |
|---|---|---|---|
| Decimal | 10 | 0-9 | |
| Hexadecimal | 16 | 0x oder 0X | 0-9, A-F |
| Octal | 8 | O | 0-7 |
| Binary | 1 | 0b oder 0B | 0-1 |
| Floating Point | 10 | 0f oder 0F | 0-9 |
Compiling
We have saved our source code in the directory “C:\msys64\home\xxx” on Windows. To compile it, we open “msys2” on Windows, which can be found via the Windows menu.
On Linux, we open the terminal.
Usually, the corresponding home directory is opened, regardless of whether we are using Windows or Linux.
If it is not there, you can simply switch to this directory using the command “cd ~”.
With “ls” you can display the contents.
Here is an example under Windows:
To perform the compiling, three commands are needed:
arm-none-eabi-as erstes.s -o erstes.o
arm-none-eabi-ld --no-undefined erstes.o -o erstes.elf
arm-none-eabi-objcopy erstes.elf -O binary kernel7l.img
“arm-none-eabi-as” first generates an object file with the “-o” option. Since the assembler might not know everything yet, it initially generates this file.
The linker “arm-none-eabi-ld” generates the actual machine code and combines information that may not have been declared in the code itself.
Since the linker creates an “elf” file, but our Raspberry Pi in its initial state does not understand this and expects a different type of file, we need to convert this file. The command “arm-none-eabi-objcopy” takes care of this for us. The result is a “kernel7l.img” file.
Running the Program on the Raspberry Pi
Preparing the SD Card (Simple Method)
To run the file on the Raspberry Pi, we need to prepare a few things.
The simplest method is to use an already functioning SD card with “Raspbian” installed. When you look at this SD card in a file manager, there are two partitions. We are only interested in the boot partition. It is best to rename all the files that start with “kernel…”. These would interfere with running our program and might not start.
Rename them so that you can later revert them if you decide you no longer want to continue… Here is an example:
Preparing the SD Card (Preferred Method)
A second method that I prefer is to create a dedicated SD card for these experiments. Here’s how to do it.
First, format an SD card in “FAT32” format and name it “BOOT”. To make this card bootable, a few files are needed:
bcm2711-rpi-4-b.dtb
bootcode.bin
start.elf
start4.elf
These can be downloaded either from https://github.com/raspberrypi/firmware/tree/master/boot or copied from a working SD card and then copied to the SD card.
Running the Program
Whichever version you’ve chosen, you can now copy your generated “kernel7i.img” file to the boot partition of the SD card. Insert the card into the Raspberry Pi and start it. In this example, nothing will happen because our code does nothing but run an infinite loop. And that is what it does.
If you have the HDMI cable connected, you can follow the boot process on the screen and see the colorful gradient at the end.
In the next chapter, we will let the Raspberry Pi perform the first visible action (turning on an LED) without needing to use any additional hardware.