How Assembly Functions Work - The Stack Explained
(YouTube)
RISC-V Assembler: Jump and Function
confusion about function call stack
RISC-V assembly - stack layout - function call
Calling Functions in Assembly
1.1. RV32I Base Integer Instruction Set, Version 2.1
Stack Frame
Functions need to keep track of its local variables. Local variables
go in stack frames on the call stack. When a function is called it
moves the stack pointer down making room for the function's local
variables. When it returns it moves the stack pointer up.
A register is used to keep track of the address of the
current stack frame. According to the RISC-V calling convention, register sp (x2)
contains the address of the bottom (the smallest address) of the current stack
frame.
Code interacts with sp in three ways:
- When a function is called, it will move the sp downward to make space for
its stack frame. (The stack frame contains the function’s local variables.)
- During the execution of a function, it will use (positive) offsets from the
sp to locate its local variables. For example: ld a5, 4(sp) and
sd a6, 44(sp) to load and store local variables using offsets from the sp.
- Before a function returns it will adjusting sp back
to its pre-call value.
In this example addi at the beginning and end of a function "creates"
and "destroys" a stack frame of 8 bytes.
func_label:
addi sp, sp, -8
...
...
...
addi sp, sp, 8
ret
Code from
HERE
Print a String
Using the Standard Linux System Call (sys_write)
If you are compiling your assembly code directly on a RISC-V Linux
machine or executing via QEMU, you must utilize standard Linux ABI
system calls.
- System Call Number: Loaded into register a7 (e.g., 64 for sys_write)
- Arguments: Loaded sequentially into registers a0, a1, and a2.
Printing a String (sys_write)...
.data
msg: .ascii "Hello via Linux!\n"
len = . - msg # Calculate length of the string dynamically
.text
.global _start
_start:
li a7, 64 # Linux sys_write system call number
li a0, 1 # File descriptor: 1 is stdout
la a1, msg # Pointer to the string
li a2, len # Length of the string
ecall # Invoke system call
# Graceful exit (sys_exit)
li a7, 93 # Linux sys_exit system call number
li a0, 0 # Exit status code 0
ecall
Note: If your environment links against a C standard library (like glibc),
you can skip raw system calls and directly branch to printf.
Using the C standard printf...
.data
format: .asciiz "The answer is: %d\n"
.text
.global main
main:
# Save return address to stack
addi sp, sp, -16
sd ra, 0(sp)
la a0, format # 1st argument: Format string
li a1, 42 # 2nd argument: Integer replacement value
call printf # Invoke standard C printf
# Restore return address and exit
ld ra, 0(sp)
addi sp, sp, 16
li a0, 0 # Return 0
ret
Function Call Register Responsibilities
The hardware registers are divided into two primary categories during a
function call:
- Caller-Saved Registers (ra, a0–a7, t0–t6): The function being
called (callee) is free to overwrite these. If the calling function needs
these values after the call, it must manually push them onto the stack
before jumping.
- Callee-Saved Registers (sp, s0–s11): These must be preserved. If the
called function uses them, it must back them up to the stack and restore
them before returning.