Kevin Boone

ARM assembly-language programming for the Raspberry Pi

4. Passing arguments in registers

In the previous example, I raised the subject of function calls. I will introduce a proper function call example shortly, but first we need to discuss the subject of passing arguments in more detail. I've already mentioned argument passing in the context of syscalls but, of course, you're going to want to pass your own arguments to functions you define.

Modern practice nearly always favours argument passing by registers. The other common technique is to use the stack -- an area of memory that expands to accommodate data placed in it. We will encounter the use of the stack for handling command-line arguments -- this is the way the Linux kernel passes arguments to user programs. However, for now, let's stick with registers.

The example

In the previous example, we looked at how to create a label exit, such that a branch to that label would exit the program. It's plausible that we might want to exit the program from multiple points, and it's also conceivable that we might want to provide a different exist status code each time. In the previous example, the status code was embedded in the 'exit' code, but here's a slight modification that makes it more flexible.

// Set exit value to a specific number, by jumping to code that
//  invokes sys_exit. The exit code is passed in the r0 register
.text

.global _start

// Exit the program.
//   On entry, r0 should hold the exit code
exit:
    mov    %r7, $1     // sys_exit is syscall #1
    swi    $0          // invoke syscall 

_start:
    mov    %r0, $43
    b      exit

The only difference between this example and the previous one is where the r0 register is set. I've defined the exit code to take a value in the r0 register and, as it happens, that's what the sys_exit syscall expects as well. So we don't have to do any extra work in the program code.

Argument passing

It turns out that this use of the r0 register is not arbitrary -- it is defined in the Procedure call standard for the ARM architecture, which is part of the Application Binary Interface (ABI). The procedure call standard sets out how arguments should be passed, for different types of data. Standardization of this sort makes it easy (well, easier) to build an application from components written by different people, using different programming languages.

If you're working in a high-level language, you don't need to worry about procedure call conventions or any other part of the ABI -- the compiler takes care of that. If you're working in assembly you do have to worry, and not just about procedure calling. There are issues of alignment as well, which I'll outline later.

The procedure call standard for 32-bit ARM devices states that, for 32-bit integer arguments (and this includes arguments that are memory addresses) the first four arguments are placed into registers r0 to r3. If there are more arguments than this, they are pushed onto the stack. The Linux kernel does not follow this convention for command-line arguments, nor does it fully do so for syscalls. Syscalls take all their arguments in registers and never use the stack. Since no syscall takes more than six arguments, and most take three or fewer, this technicality won't concern us here.

There are other rules in the procedure call standard for data elements larger than 32-bits, including floating-point numbers, which I will introduce in due course.

A digression about registers

Registers are data storage cells within the CPU. The 32-bit ARM CPUs have sixteen registers, labeled r0 to r15. In some circumstances these registers can be used in pairs to form 64-bit values. Some of the registers have specific purposes, and can be referred to in assembly language with more descriptive names. For example, r13 is the stack pointer (sp) whose crucial role will become apparent very soon.

Both for argument passing and for calculation, using registers is much faster than using memory. However, only eleven registers are available for general-purpose use -- r0-r8 and r10-r11. Of these, as I've mentioned, the first four are used for argument passing; but that doesn't preclude them being used for other things as well, if we're careful.

Within a particular function, eleven registers may be sufficient to store all the working data of that function. If it isn't, the function will have to make use of general memory or the stack, as we'll see.

Summary