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
Functions are conventionally called with arguments in registers or on the stack.
The Procedure call standard attempts to ensure interoperability between software components by standardizing the use of registers and the stack, among other things.
32-bit ARM CPUs have sixteen registers, or which eleven are general-purpose.
- Previous: 3. Using branch instructions
- Table of contents
- Next: 5. Using constants in assembly programming