8.5. Functions in Assembly
In the previous section, we traced through simple functions in assembly. In this section, we discuss the interaction between multiple functions in assembly in the context of a larger program. We also introduce some new instructions involved with function management.
Let’s begin with a refresher on how the call stack is managed. Recall that
%esp is the stack pointer and always points to the top of the stack. The
register %ebp represents the base pointer (also known as the frame pointer)
and points to the base of the current stack frame. The stack frame (also
known as the activation frame or the activation record) refers to the
portion of the stack allocated to a single function call. The currently
executing function is always at the top of the stack, and its stack frame is
referred to as the active frame. The active frame is bounded by the stack
pointer (at the top of stack) and the frame pointer (at the bottom of the
frame). The activation record typically holds local variables and parameters
for a function.
Figure 1 shows the stack frames for main and a function it calls
named fname. We will refer to the main function as the
caller function and fname as the callee function.
In Figure 1, the current active frame belongs to the callee function
(fname). The memory between the stack pointer and the frame pointer is used
for local variables. The stack pointer moves as local values are pushed and
popped from the stack. In contrast, the frame pointer remains relatively
constant, pointing to the beginning (the bottom) of the current stack frame.
As a result, compilers like GCC commonly reference values on the stack relative
to the frame pointer. In Figure 1, the active frame is bounded below by
the base pointer of fname, which contains the stack address 0x418. The value
stored at this address is the "saved" %ebp value (0x42c), which itself
indicates the bottom of the activation frame for the main function. The top
of the activation frame of main is bounded by the return address, which
indicates the program address at which main resumes execution once the
callee function finishes executing.
|
The return address points to program memory, not stack memory
Recall that the call stack region (stack memory) of a program is different from
its code region (code memory). Whereas
Figure 2. The parts of a program’s address space
|
Table 1 contains several additional instructions that the compiler uses for basic function management.
| Instruction | Translation |
|---|---|
|
Prepares the stack for leaving a function. Equivalent to: mov %ebp, %esp pop %ebp |
|
Switches active frame to callee function. Equivalent to: push %eip mov addr, %eip |
|
Restores active frame to caller function. Equivalent to: pop %eip |
For example, the leave instruction is a shorthand that the compiler uses to
restore the stack and frame pointers as it prepares to leave a function. When
the callee function finishes execution, leave ensures that the frame pointer
is restored to its previous value.
The call and ret instructions play a prominent role in the process where
one function calls another. Both instructions modify the instruction pointer
(register %eip). When the caller function executes the call instruction,
the current value of %eip is saved on the stack to represent the return
address, or the program address at which the caller resumes executing once the
callee function finishes. The call instruction also replaces the value of
%eip with the address of the callee function.
The ret instruction restores the value of %eip to the value saved on the
stack, ensuring that the program resumes execution at the program address
specified in the caller function. Any value returned by the callee is stored in
%eax. The ret instruction is usually the last instruction that executes in
any function.
8.5.1. Tracing Through an Example
Using our knowledge of function management, let’s trace through the code example first introduced at the beginning of this chapter.
#include <stdio.h>
int assign(void) {
int y = 40;
return y;
}
int adder(void) {
int a;
return a + 2;
}
int main(void) {
int x;
assign();
x = adder();
printf("x is: %d\n", x);
return 0;
}
We compile the code with the -m32 flag and use objdump -d to view the
underlying assembly. The latter command outputs a pretty big file that contains
a lot of information that we don’t need. Use less and the search
functionality to extract the adder, assign, and main functions:
804840d <assign>: 804840d: 55 push %ebp 804840e: 89 e5 mov %esp,%ebp 8048410: 83 ec 10 sub $0x10,%esp 8048413: c7 45 fc 28 00 00 00 movl $0x28,-0x4(%ebp) 804841a: 8b 45 fc mov -0x4(%ebp),%eax 804841d: c9 leave 804841e: c3 ret 0804841f <adder>: 804841f: 55 push %ebp 8048420: 89 e5 mov %esp,%ebp 8048422: 83 ec 10 sub $0x10,%esp 8048425: 8b 45 fc mov -0x4(%ebp),%eax 8048428: 83 c0 02 add $0x2,%eax 804842b: c9 leave 804842c: c3 ret 0804842d <main>: 804842d: 55 push %ebp 804842e: 89 e5 mov %esp,%ebp 8048433: 83 ec 20 sub $0x14,%esp 8048436: e8 d2 ff ff ff call 804840d <assign> 804843b: e8 df ff ff ff call 804841f <adder> 8048440: 89 44 24 1c mov %eax,0xc(%esp) 8048444: 8b 44 24 1c mov 0xc(%esp),%eax 8048448: 89 44 24 04 mov %eax,0x4(%esp) 804844c: c7 04 24 f4 84 04 08 movl $0x80484f4,(%esp) 8048453: e8 88 fe ff ff call 80482e0 <printf@plt> 8048458: b8 00 00 00 00 mov $0x0,%eax 804845d: c9 leave 804845e: c3 ret
Each function begins with a symbolic label that corresponds to its declared
name in the program. For example, <main>: is the symbolic label for the
main function. The address of a function label is also the address of the
first instruction in that function. To save space in the figures that follow, we
truncate addresses to the lower 12 bits. So, program address 0x804842d is
shown as 0x42d.
8.5.2. Tracing Through main
Figure 3 shows the execution stack immediately prior to the execution
of main.
Recall that the stack grows toward lower addresses. In this example, %ebp is
address 0x140, and %esp is address 0x130 (both of these values are made
up for this example). Registers %eax and %edx initially contain junk
values. The red (upper-left) arrow indicates the currently executing instruction. Initially,
%eip contains address 0x42d, which is the program memory address of the
first line in the main function. Let’s trace through the program’s
execution together.
The first instruction pushes the value of ebp onto the stack, saving address 0x140. Since
the stack grows toward lower addresses, the stack pointer %esp updates to 0x12c, which is 4 bytes less than 0x130.
Register %eip advances to the next instruction in sequence.
The next instruction (mov %esp, %ebp) updates the value of %ebp to be the same as %esp.
The frame pointer (%ebp) now points to the start of the stack frame for the
main function. %eip advances to the next instruction in sequence.
The sub instruction subtracts 0x14 from the address of our stack
pointer, "growing" the stack by 20 bytes. Register %eip advances to the next instruction, which is
the first call instruction.
The call <assign> instruction pushes the value inside register
%eip (which denotes the address of the next instruction to execute) onto the
stack. Since the next instruction after call <assign> has the address
0x43b, that value is pushed onto the stack as the return address.
Recall that the return address indicates the program address where execution should
resume when program execution returns to main.
Next, the call instruction moves the address of the assign function (0x40d) into
register %eip, signifying that program execution should continue
into the callee function assign and not the next instruction in main.
The first two instructions that execute in the assign function are the usual book-keeping
that every function performs. The first instruction pushes the value stored in %ebp (memory
address 0x12c) onto the stack. Recall that this address points to the beginning of the
stack frame for main. %eip advances to the second instruction in assign.
The next instruction (mov %esp, %ebp) updates %ebp to point to the top of the stack,
marking the beginning of the stack frame for assign. The instruction pointer (%eip) advances to
the next instruction in the assign function.
The sub instruction at address 0x410 grows the stack by 16 bytes,
creating extra space on the stack frame to store local values and updating
%esp. The instruction pointer again advances to the next instruction in the assign function.
The mov instruction at address 0x413 moves the value $0x28 (or 40) onto the stack
at address -0x4(%ebp), which is four bytes above the frame pointer.
Recall that the frame pointer is commonly used to reference locations
on the stack. %eip advances to the next instruction in the assign function.
The mov instruction at address 0x41a places the value $0x28 into register %eax, which
holds the return value of the function. %eip advances to the leave instruction in the assign function.
At this point, the assign function has almost completed execution.
The next instruction that executes is the leave instruction, which
prepares the stack for returning from the function call. Recall that leave is analogous to the
following pair of instructions:
mov %ebp, %esp pop %ebp
In other words, the CPU overwrites the stack pointer with the frame
pointer. In our example, the stack pointer is initially updated from 0x100 to
0x110. Next, the CPU executes pop %ebp, which takes the
value located at 0x110 (in our example, the address 0x12c) and places
it in %ebp. Recall that 0x12c is the start of the stack frame for main.
%esp becomes 0x114 and %eip points to the ret instruction in the
assign function.
The last instruction in assign is a ret instruction. When ret
executes, the return address is popped off the stack into register %eip.
In our example, %eip now advances to the call to the adder function.
Some important things to notice at this juncture:
-
The stack pointer and frame pointer have been restored to their values prior to the call to
assign, reflecting that the stack frame formainis once again the active frame. -
The old values on the stack from the prior active stack frame are not removed. They still exist on the call stack.
The call to adder overwrites the old return address on the stack
with a new return address (0x440). This return address points to the next
instruction to be executed after adder returns, or mov %eax, 0xc(%ebp).
%eip reflects the first instruction to execute in adder, which is at
address 0x41f.
The first instruction in the adder function saves the caller’s frame pointer
(%ebp of main) on the stack.
The next instruction updates %ebp with the current value of %esp, or address 0x110.
Together, these last two instructions establish the beginning of the stack frame for adder.
The sub instruction at address 0x422 "grows" the stack by 16 bytes. Notice
again that growing the stack does not affect any previously created values
on the stack. Again, old values will litter the stack until they are
overwritten.
Pay close attention to the next instruction that executes: mov $-0x4(%ebp),
%eax. This instruction moves an old value that is on the stack into register %eax!
This is a direct result of the fact that the programmer forgot to initialize a in the
function adder.
The add instruction at address 0x428 adds 2 to register %eax. Recall that IA32 passes
the return value through register %eax. Together, the last two instructions are
equivalent to the following code in adder:
int a;
return a + 2;
After leave executes, the frame pointer again points to the beginning of the stack
frame for main, or address 0x12c. The stack pointer now stores the address 0x114.
The execution of ret pops the return address off the stack, restoring the instruction
pointer back to 0x440, or the address of the next instruction to execute in main. The
address of %esp is now 0x118.
The mov %eax, 0xc(%esp) instruction places the value in %eax
in a location 12 bytes (three spaces) below %esp.
Skipping ahead a little, the mov instructions at addresses 0x444 and 0x448 set
%eax to the value saved at location %esp+12 (or 0x2A) and places 0x2A one spot
below the top of the stack (address %esp + 4, or 0x11c).
The next instruction (mov $0x80484f4, (%esp)) copies a constant value that is a memory address
to the top of the stack. This particular memory address, 0x80484f4, contains the string "x is %d\n".
The instruction pointer advances to the call to the printf function (which is denoted with
the label <printf@plt>).
For the sake of brevity, we will not trace the printf
function, which is part of stdio.h. However, we know from
the manual page (man -s3 printf) that printf has the following format:
int printf(const char * format, ...)
In other words, the first argument is a pointer to a string
specifying the format, and the second argument onward specify
the values that are used in that format. The instructions
specified by addresses 0x444 - 0x45c correspond to the following
line in the main function:
printf("x is %d\n", x);
When the printf function is called:
-
A return address specifying the instruction that executes after the call to
printfis pushed onto the stack. -
The value of
%ebpis pushed onto the stack, and%ebpis updated to point to the top of the stack, indicating the beginning of the stack frame forprintf.
At some point, printf references its arguments, which are the
string "x is %d\n" and the value 0x2A. Recall that the return address is located directly
below %ebp at location %ebp+4. The first argument is thus located at %ebp+8 (i.e.,
right below the return address), and the second argument is located at %ebp+12.
For any function with n arguments, GCC places the first argument at location %ebp+8,
the second at %ebp+12, and the nth argument at location (%ebp+8) + (4*(n-1)).
After the call to printf, the value 0x2A is output to the user
in integer format. Thus, the value 42 is printed to the screen!
After the call to printf, the last few instructions clean up the stack and prepare a clean
exit from the main function. First, the value 0x0 is placed in register %eax, signifying that
the value 0 is returned from main. Recall that a program returns 0 to indicate correct termination.
After leave and ret are executed, the stack and frame pointers revert to their original values prior
to the execution of main. With 0x0 in the return register %eax, the program returns 0.
If you have carefully read through this section, you should understand why our program prints out the value 42. In essence, the program inadvertently uses old values on the stack to cause it to behave in a way that we didn’t expect. While this example was pretty harmless, we discuss in future sections how hackers have misused function calls to make programs misbehave in truly malicious ways.