More on registers: the ABI
Past lessons in this series:
- Reversing 101 - introduction
- Preparing to Reverse
- Introduction to registers
Lesson Objective
In this lesson we will continue our walk through the registers, and we formalise the discussion around
movandbl.Setup
Not strictly required, but having access to a Linux machine will help with the exercises.
Reviewing the code
Last time we worked with this code
.global _main
.extern _exit
_main:
mov X0, #0x1723 // exit code
bl _exit // invokes exit
If you did not do it yet, you should compile and run it. The instructions are in the previous lesson.
If I were new to assembly, my first question would be
“why are we moving the value 0x1723 into X0?”
(We’ll cover the mov instruction with greater detail soon)
The Reverser approach Run the program, observe what happens, and form a hypothesis to test.
Running it gives:
gabrielebiondo@RevEng3 binaries_to_test % ./ASM_o_DETH
gabrielebiondo@RevEng3 binaries_to_test % echo $?
35
So ASM_o_DETH exits with the exit code 35. Weird? No, not really.
Observe that
0x1723is two bytes.- The least significant one is
0x23- here’s where the 35 could originate, and
$?returns only a byte. Makes sense.
To validate this hypothesis, change 0x1723 to something simple like 0, 1, or 4. If echo $? reflects those values, your hypothesis is almost certainly correct. (Spoiler: it is. This is how the shell behaves.)
So, if _exit was an ordinary function (and it is, but this is another story), the effect of doing mov X0, something is like invoking _exit(something).
Then why moving a value in X0. Enter the ABI!
The ABI
The acronym ABI stands for Application Binary Interface and represents the fundamental contract between the compiler, the operating system, the CPU/circuitry, and the code itself.
This contract dictates how data, “functions”, stack, registers, memory, and “calls” must be organised at the byte level.
Two remarks:
- the word “call” here must be intended in its broader sense: anything that transfers control to another address under a defined convention;
- the quotation marks are intentional. In a few lessons you’ll see why “function” and “call” are not what high-level programmers think they are.
The PCS (Procedure Call Standard) is the part of the ABI that defines how a “call” works at the binary level: where arguments go, where return values appear, which registers a callee may destroy, and how the stack must be managed. In the ARM world, this is called AAPCS64.
When we talk about “passing arguments to a function”, we are really talking about moving data. In mature languages such as C or Rust, you know that sometimes you pass a copy of a value, and sometimes you pass the address of the memory cell that holds it.
If you come from Python or similarly abstracted environments (the ones I don’t mention because I don’t care), you’re effectively always passing copies.
In our previous lesson we observed that operations always happen in registers, not in memory, so to implement the functionality of “routine calling”, there must be a standardised way to stuff the values in some registers. This operation must always be self-consistent, otherwise we’d lose the deterministic aspect of the concept of algorithm.
*As here we talk about Apple, if you are wondering…: Languages such as C, Rust, Swift, and Objective-C all follow the same AArch64 ABI rules on Apple Silicon. They compile down to the same calling convention: arguments go into X0–X7, the return value appears in X0, and anything larger or more complex is handled through the standard ABI mechanisms (stack, indirect result pointers, or aggregates).
Even languages used in cross-platform mobile development — such as Kotlin when compiled through the LLVM toolchain (for example with Kotlin/Native) — ultimately obey the exact same ABI.
Different syntax, different semantics, different runtimes — same binary contract.
*None of these languages behave like Python at the machine level. When the compiler finishes its work, the ABI is the law, and they all comply with it.**
So, we have defined the problem: there must be a deterministic, universally-agreed way to pass arguments, return values, and manage control flow at the binary level.
The ABI — and, on AArch64, the AAPCS64 — is the formal solution to that problem.
Input values
According to AAPCS64, the input values of a “function” must be placed in the first eight registers: the first argument goes into X0, the second into X1, and so on up to X7.
If the function takes more than eight arguments, the remaining ones are passed on the stack. These stack arguments follow strict rules (16-byte alignment, stack growth direction, and other constraints). We will look at these details later, when we introduce routines properly.
Return values
If the routine has return values:
- for a single return value, it must be placed in
X0; - if there are multiple return values:
- the second may be returned in
X1; X2can be used in some very specific cases (mainly for aggregated data);- in all other cases, the ABI requires using a memory buffer referenced by
X0(the so-calledsretmechanism).
- the second may be returned in
As with the input rules, we will cover these details properly when we deal with routines.
The Indirect Result Location Register
X8 is a special-purpose register known as the Indirect Result Location Register.
When a routine must return a complex structure whose size exceeds what fits in X0/X1, the caller places in X8 a pointer to a memory buffer where the routine will write the result.
The underlying idea is to avoid polluting X0, which is supposed to hold the “actual” return value.
Interestingly, in a Linux environment, X8 also serves as the syscall number register: it contains the identifier of the syscall that is about to be invoked.
Registers - key takeaways
- Parts of the CPU.
- Fast circuitry where all meaningful operations occor.
- Abiding to the ABI contract:
- Following the AAPCS64 protocol to invoke routines;
- Following the AAPCS64 protocol to store return values.
The instructions
So far, we have seen only two instructions, mov and bl, and look at where we are now: we can write a program that actually exits!
Don’t take this for granted - it’s not that easy! on my Patreon, there is a wider discussion on this, by the way.
Moving data within registers
Time to see the mov instruction. In its simplest form, the mov instruction has the following syntax:
mov DESTINATION_REGISTER #IMMEDIATE_VALUE
This form moves an immediate value directly into a register — exactly what we did when placing 0x1723 into X0.
The hash symbol (#) indicates that the operand is a constant (an immediate), and the 0x prefix tells us that the value is written in hexadecimal.
Branch with Link
The bl instruction stands for Branch with Link.
Its job is twofold:
- it transfers control to another address (or symbol),
- and it records where execution must return once that routine is finished.
The basic syntax is:
bl TARGET
where TARGET is usually a symbol representing the start of a routine.
This is the low-level mechanism that high-level languages implement as a “function call”.
When the routine completes, execution flows back to the point of the original bl.
For now, this mental model is enough: bl jumps away and guarantees that the program will return.
We will see how the return address is stored, and what really happens inside a routine, in the upcoming lessons.
The Code
I promised I would have shown the same things on Linux, so here we go. Try to see if the following code makes sense — it should not be a challenge:
.global _start
_start:
// exit code
mov x0, #0x1723 // same value as on macOS;
// shell will see 0x23 = 35
// syscall: exit
mov x8, #93 // SYS_exit on Linux AArch64
svc #0
to compile it:
as -o asm_o_deth_linux.o asm_o_deth_linux.s
ld -o ASM_o_DETH asm_o_deth_linux.o
./ASM_o_DETH
echo $?
or if you prefer to see the whole process:
Further Reading
- https://github.com/ARM-software/abi-aa/releases
- https://github.com/ARM-software/abi-aa/blob/c51addc3dc03e73a016a1e4edf25440bcac76431/aapcs32/aapcs32.rst
- https://developer.arm.com/documentation/ddi0597/2025-09/Base-Instructions/MOV–MOVS–immediate—Move–immediate–
- https://developer.arm.com/documentation/dui0801/l/A32-and-T32-Instructions/BL–A32-
Next Lesson
In the next lesson I will show you why I told you not to take for granted the possibility to exit from a program, we’ll dive more deep into the magic of mov and we’ll finally introduce the friend of long sleepless nights: the debugger!
Want the deep dive?
If you’re a security researcher, incident responder, or part of a defensive team and you need the full technical details (labs, YARA sketches, telemetry tricks), email me at info@bytearchitect.io or DM me on X (@reveng3_org). I review legit requests personally and will share private analysis and artefacts to verified contacts only.
Prefer privacy-first contact? Tell me in the first message and I’ll share a PGP key.
Subscribe to The Byte Architect mailing list for release alerts and exclusive follow-ups.
Gabriel(e) Biondo
ByteArchitect · RevEng3 · Rusted Pieces · Sabbath Stones