Skip to content

Stack and Procedure Calls ​

The stack is a memory region used for temporary data storage such as return addresses, local variables, and register values during subroutine calls. ARM relies on software conventions to manage the stack, typically using the stack pointer register (SP, or R13).

Stack ​

Stack Growth and Pointer Meaning ​

  • Ascending vs. Descending:

    • Ascending stack grows toward higher memory addresses; SP increases when pushing data.
    • Descending stack grows toward lower memory addresses; SP decreases when pushing data.
  • Full vs. Empty: This indicates what the stack pointer (SP) points to:

    • Full stack: SP points to the last stored item (top of stack).
    • Empty stack: SP points to the next free slot where data will be pushed.

ARM Cortex-M uses a Full Descending Stack — the stack grows downward, and SP points to the most recently stored item.

PUSH and POP Instructions ​

PUSH and POP let you efficiently save and restore multiple registers on the stack.

  • Register Order: Registers are stored on the stack in numerical order, with the lowest numbered register at the lowest address.

    asm
    PUSH {R3, R1, R2}

    Although written as {R3, R1, R2}, ARM pushes in the order R1, R2, then R3. The lowest register (R1) is stored at the lowest memory address on the stack, and the highest (R3) at the highest address.

Procedures (Subroutines) ​

What is a Procedure? ​

A Procedure (also called a subroutine or function) is a named block of code designed to perform a specific task. Procedures allow you to:

  • Organize code into reusable, modular units.
  • Avoid repetition by calling the same code from multiple places.
  • Simplify program structure and improve readability.

In ARM assembly, procedures are often marked using the assembler directives PROC and ENDP, which help the assembler identify the start and end of a procedure. Procedures are called using the BL (Branch with Link) instruction, which stores the return address in the link register (LR, R14).

asm
MyProc PROC
    ; Add 1 to R0
    ADD R0, R0, #1
    BX LR
ENDP

Calling a Subroutine ​

When you call a subroutine using BL label, the return address (the instruction immediately following the call) is automatically saved in the link register (LR, R14).

To return from the subroutine, you have two common options:

  • MOV PC, LR — moves the return address from LR into the program counter, causing a return to the caller.
  • Preferably: BX LR — branches to the address in LR, also handling a switch between ARM and Thumb instruction sets if necessary.

Important

Since the link register holds only one return address, if a subroutine itself calls another subroutine (nested calls), the original return address in LR will be overwritten. To avoid losing return information, the current value of LR should be saved on the stack (or another register) before making another subroutine call, and restored before returning.

Nested Subroutine Calls ​

When a subroutine calls another subroutine, the current value of LR (the return address) must be saved to avoid losing it. This is typically done by pushing LR onto the stack before making the nested call.

asm

outerFunction
    PUSH {LR}           ; Save return address
    BL innerFunction    ; Call innerFunction
    POP {PC}            ; Restore return address

innerFunction
    ; Do something
    BX LR               ; Return from innerFunction

Reset_Handler
    BL outerFunction
    ; Continue execution after outerFunction returns

ARM Procedure Call Standard (AAPCS) Summary ​

The ARM Architecture Procedure Call Standard (AAPCS) defines rules for function calls to ensure interoperability between different code modules and tools.

Parameter Passing

  • The first four integer or pointer parameters are passed in registers R0-R3.
  • Additional parameters are passed on the stack.

Return Values

  • The primary return value is returned in R0.
  • Larger or complex return values may use multiple registers or memory.

Register Usage

  • Caller-saved registers: R0-R3, R12, LR
    These may be overwritten by the called function, so the caller must save them if needed.

  • Callee-saved registers: R4-R11, SP (R13)
    The called function must preserve these if it uses them (typically by pushing/popping on the stack).

  • Stack Pointer (SP or R13):
    Points to the top of the stack. Must be preserved by the callee.

Examples ​

Example 1: Summation using a Procedure ​

This example demonstrates how to use a procedure to compute the sum of numbers from 1 => N.

asm
    AREA RESET, CODE, READONLY
    EXPORT __Vectors
__Vectors
    DCD 0x20001000
    DCD Reset_Handler
    ALIGN

N       DCD 5           ; Sum numbers 1 to 5
    AREA MYDATA, DATA, READWRITE
RESULT  DCD 0           ; Store sum result

    AREA MYCODE, CODE, READONLY
    ENTRY
    EXPORT Reset_Handler

