pete > courses > CS 202 Spring 24 > Lecture 24: functions I


Lecture 24: functions I

Goals


functions

start with intuition, then move on to specifics


starting with our stupid-simple example: func.c

some things are the same every time; namely, the instructions that get executed

but what’s (potentially) different every time?

so if you have a function main() and it calls a function foo(), something is going to have to remember all those things

main() is going to have to put the parameters someplace foo() knows to get at them

the compiler is going to need to make enough space for foo()’s local variables

foo() is going to need to put its return value someplace where main() knows to get at it

and finally, when foo() is done, we’re going to have to resume execution back where we left off in main()


on that last point, "where to resume execution" is just an instruction

an instruction in memory, because we’re working with a Von Neumann architecture

so the easiest way to identify a specific instruction in memory is therefore by its address

thus we call this fourth piece of info the return address


wrench in the works: what if foo() itself calls another function? (call it bar())

we need to remember all this same information for that invocation, too

but we also need to keep around all the information about foo()’s incovation

because when bar() returns, foo() is going to need to be able to access its parameters, local variables, etc

and most especially it’s going to need to return to the right place in main()


conclusion: every time a function is called, we need to gather this information and put it somewhere

we could have a bunch of sets of this information, too, for each function currently "in progress"

(ie, if main() calls foo() calls bar(), we’ll need this info for each of main(), foo(), and bar())

lets give this chunk of information a name

we’ll call it an activation record, because it’s the record of a function being activated


here are some interesting properties

once we return from a function, its activation record is meaningless

and at any given point, we only care about the most-recent activation record

thus when a new function is called, its activation record is going to be the most important

do you know of any data structures where we add stuff on one end, take stuff off that same end, and only ever look at the thing on the end?

a stack!

we could have a stack of activation records

where calling a function results in a new record being pushed onto the stack

and returning from a function results in the topmost record being popped (ie, disappearing)


so we want a stack

where should the constituent data live?

only one real choice here: main memory

in fact, part of main memory is set aside specifically for the stack of activation records

furthermore, it’s so pervasive that this area of memory is called, simply, The Stack

furthermore furthermore, another term for "activation record" is stack frame


so what does a stack frame look like?

bad news: there are no assembly language instructions that explicitly delineate the various data in the stack frame: it’s all implicit

so let’s go to the assembly and see if we can figure it out

I’m going to use a slightly different method to produce the assembly, because I think the assembly this method produces is clearer for our purposes today

decide for yourself which you prefer

first I’m going to compile func.c to machine code (the "-c" says "compile to machine code but don’t try to make an executable"—because a full-blown executable has a lot of other boilerplate that’s just going to get in the way today):

$ arm-none-eabi-gcc -c func.c

this produces a file called func.o (.o for "object", the old-school term for binary/machine code: "object code")

then I’m going to disassemble the machine code:

$ arm-none-eabi-objdump -d -j .text func.o > func.s

normally, objdump will just print its results to the screen; the "> func.s" bit will cause those results to be put in the file func.s instead

(the "-j .text" part tells objdump to focus on the .text section, which is what contains the actual instructions; there are other sections that contain, eg, initialized data like strings, which we’ll see more of later)

func.s

the left-most column is offset within the file

the next column is the machine code


let’s look at foo() first

we can infer that…

the last one is tricky: we can infer this because of this pair of instructions at the end:

ldr r3, [fp, #-8]
mov r0, r3

considering that we decided that The Stack is stored in memory, we can now start to make sense of this sp and fp nonsense

everything stored on the stack uses the contents fp as the base for the memory address calculation

and at the beginning of the function, fp gets the value of sp (and then sp is changed: we’ll get to that in a sec)

this implies that these two registers hold the memory address of (part of) The Stack

sp is a register (r13, to be precise) that contains the memory address of the last used word (32-bit value) in the stack

(incidentally, setting the initial value for the stack pointer is part of the work the operating system performs in running a program)

it’s otherwise known as the "stack pointer"

fp is another register (r11, to be precise) that contains the memory address of the first word in the current stack frame

it’s otherwise known as the "frame pointer"

with that in mind, let’s look at the first few instructions of foo()


push    {fp}        ; (str fp, [sp, #-4]!)

the instruction is "push {fp}", which objdump helpfully tells us is exactly equivalent to the instruction after the semi-colon

it’s storing the current value of the frame pointer (which will still point to the calling function’s frame) at sp-4

because we’re in a brand-new function, we can assume it’s storing this after all the existing stack frames

thus we can infer that newer stack frames have addresses that are more negative than older stack frames (because the new frame is in addresses that are the result of subtracting from sp)

conclusion: much like the enemy gate, the stack grows down

the exclamantion point (frequently enunciated as "bang") says to set sp to sp-4 after the instruction executes

thus this instructions effects are two-fold:

(this makes sense: we’ve pushed a 4-byte value—fp—onto the stack and now we need to maintain the property that sp points to the last used value)


add fp, sp, #0

the new frame starts at the last used word of the stack (which now happens to contain the calling function’s frame pointer)

sub sp, sp, #20

subtract 20 from the stack pointer: making room on The Stack for the new stack frame

str r0, [fp, #-16]

from the following code, we can infer that fp-16 stores the parameter, baz

thus from this instruction, we can infer that the caller passed the parameter to the callee (ie, foo()) in r0

that is, if we go back to the code for main, we should see that it fills r0 with the value it’s passing to foo()

and we see precisely that:

  1c:   e51b0008    ldr r0, [fp, #-8]

(convince yourself this is the case)


so that’s the (implicit) creation of the stack frame

we’ll look at more on Wednesday:

Last modified: