CS 352 Lecture 28 – Functions
Dear students,
Today we look at how functional abstractions are built atop rudimentary assembly instructions. The magic to make functions happen is really just two ideas:
- a register (
lr), which places a bookmark for our program counter so we know where to return to after the function finishes - a standard protocol for passing parameters and return values
The first of these is necessary because we branch to functions. Branching changes the program counter. When a function finishes, we have to resume execution where we left off. The bl instruction preserves this “where we left off”. Essentially, bl myfunc executes these two instructions:
mov lr, pc + 8 // pc + 0 b myfunc // pc + 4 next instruction // pc + 8
Note that there’s only one lr register. If our function calls a function, it will need to maintain its own bookmark in lr, which will clobber the original. It’s a function’s job to not clobber the data of the caller. So, we must safeguard a copy of the caller’s lr on the stack:
sub sp, sp, #4 str lr, [sp]
There’s an abbreviation for this:
push {lr}
The calling convention—the ARM Architecture Procedure Call Standard (AAPCS)—tells us how to send data to and receive data from a function:
- The first four parameters are sent via registers
r0throughr3. - The remaining parameters must be pushed on the stack.
r13orsppoints to the last allocated stack element.r14orlrpoints to the instruction in the caller to execute after the function finishes. We could call thisoldpc.r15orpcpoints to the current instruction.
Registers r0 through r3, the status register, and any stack memory below a function’s stack pointer are likely to be obliterated by a function call. If these contain precious data, get them in a safe place: either in registers r4 through r11 or on the stack. We call vulnerable registers caller-save. On other hand, if a function wants to use registers r4 through r11, r13, or r15, it must preserve a copy of these values and restore them just before it finishes.
Let’s write a few programs that involve functions but no stack memory:
- Collatz sequences
toupperandtolower
We try (through the compiler) as hard as we can to use registers for our calculations, but we cannot always avoid memory. We will look at how local variables are allocated, manipulated, and released through a few more examples. These will be contrived, because small problems easily fit into the available registers.
- double a number
- compute rise over run between two points
See you next class!

P.S. Here’s the code we wrote together…
caseconverter.s
.text
.global main
main:
push {lr}
ldr r0, =letter
ldr r0, [r0]
bl toupper
mov r1, r0
ldr r0, =message
bl printf
pop {lr}
mov pc, lr
tolower:
// arg0 is going to be in r0
// return value in r0
// if we alter lr, restore it before returning
orr r0, r0, #32
mov pc, lr
toupper:
mvn r1, #32
and r0, r0, r1
mov pc, lr
.data
letter:
.byte 'x'
message:
.asciz "Today's class is brought to you by the letter %c!\n"
collatz.s
.text
.global main
main:
push {lr}
// n = ...
// while n != 1
// print n
// n = collatzify n
//ldr r1, [r1, #4] // r1 = argv[1]
//bl atoi
//
//mov r1, r0
//ldr r0, =message
//bl printf
ldr r0, =n
ldr r0, [r0]
collatzify
mov r1, r0
bl printf
collatzify
mov r1, r0
bl printf
pop {lr}
mov pc, lr
collatzify:
and r1, r0, #1
cmp r1, #1
addeq r0, r0, r0, lsl #1 // r0 = r0 + r0 * 2
addeq r0, r0, #1
lsrne r0, r0, #1
mov pc, lr
.data
n:
.word 4
message:
.asciz "%d\n"