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 returnret‘ 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.