; Procedure to compute sum 1+2+...+N (input in R0), returns sum in R0
SumUp PROC
    MOV R1, #0          ; R1 = accumulator
LoopSum
    ADD R1, R1, R0      ; Add current number
    SUBS R0, R0, #1     ; Decrement counter
    BGT LoopSum         ; Continue if R0 > 0
    MOV R0, R1          ; Move result to R0
    BX LR               ; Return
ENDP

Reset_Handler
    LDR R0, N
    BL SumUp            ; Call sum procedure

    LDR R1, =RESULT
    STR R0, [R1]        ; Store result (should be 15)
	LDR R2, [R1]
STOP B STOP
	END

Example 2: Sum of Array Elements After Modulo Operation ​

This example demonstrates nested subroutine calls in ARM assembly. It computes the sum of an array of integers after applying a modulo operation to each element.

asm
 AREA RESET, CODE, READONLY
    EXPORT __Vectors
__Vectors
    DCD 0x20001000             ; Initial Stack Pointer value
    DCD Reset_Handler          ; Reset handler address
    ALIGN

ARR     DCD 23, 25, 9, 11, 6, 90  ; Array of 6 integers
LEN     DCB 6                     ; Length of the array (6 elements)
N       DCB 3                     ; Modulus value (3)

    AREA MYDATA, DATA, READWRITE
RESULT  DCD 0                     ; Variable to store the final result (sum)

    AREA MYCODE, CODE, READONLY
    ENTRY
    EXPORT Reset_Handler

Reset_Handler
    LDR R0, =ARR                  ; R0 = address of array
    LDRB R1, LEN                  ; R1 = length of array
    LDRB R2, N                    ; R2 = modulus N

    BL SUM_MOD_N                  ; Call procedure: sum modulo elements

    LDR R3, =RESULT               ; R3 = address of RESULT
    STR R0, [R3]                  ; Store sum result in RESULT

    ; Infinite loop to halt here
STOP
    B STOP                        ; Loop forever

;-----------------------------------------------
; Procedure: SUM_MOD_N
; Sum elements of an integer array after applying modulo N to each element.
; Inputs:
;   R0 - pointer to start of array
;   R1 - number of elements
;   R2 - modulus value N
; Output:
;   R0 - sum of (array[i] % N)
;-----------------------------------------------
SUM_MOD_N
    PUSH {R4, R5, R6, LR}   ; Save registers
    MOV R4, #0              ; accumulator
    MOV R5, R0              ; pointer to array
    MOV R6, R1              ; save LEN

SUM_LOOP
    CBZ R6, SUM_MOD_N_EXIT
    LDR R3, [R5], #4        ; load element

    MOV R0, R3              ; dividend
    MOV R1, R2              ; divisor
    BL MOD_N

    ADD R4, R4, R0
    SUBS R6, R6, #1         ; decrement loop counter
    BNE SUM_LOOP

SUM_MOD_N_EXIT
    MOV R0, R4
    POP {R4, R5, R6, PC}

;-----------------------------------------------
; Procedure: MOD_N
; Compute remainder R0 = R0 % R1 (modulus operation).
; Inputs:
;   R0 - dividend
;   R1 - divisor
; Output:
;   R0 - remainder
;-----------------------------------------------
MOD_N
    PUSH {R4, LR}                ; Save callee-saved registers and LR

    UDIV R4, R0, R2             ; R4 = quotient = dividend / divisor
    MUL R4, R4, R2              ; R4 = quotient * divisor
    SUB R0, R0, R4              ; R0 = dividend - quotient * divisor (remainder)

    POP {R4, PC}                ; Restore R4 and return (pop PC = pop LR)

    END

Tasks ​

Task 1 - Count Vowels using Procedures ​

Extend the previous experiment to count the number of vowels in a string using procedures. Implement a procedure that takes a string pointer and returns the count of vowels in that string. The procedure should have the following signature:

asm
CountVowels PROC
    ; Input: R0 - pointer to string
    ; Output: R0 - number of vowels
ENDP

Preferably, the procedure should use nested procedures for character checking and counting.

Task 2 - Recursive Factorial Calculation ​

Implement a recursive procedure to calculate the factorial of a number. A factorial of n (n!) is the product of all positive integers less than or equal to n. The procedure should have the following signature:

asm
Factorial PROC
    ; Input: R0 - integer n
    ; Output: R0 - factorial of n
ENDP

The procedure should use a base case for n = 0 and a recursive case for n > 0 .