Calling Functions in Assembly

CS 301 Lecture, Dr. Lawlor

Last time we saw how to call a function from assembly using the "call" instruction:
extern read_input
call read_input
(executable NetRun link)

This is pretty easy.  But you do have to be very careful, because the function you call can overwrite all the registers to store its own stuff.

What you need is a place of your own to store your stuff.  That place is the stack.

Stacks Generally

For example, this ordinary C++ code implements a stack as a C++ object:
class my_stack {
int storage[10]; /* contents of stack */
int top; /* index of element on top of stack, or -1 if stack is empty */
my_stack() {top=-1;}
~my_stack() {if (top!=-1) std::cerr<<"You left stuff on the stack, foo!";}

void push(int i) { storage[++top]=i; }
int pop(void) { return storage[top--]; }
int foo(void) {
my_stack s;
return s.pop();
(executable NetRun link)
The defining feature of a stack is that the newest stuff comes off first (a stack is "LIFO": Last-In, First-Out).  This means if you push 3, then 7, you'll pop first the 7, then the 3.  So the first "pop" above returns 7, and the next "pop" returns 3.   If you don't pop exactly the same number of times that you push, you're either leaving extra junk on the stack, or taking too much off the stack, which is an error.  In assembly, of course, "an error" means a segfault or some other equally horrible crash.

The CPU itself supports a stack, normally called "The Stack" (although not normally capitalized!).  It's "The" stack because the CPU itself does pushing and popping, most simply with the push and pop routines:
    push eax;   <- copies eax onto the top of the stack
    pop ecx;  <- copies the top of the stack into ecx

You can push almost anything (a register, a constant, a label, etc.), and you can pop almost anything as well. 

The biggest use of The Stack in assembly is as a convenient place to stash variables while you call another function.  For computing, registers are the most convenient place to store variables, but there aren't that many registers (especially on x86!), and other subroutines have a nasty habit of overwriting your scratch registers.  The stack, by contrast, can store at least several megabytes of data, and is guaranteed to be private.

Call and Return

The "call" instruction is used to run another function.  When that function hits the "ret" instruction, the machine continues execution where the "call" left off.  For example, here are two assembly functions, foo and bar:
global foo ; <- allow foo to be called from main
; A function, like the C/C++: "int foo(void) {return bar()+1000;}"
call bar; Run bar, and come back with return value in eax.
add eax,1000; Add 1000 to whatever bar returned us

; A function, like the C/C++: "int bar(void) {return 17;}"
mov eax,17; eax is the return result register on x86
(runnable NetRun Link)

The machine keeps track of where "ret" should return to by using the stack:
So we could be really explicit by writing the same subroutines foo and bar above like this:
global foo ; <- allow foo to be called from main
; A function, like the C/C++: "int foo(void) {return bar()+1000;}"
; See below... call bar; Run bar, and come back with return value in eax.
push rest_of_foo; <- Where to jump back to
jmp bar; <- Go run bar function now
add eax,1000; Add 1000 to eax

; A function, like the C/C++: "int bar(void) {return 17;}"
mov eax,17; eax is the return result register on x86
; See below... ret
pop ecx ; <- Where to jump back to
jmp ecx ; Jumps back to rest_of_foo
(runnable NetRun Link)

Of course, push/jmp isn't nearly as clear as "call", and it uses more machine-code instructions.  But it's important to understand what call and return are doing.

Why?  Well, calling a recursive function (or any deeply-nested sequence of functions) uses up stack space to store the return address.  This means recursion can easily run out of stack space with only a few million calls--and this can happen in way under a second!

For example, here's a dumb way to do addition:
/* a really dumb way to add a+b */
int stupid_add(int a,int b) {
if (b==0) return a;
else return stupid_add(++a,--b);

int foo(void) {
return stupid_add(3,7000*1000);
(executable NetRun link)

Why does this program crash?  It runs out of stack space!  Try it with 6000*1000--it'll work!   Lesson: don't let functions-call-functions millions of levels deep, or you'll get weird crashes.

Passing Parameters on the Stack

Consider how we can pass parameters into a function, like the values 3 and 7 in this C/C++ code:
The nicest way to pass parameters *into* a function is to stash them in registers.  For example, on 64-bit x86, the first parameter, 3, goes into register edi.  The second parameter, 7, goes into esi.  You then call bar, and bar can look in edi and esi to find its parameters.  On PowerPC, the first parameter goes into %r3, the second into %r4, and so on.  Note that even on 32-bit x86, the return value is passed *out* of a subroutine in a register, eax.

But there's one big drawback to passing parameters in registers.  What happens when a function takes more parameters than you've got registers?
That's 11 parameters, and x86 only has 8 registers counting the stack pointer--so we can't keep all the parameters in registers.

So on 32-bit x86, the usual thing is to pass all parameters "on the stack".  That is, to call bar(3,7), we push 7, then push 3, then call bar, and then clean up the stack:
    push 7
    push 3
    call bar
    pop esi
    pop esi

The "pop"s are used to clean up the stack after we call bar--because we pushed on those parameters, we have to pop them, or we'll get a horrible crash when our subroutine tries to return.

Bar then can access its parameters by looking on the stack--in the worst case, it could tear apart the stack looking for its parameters:
    pop edx ; <- return address, from "call"
    pop ecx ; <- first parameter, 3
    pop eax;  <- second parameter, 7
But I don't recommend this approach, because you've got to be very careful to put back the return address (so "ret" will work) *and* all the parameters (so your caller will be able to pop the parameters he pushed).

Instead, the best way to access your parameters is to do pointer arithmetic on the stack pointer, like this horrific thing:
    mov ecx, [esp+4] ; <- copies the first parameter, 3, into edx
The [] brackets are how you access memory in assembly--they're the equivalent of a C/C++ pointer-dereference, like "*esp".

We add 4 to esp to skip over the 4-byte return address, and get one int deeper into the stack, where our first parameter is stored.

We'll talk more about memory access and pointer arithmetic in the coming weeks.