ARM 64 Assembly Series — Offset and Addressing modes

+Ch0pin🕷️
6 min readJul 8, 2022

Lab Set up

Before we start exploring the AArch64’s instruction set, let us first set up our lab and run the traditional “Hello world”, just to make things a bit more interesting. Here is a handy script which I found here to help you setup your raspberry pi testing machine:

If everything went well, you should see the following:

If so, use your favourite editor to write the following code:

.data

msg:
.ascii "Hello, AArch64!\n"
len = . - msg

.text

.globl _start
_start:
// Prepare write(int fd, const void *buf, size_t count)
mov x0, #1
ldr x1, =msg
ldr x2, =len
mov w8, #64
svc #0

// Prepare exit(int status)
mov x0, #1337
mov w8, #93
svc #0

Compile with as helloWorld.S -o hw.o && ld hw.o -o hw and finally run it:

Or, even better, debug it:

Back to boring theory

The ARMv8 architecture defines two execution states, the AArch32 and AArch64 (on which these posts are focused). While AArch32 supports the thumb instruction set (in addition to A32), AArch64 supports only a single set called A64 which consists of a fixed-length 32 bit instructions. These instructions can be grouped according to their functionality of:

  • Loading and storing data
  • Changing the address of the next instruction to be executed (brunch instructions)
  • Performing arithmetic and logical operations (data processing)
  • Performing a special operation (special instructions).

Another concept which is important to know before we start exploring these groups is that AArch64 is a load-store architecture, which means that only load and store data instructions can access the memory while any computation instruction can only modify values that are contained in the registers. So, for example, to increase or add to a value stored to a memory address, then this value must first loaded to a register, modified and then stored back to the memory.

Immediate values

An immediate value is just a constant which can be defined by the developer. You may find them starting with 0, 0x, 0b or just an integer, which in the first case the starting 0 indicates an octal, the 0x a hex and the 0b a binary. When these are omitted, the immediate will be interpreted as decimal.

Offset Modes

The Load Register (LDR) and Store Register(STR) instructions can transfer bytes (8 bits), half-words (16 bits), words (32 bits) and double words (64 bits) from a memory address to a register (ldr)or a register to a memory address (str). To refer to a memory address the AArch64 instruction set defines the following offset modes:

  • Register Address: [Rn]
  • Immediate offset: [Rn, #val]
  • Register offset: [Rn, Rb]
  • Literal: label
  • Pseudo load: =<value or symbol>

Lets see each one of them using some easy examples:

Register Address: [Rn]

Example:

LDR R4, [R0] means R4 = *R0 (1)

STR R1, [R0] means *R0 = R1 (2)

For those who are familiar with C pointers, the correspondence between the brackets [] and C’s asterisk * is obvious. For those who are not, assume that R0 contains the memory address 0x0A, then the above instructions can be interpreted as follows:

- Load to register R4 the value which is stored at address 0x0A (1)

- Store at 0X0A the value which the R1 register contains (2)

Immediate offset: [Rn, #val]

Example:

LDR R4, [R0, #1] means R4 = *(R0+1) (1)

STR R1, [R0, #1] means *(R0+1) = R1 (2)

Similarly to previous case assume that R0 contains the memory address 0x0A, then the above instructions can be interpreted as follows:

- Load to register R4 the value which is stored at address 0x0A+1 = 0xB(1)

- Store at 0X0B the value which the R1 register contains (2)

Register offset: [Rn, Rb]

Example:

LDR R4, [R0, R1] means R4 = *(R0+R3)

Using a register as an offset is very similar to the immediate offset that we saw before, so there is no much to explain at this point. What might be a little bit confusing in this case, is the addition of a shifter:

The shifter (can also be found as barrel shifter) can be used to shift or rotate a register. Keep in mind that shifting a number a single position to the left (LSL #1) is like multiplying it with 2. For example the binary 0010 == 2 and when shifted once to the left it becomes 0100 == 4, when shifted twice it becomes 1000==8 and so on…

That being said, you may see the following:

LDR R4, [R1, R2, LSL #1]

which is like an instruction embedded to another instruction and means, load the value which is stored to the address R1+R2*2, to the R4 register. This type of addressing is typically used to access an array where R1 contains the address of the beginning of the array and R2 is an integer index. The shifting value depends on the size of a value in the array.

Literal: label

Example:

LDR R4, lbl_1 means R4 = *lbl_1 (1)

Where lbl_1 refers to a memory address of a value

Pseudo load: =<value or symbol>

Example:

LDR R4, =0x123means R4 = 0x123

LDR R4, =lbl means R4 = *lbl

Addressing Modes !

I haven’t add the exclamation mark just to track your attention. You probably notice that in all the examples above the register that is used as an offset, is not actually modified, for example after executing the instruction

LDR R4, [R0, #1]

the R4 will store the value of *(R0 + 1) but, the register R0 doesn’t change. Adding an exclamation mark at the end of the instruction will add 1 to R0, thus this instruction can be interpreted as follows:

LDR R4, [R0, #1]! means R4 = *(R0 +1) and then R0 = R0 + 1 // A concept similar to the prefix + + or — -in C

LDR R4, [R0], #1 means R4 = *(R0) and then R0 = R0 + 1// A concept similar to the postfix + + or — -in C

Hello gdb

Let’s see a simple C program:

void main()
{
int *p; //p is a pointer to an integer
int i = 10;
p = &i; //p is pointing to the address of i
(*p)++; //the value where p is pointing gets
//increased by 1
}

ssh to your raspberry pi, compile it (with gcc) and load it to gdb. Then type disas /r main:

A64 fixed-length 32 bit instructions

So, what we have here: Assume that the stack pointer (sp) points to 0x7ffffff410, then after +0, sp will point to 0x7ffffff400.Then, R0 (or w0 if you like) is set equal to 10 and its value is stored at 0x7ffffff404 . The address where 10 is saved is stored R0 at +12 and then at+16 is stored back to the memory at 0x7ffffff408 :

At +20 and +24 the 0x7ffffff404 is stored back to x0 and w0 will be set to 10. At +28 , w1 = 10 +1 and finally the 0x7ffffff404 will be overwritten with the new value from w1 (+36).

Examples

ldr x1, [x2]      means --> x1 = *x2
ldr x1, [x2, #3] means --> x1 = *(x2+3)
ldr x1, [x2, #3]! means --> x1 = *(x2+3), x2 += 3
ldr x1, [x2], #3 means --> x1 = *x2, x2 += 3
ldr x1, [x2, 3] same as --> ldr x1, [x2, #3]
str x1, [x2] means --> *x2 = x1
str x1, [x2, #3] means --> *(x2+3) = x1
str x1, [x2, #3]! means --> *(x2+3) = x1, x2 += 3
str x1, [x2], #3 means --> *x2 = x1, x2 += 3

[1] Bruce Dang, Alexandre Gazet, Elias Bachaalany, and Sbastien Josse. 2014. Practical Reverse Engineering: x86, x64, ARM, Windows Kernel, Reversing Tools, and Obfuscation (1st. ed.). Wiley Publishing.

--

--