This article accompanies Lesson 3 “LDR, STR” of LaurieWired in the ARM assembly tutorial.

Documentation for ARM32 and ARM64 assembly

On https://developer.arm.com, search for “armasm user guide”. In the result list, one may find

Arm Compiler armasm User Guide. Version: 6.6.5. Recommended

Version 6.6.5 is the latest version as of writing. Click on it. The LDR and STR instructions are documented under the section “A64 Data Transfer Instructions”.

LDR (load register)

LDR reads a word (32-bit) at specified address. We will look at two versions of ldr.s that does the same thing.

version 1

.global _start

.text
_start:
    // load content of var2, the decimal 6
    ldr x0, var2
    mov x8, #0x5d
    svc #0

.data
// a word is 32-bit
// or 8-digit in heximal mode
var1: .word 5     // decimal 5
var2: .word 6     // decimal 6

The code in this article has to be linked with -static flag, or an error will be produced. To run it in the shell:

$ rm ldr.o; as -o ldr.o ldr.s
$ gcc -o ldr ldr.o -nostdlib -static
$ ./ldr; echo $?
6

If we dissemble it in the shell:

$ objdump -d ldr

ldr:     file format elf64-littleaarch64


Disassembly of section .text:

00000000004000b0 <_start>:
  4000b0:	58080080 	ldr	x0, 4100c0 <var2>
  4000b4:	d2800ba8 	mov	x8, #0x5d                  	// #93
  4000b8:	d4000001 	svc	#0x0

We suppose at the address 0x4100c0 is the number decimal 6. How to prove it?

We first print the symbol table in the executable ldr:

$ readelf -s ldr

Symbol table '.symtab' contains 15 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 00000000004000b0     0 SECTION LOCAL  DEFAULT    1 .text
     2: 00000000004100bc     0 SECTION LOCAL  DEFAULT    2 .data
     3: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS ldr.o
     4: 00000000004000b0     0 NOTYPE  LOCAL  DEFAULT    1 $x
     5: 00000000004100c0     0 NOTYPE  LOCAL  DEFAULT    2 var2
     6: 00000000004100bc     0 NOTYPE  LOCAL  DEFAULT    2 var1
     7: 00000000004100c4     0 NOTYPE  GLOBAL DEFAULT    2 _bss_end__
     8: 00000000004100c4     0 NOTYPE  GLOBAL DEFAULT    2 __bss_start__
     9: 00000000004100c4     0 NOTYPE  GLOBAL DEFAULT    2 __bss_end__
    10: 00000000004000b0     0 NOTYPE  GLOBAL DEFAULT    1 _start
    11: 00000000004100c4     0 NOTYPE  GLOBAL DEFAULT    2 __bss_start
    12: 00000000004100c8     0 NOTYPE  GLOBAL DEFAULT    2 __end__
    13: 00000000004100c4     0 NOTYPE  GLOBAL DEFAULT    2 _edata
    14: 00000000004100c8     0 NOTYPE  GLOBAL DEFAULT    2 _end

It reads var2 is at address 0x4100c0, in section 2 (under column Ndx); var1 is at address 0x4100bc, in the same section 2. We now print out the section 2, or the section .data of ldr:

$ readelf -x2 ldr         # or readelf -x .data ldr

Hex dump of section '.data':
  0x004100bc 05000000 06000000                   ........

We see at address 0x004100bc is some content 0x05000000, at the next address which would be 0x004100bc + 0x00000004 = 0x004100c0 is some content 0x06000000.

But 0x05000000 is many times larger than the decimal 5, or heximal 0x5? We have to talk about the order in which the processor records a number, “endianness”. If we search for “aarch64 little endian”, we may find:

AArch64, which is the 64-bit architecture for ARM, typically uses little-endian format by default. This means that the least significant byte is stored at the smallest memory address.

What it says is that from address 0x004100bc to 0x004100bf, to record the number 0x00000005, going from the most significant to the least significant byte (left to right), the four bytes 0x00, 0x00, 0x00, 0x05 would go into addresses

The Address Recorded Byte
0x004100bc 0x05
0x004100bd 0x00
0x004100be 0x00
0x004100bf 0x00

which explains starting from the address 0x004100bc, why the content looks to be 0x05000000.

version 2

We look at an variety of ldr.s:

.global _start

.text
_start:
    ldr x1, =var2
    ldr x0, [x1]
    mov x8, #0x5d
    svc #0

.data
var1: .word 5
var2: .word 6

It would still return decimal number 6:

$ rm ldr.o; as -o ldr.o ldr.s
$ gcc -o ldr ldr.o -nostdlib -static
$ ./ldr; echo $?
6

Let’s look at the dissembled ldr:

$ objdump -d ldr

ldr:     file format elf64-littleaarch64


Disassembly of section .text:

00000000004000b0 <_start>:
  4000b0:	58000081 	ldr	x1, 4000c0 <_start+0x10>
  4000b4:	f9400020 	ldr	x0, [x1]
  4000b8:	d2800ba8 	mov	x8, #0x5d                  	// #93
  4000bc:	d4000001 	svc	#0x0
  4000c0:	004100cc 	.word	0x004100cc
  4000c4:	00000000 	.word	0x00000000

The line ldr x1, 4000c0 would load the (32-bit) word at address 0x4000c0 into register x1. Several lines down, 4000c0: 004100cc: at 0x4000c0 is the word 0x004100cc, which will be read into x1.

The next line ldr x0, [x1] would read the word found at address 0x004100cc into x0. Let’s check if 0x004100cc is an address in the section .data.

$ readelf -x .data ldr

Hex dump of section '.data':
  0x004100c8 05000000 06000000                   ........

So at address 0x004100c8 is some content 0x05000000, at the next address which would be 0x004100c8 + 0x00000004 = 0x004100cc is some content 0x06000000. For what we explained about endianness, that would be heximal number 0x6, or decimal number 6.

conclusion

We see from the above code,

  • plain var2 refers to an address (or a “pointer” in C programming language’s jargon), at which the number 6 is stored;
  • =var2 is a pointer to the address at which number 6 is stored (“pointer to a pointer”).

STR (store register)

STR stores the content in a register to a specified address. The ARM64 code is not much different from the ARM32 one in Laurie’s lesson.

.global _start

.text
_start:
    ldr x2, =var1
    ldr x3, =var2
    mov x1, #5
    str x1, [x3]
    ldr x0, [x3]
    mov x8, #0x5d
    svc #0

.data
var1: .word 6
var2: .word 7

To run it,

$ rm str.o; as -o str.o str.s
$ gcc -o str str.o -nostdlib -static
$ ./str; echo $?
5