This page introduces functions and their basic structure.
Functions, also called procedures or subroutines, begin with a label. The last
instruction executed by a function is a return statement. Typically this return
statement is the last instruction that appears in the code of the function, but
that is not a requirement as it may also appear in the middle of the function
code. A return statement transfers the control of execution back to the caller
and uses the return address register ra
to specify the address where execution
should return to. In RISC-V there are multiple instructions that can be used for
this return statement: jalr
/jal
/j
and ret
.
Important: The execution of a function cannot end with a branch or jump to a label. Also, a function should not end by terminating the program. It must return to its caller.
A function is a "leaf function" when it does not call any other functions. The execution of a program through varios functions can be represented by a call graph -- in general it is not a tree because it contain cycles. Any node in this graph that does not contain an outgoing edge is a leaf of the graph.
A function starts with a label:
foo:
...
This is just a regular label, but in the syntax of our program it represents a
function. What makes it a function is the fact that this label is used as the
target of a function call instrution jal
. The code of the function itself is
likely to contain more labels. These labels are not interpreted as the start of
a function because they are never the target of a jal
instruction.
foo:
# Content of foo
_fooLabel1:
# Internal label of foo
jalr zero, ra, 0
Consider prefixing label names differently so its easier to tell them apart from function labels. These non-function labels can also be indented but it is common to leave them un-indented.
The instruction jal
is the jump-and-link instruction. It is used to call a
function. Here is an example where a function foo
calls another function
called bar
after setting the parameters for bar
to be a0=0
and a1=1
:
foo1:
addi a0, zero, 0 # a0 <- 0
addi a1, zero, 1 # a1 <- 1
jal ra bar # Call bar(0, 1)
add s0, zero, a0 # s0 <- a0
In this example, the return address for this call for the function bar is the
address of the add s0, zero, a0
instruction because this is the instruction
that must be executed immediately after the execution of bar
. The word zero
represents the register zero that always contains the value zero. An addi
instruction is an add immediate instruction that adds a constant to a register.
For more information on passing in arguments as seen in this example with setting
a0=0
and a1=1
, see page 02b - Argument and Return Values
.
A label may appear in the assembly code even when it is not preceded by an instruction that transfer control flow -- such as a branch or a jump. Consider this example:
foo2:
# Initialize t0.
add t0, zero, zero # t0 <- 0
bne a0, zero, _skip # if a0 == 0, skip initialization of t1
add t1, zero, zero # t1 <- 0
_skip:
sub t2, a1, t0
In the above example if there there is no branch or jump instruction between
the add t1, zero, zero
instrucion and the label _skip
, therefore the execution
"falls through" to the sub
instruction.
The instruction jalr zero, ra, 0
returns execution to the instruction immediately
after the function call.
There are alternative instructions that can be used in its place. In layman's terms, the instruction is telling the processor to "go back to where this function was called". That is, return back to the function that called this one.
It is often a good idea to give the above instruction its own label and limit the exit points in the routine to just a few, if not just one. If the function is short it may not be necessary though. This "exit label" also gives the programmer a place to do any last-minute tasks before leaving. Below is a code snippet of a function with an exit label:
bar:
# bar code...
addi t0, zero, 31 # t0 <- 31
_barLabel:
# Also showing the indenting of a label inside of a routine.
# This is ok style but not the usual convention.
addi t0, zero, 30 # t0 <- 30
# Say we wanted to exit here. Instead of having another exit point, we
# can just jump to _barDone.
j _barDone
# Another super useful command.
sub t0, zero, t0 # t0 <- 0 - t0
# And if we didn't jump to _barDone earlier this falls through and goes to it
# anyways, which is what we want.
_barDone:
# At this point we are done and want to return.
jalr zero, ra, 0