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.
- Ascending stack grows toward higher memory addresses;
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.
- Full stack:
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.
asmPUSH {R3, R1, R2}
Although written as
{R3, R1, R2}
, ARM pushes in the orderR1
,R2
, thenR3
. 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)
.
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 fromLR
into the program counter, causing a return to the caller.- Preferably:
BX LR
— branches to the address inLR
, 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.
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
orR13
):
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
.
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.
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:
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:
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 .