Lesson 15 of 28

Function Pointers

Indirect Branching and Function Pointers

So far, every branch target has been a fixed label known at assembly time. But what if you want to choose which function to call at runtime? This is what indirect branching is for.

BR -- Branch to Register

BR Xn jumps to the address stored in register Xn:

LDR X9, =my_label    // Load address of a code label
BR X9                 // Jump to that address

Unlike BL, BR does not save a return address in LR. It is a plain jump, like a goto. This makes it ideal for implementing jump tables and dispatch patterns.

Function pointers are the Changelings of assembly -- like the Founders from Deep Space Nine, they can take any form. A single register can point to any function, and you only discover which one at runtime.

Why Indirect Branching Matters

At the hardware level, indirect branching is how the CPU implements:

  • Switch statements: The compiler generates a jump table of addresses
  • Virtual method dispatch: C++ vtables are arrays of function pointers
  • Callbacks: Passing functions as arguments to other functions
  • Dynamic linking: Shared libraries are resolved via indirect jumps

The Jump Table Pattern

A jump table is a classic use of indirect branching. Instead of a chain of if/else branches, you compute the address of the target code and jump directly to it:

// Each handler ends with "B done" to rejoin common code
handler_0:
    // ... handle case 0 ...
    B done
handler_1:
    // ... handle case 1 ...
    B done
handler_2:
    // ... handle case 2 ...
    B done

You select a handler by loading its address and jumping:

LDR X9, =handler_1   // select handler 1
BR X9                 // jump to it

Building a Calculator Dispatch

Here is a simple calculator pattern. Each operation computes a result and jumps to the print section:

op_add:
    ADD X10, X0, X1
    B print_result
op_sub:
    SUB X10, X0, X1
    B print_result
op_mul:
    MUL X10, X0, X1
    B print_result

_start:
    MOV X0, #6
    MOV X1, #7
    LDR X9, =op_mul   // select multiplication
    BR X9              // dispatch!

print_result:
    // convert X10 to ASCII and print

The key insight is that _start does not need to know which operation runs -- it just loads an address and jumps. Changing the dispatched function is a single-line change.

Your Task

Write three operation blocks: op_add (computes X0 + X1), op_sub (computes X0 - X1), and op_mul (computes X0 * X1). Each should store the result in X10 and branch to print_result. In _start, load arguments 6 and 7, then dispatch to op_mul using BR. Print the result (42) followed by a newline.

ARM64 runtime loading...
Loading...
Click "Run" to execute your code.