Assembly Procedures
In x86-64 assembly, a procedure is a named block of code that does not need to be called in order to execute.
Procedures are similar to functions, but are simpler and are most commonly to refactor assembly code, making it cleaner, more descriptive, and more efficient. Like functions, procedures can be called – but they don’t have to be.
In the following example, we create a procedure called ‘add_numbers’, which adds two numbers stored in the rax and rbx registers.
section .text
global _start
_start:
; Initialize variables
mov rax, 5 ; First number
mov rbx, 10 ; Second number
add_numbers:
; Procedure to add two numbers
add rax, rbx ; Add rbx to rax
After the ‘add_numbers’ procedure executes, the value of rax will be 15.
In this example, the add_numbers procedure executes without being called. The flow of program execution simply goes line by line through the program. The main benefit here of using the ‘add_numbers’ procedure is that it helps make the code cleaner.
However, we could leave out the procedure entirely and simply keep the ‘add rax, rbx’ instruction at the end of the program. It would execute the same way. If we want to get more out of our procedures, we should be calling them.
Calling a Procedure
In addition to being executed automatically as part of the normal line-by-line flow of the program, procedures can also be called from any point within the program.
Calling allows them to be reused as many times as needed. To call a procedure, we use the call instruction and the procedure name:
call <procedure_name>
The only caveat is that we also need a way for program execution to resume from the point at which it was called after the procedure is done executing.
This is done by placing the return ‘ret‘ instruction at the end of the procedure.
Let’s update our example so that it integrates the call and return instructions:
section .text
global _start
_start:
mov rax, 5
mov rbx, 10
; Call the add_numbers procedure
call add_numbers
add_numbers:
add rax, rbx
ret ; Return from procedure
Note that this program will not function as expected; we will see why and learn how to correct it below.
Optimizing Our Procedure
Let’s take this one step further and integrate the variable initialization section into its own procedure.
section .text
global _start
_start:
call init_variables
call add_numbers
init_variables:
mov rax, 5
mov rbx, 10
ret
add_numbers:
add rax, rbx
ret
Using call and return, we were able to make the code cleaner and easier to read.
How Procedure Calling Works
When we call a procedure, a few things happen so that program execution can work properly.
Specifically, the program needs to be able to 1) jump to the location of the procedure, and 2) jump back. We’ve learned that we can do this using the call and return instructions, respectively. But let’s see how this works under the hood.
When the procedure gets called, what the call instruction does is to increment and push the value of the instruction pointer (RIP) onto the stack. This is the memory location of the next instruction to be executed after the procedure.
Similarly, when the ret (return) instruction executes, it pops the value off the stack and stores it back into the instruction pointer (RIP). This returns program execution to the instruction following the original procedure call.
A Fully Working Example
The previous example using the call and return functions will not function properly as a standalone program because the execution flow goes through the procedures twice and does not exit.
This can be resolved cleanly by creating an ‘Exit’ procedure and calling it after the others:
section .text
global _start
_start:
call init_variables
call add_numbers
call Exit
init_variables:
mov rax, 5
mov rbx, 10
ret
add_numbers:
add rax, rbx
ret
Exit:
mov rax, 60
mov rdi, 0
syscall
The ‘Exit‘ procedure is like the others, but it uses a system call (syscall) to exit the program. This resolves the issues we had with the previous example. Unlike the other procedures,
While syscalls are covered in detail on a separate page, this example is simple enough to describe. The Exit syscall is numbered 60, so this value is first moved into the rax register. It also takes one integer argument (from rdi), which is the exit code. When the syscall instruction executes, the values in rax and rdi are evaluated and the program exits as a result.