Skip to main content
\(\newcommand{\doubler}[1]{2#1} \newcommand{\binary}{\texttt} \newcommand{\hex}{\texttt} \newcommand{\octal}{\texttt} \newcommand{\prog}{\texttt} \newcommand{\lt}{<} \newcommand{\gt}{>} \newcommand{\amp}{&} \)

Section10.3Stack Management In a Function

The protocol that specifies the interaction between functions needs to be followed very precisely, or the program will almost surely not work correctly. The usual result is a crash. In this section we look at how we can use the stack to ensure we follow the correct protocol.

The first issue to consider is how a called function can return to the calling function. A calling function uses the bl instruction to call a function, which places the return address in the lr (r13) register. The called function needs to preserve this address in order to know where to return to. It could simply avoid using this register, but that strategy fails if this function needs to call another function. The solution to this problem is to save the contents of the lr register on the stack.

Looking at Table 10.1.1, we see that the called function only has free use of registers r0r3. But you even need to treat these with care. If the calling function passed any arguments in these registers, the called function must make sure that it will no longer need the value before changing it in the register. It should be clear that most functions will use the stack quite a bit.

Now we can return to the program in Listing 10.1.4 and describe how the stack is managed in this function. The first two instructions:

stmfd   sp!, {fp, lr}
add     fp, sp, #4

set up a portion of the stack for use in this function. This acts as a prologue before performing the algorithm that is the purpose of this function.

The stmfd instruction at the beginning of this function:

  1. Pushes the caller's frame pointer, the value contained in the fp register, onto the stack.

  2. Pushes the return address, the value contained in the lr register, onto the stack.

  3. Updates the sp to show that two 32-bit values have been pushed onto the top of the stack.

The add instruction adds \(4\) to the value in the sp register and stores the sum in the fp register, thus setting the frame pointer for this function such that it points to the frame pointer of the calling function.

The frame pointer is used as a reference point within the area of the stack that the function is allowed to access, called the stack frame. This will be explained in Section 10.4.

After this function has performed its action, the stack needs to be restored to the state that the calling function was using. This is accomplished with the instruction:

ldmfd   sp!, {fp, pc}

So this instruction effectively pops the caller's frame pointer off the top of the stack, back into the fp register. Then the return address is popped into the pc, and the stack pointer, sp, is updated to the new top of the stack. This acts as an epilogue to clean up the stack after performing the algorithm that is the purpose of this function.

We now look at an assembly language version of this program so we can use gdb to observe how the stack actually changes. My assembly language is shown in Listing 10.3.1.

@ helloWorld2.s
@ Hello World program, in assembly language.
@ Bob Plantz - 5 August 2016

@ Define my Raspberry Pi
        .cpu    cortex-a53
        .fpu    neon-fp-armv8
        .syntax unified         @ modern syntax

@ Useful source code constant
        .equ    STDOUT,1

@ Constant program data
        .section  .rodata
        .align  2
helloMsg:
        .asciz	 "Hello, World!\n"
        .equ    helloLngth,.-helloMsg

@ Program code
        .text
        .align  2
        .global main
        .type   main, %function
main:
        stmfd   sp!, {fp, lr}   @ save caller's info
        add     fp, sp, 4       @ our frame pointer

        mov     r0, STDOUT      @ file number to write to
        ldr     r1, helloMsgAddr   @ pointer to message
        mov     r2, helloLngth  @ number of bytes to write
        bl      write           @ write the message
        
        mov     r0, 0           @ return 0;
        ldmfd   sp!, {fp, lr}   @ restore caller's info
        bx      lr              @ return
        
        .align  2
helloMsgAddr:
        .word   helloMsg
Listing10.3.1“Hello World” program using the write system call function (prog asm).

Before walking through this code with gdb, we see another assembler directive:

.equ    STDOUT,1

which defines the identifier, STDOUT, and gives it the value \(1\text{.}\) In the C version of this program we included the file unistd.h, which defined the identifier STDOUT_FILENO to be \(1\text{.}\) That's a C file, and we are using assembly language here, so we need to do this ourselves.

We can even direct the assembler to do some arithmetic for us:

helloMsg:
        .asciz  "Hello, World!\n"
        .equ    helloLngth,.-helloMsg

The assembler computes the value of this new identifier, helloLngth, as the arithmetic expression “.-helloMsg”. In this context the ‘.’ character means “here” with respect to the memory address. So this arithmetic expression subtracts the memory address of the helloMsg label from “here”, thus giving the length of the text string, including the NUL terminating character.

You can probably figure out that the assembler directive .asciz does the same thing as .ascii but adds the NUL character at the end of the text string.

I ran this program using gdb and set a breakpoint at the stmfd sp!, {fp, lr} instruction, and run the program:

(gdb) run
Starting program: /home/pi/bookProgs/chap10/helloWorld2 

Breakpoint 1, main () at helloWorld2.s:27
27	        add     fp, sp, 4

Notice that gdb did not break where I told it to, but at the next instruction. Remember that gdb is also a program. It also uses the stack, and it also needs to strictly observe the ageed-upon protocols. So it often needs to complete the stack manipulations of your program before it can switch control over to itself.

Inspecting the registers gave me (your numbers may vary):

(gdb) i r
r0             0x1	1
r1             0x7efff754	2130704212
r2             0x7efff75c	2130704220
r3             0x1041c	66588
r4             0x0	0
r5             0x0	0
r6             0x102f4	66292
r7             0x0	0
r8             0x0	0
r9             0x0	0
r10            0x76fff000	1996484608
r11            0x0	0
r12            0x76fa4000	1996111872
sp             0x7efff5f8	0x7efff5f8
lr             0x76e7f294	1994912404
pc             0x10420	0x10420 <main+4>
cpsr           0x60000010	1610612752

Looking at the stack:

(gdb) x/4xw 0x7efff5f8
0x7efff5f8:	0x00000000	0x76e7f294	0x76fa3000	0x7efff754

we see that the return address (in the lr register) has been pushed onto the stack. Let us rearrange the display to get a more intuitive view of the stack:

sp: \(\hex{0x7efff5f8:}\) \(\hex{0x00000000}\)
\(\hex{0x7efff5fc:}\) \(\hex{0x76e7f294}\)

Our function has pushed only two 32-bit values onto the stack, so this function's view of the stack ends with these two values. The value at the top of the stack is \(\hex{0x0}\) suggesting that the caller (the C runtime environment) has not set up a frame pointer. Regardless of what that code did, we must observe the agreed-upon protocol, so the next instruction sets up a frame pointer for our function. Executing this instruction gives us:

(gdb) si
29	        mov     r0, STDOUT     @ file number to write to
(gdb) i r fp lr sp pc
fp             0x7efff5fc	0x7efff5fc
lr             0x76e7e294	1994908308
sp             0x7efff5f8	0x7efff5f8
pc             0x10424	0x10424 <main+8>
(gdb) x/4xw 0x7efff5f8
0x7efff5f8:	0x00000000	0x76e7f294	0x76fa3000	0x7efff754

Now we have established a frame pointer, which serves as a reference point into the stack for this function. The area of the stack this function is allowed to access, as defined by the stack protocol, is called a stack frame. Although the frame pointer is not needed in this function, you will see its usefulness soon. Here is the view of the stack from this function:

sp: \(\hex{0x7efff5f8:}\) \(\hex{0x00000000}\)
fp: \(\hex{0x7efff5fc:}\) \(\hex{0x76e7f294}\)

I set a breakpoint at the bx instruction at the end of the program, continued, and checked that the stack was in the same state as at the end of the prologue:

(gdb) cont
Continuing.
Hello, World!

Breakpoint 2, main () at helloWorld2.s:36
36	        bx      lr              @ return
(gdb) i r fp lr sp pc
fp             0xfffffffc	0xfffffffc
lr             0x76e7e294	1994908308
sp             0x7efff600	0x7efff600
pc             0x1043c	0x1043c <main+32>
(gdb) x/4xw 0x7efff5f8
0x7efff5f8:	0x00000000	0x76e7f294	0x76fa3000	0x7efff754

The stack pointer, sp, and link register, lr, have been restored to their respective states when this function was entered, and the bx lr instruction transfers control back to the calling function.

Subsection10.3.1Exercises

1

Modify the assembly language program in Listing 10.3.1 so that it prints “Hello, yourName!” on the screen. Remember to change the documentation such that it accurately describes your program.

Solution