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
r0
throughr3
. - The remaining parameters must be pushed on the stack.
r13
orsp
points to the last allocated stack element.r14
orlr
points to the instruction in the caller to execute after the function finishes. We could call thisoldpc
.r15
orpc
points 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
toupper
andtolower
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"