Skip to content

ARM LDR literal and pseudo-instruction demos

In the previous article, we've combed AMR LDR PC-relative and Pseudo instruction to load a value or address to a register.

Here we collect some enlightening demos from some classic textbooks to consolidate knowledge that is not solid.

armasm User Guide#

Arm Compiler armasm User Guide | 7. Writing A32/T32 Assembly Language

  • 7.7 Load immediate values using LDR Rd, =const
  • 7.12 Load addresses to a register using LDR Rd, =label

LDR Rd,=const#

The LDR Rd,=const pseudo-instruction generates the most efficient single instruction to load any 32-bit number.

You can use this pseudo-instruction to generate constants that are out of range of the MOV and MVN instructions.

The LDR pseudo-instruction generates the most efficient single instruction for the specified immediate value:

  • If the immediate value can be constructed with a single MOV or MVN instruction, the assembler generates the appropriate instruction.
  • If the immediate value cannot be constructed with a single MOV or MVN instruction, the assembler:

    • Places the value in a literal pool (a portion of memory embedded in the code to hold constant values).
    • Generates an LDR instruction with a PC-relative address that reads the constant from the literal pool.

For example:

    LDR      rn, [pc, #offset to literal pool]
                          ; load register n with one word
                          ; from the address [pc + offset]

You must ensure that there is a literal pool within range of the LDR instruction generated by the assembler.

stack pointer initialized

STACK_BASE EQU 0x20000200
LDR sp, =STACK_BASE

SRAM_BASE EQU 0x40000000 ; start of RAM on LPC2132
LDR sp, =SRAM_BASE ; MOV r13, #SRAM_BASE

LDR Rd,=label#

The LDR Rd,=label pseudo-instruction places an address in a literal pool and then loads the address into a register.

The assembler converts an LDR Rd,=label pseudo-instruction by:

  • Placing the address of label in a literal pool (a portion of memory embedded in the code to hold constant values).
  • Generating a PC-relative LDR instruction that reads the address from the literal pool, for example:
LDR rn [pc, #offset_to_literal_pool]
                                    ; load register n with the address pc + offset

You must ensure that the literal pool is within range of the LDR pseudo-instruction that needs to access it.


Example of loading using LDR Rd, =label

The following example shows a section with two literal pools. The final LDR pseudo-instruction needs to access the second literal pool, but it is out of range. Uncommenting this line causes the assembler to generate an error.

The instructions listed in the comments are the A32 instructions generated by the assembler.

  1. By default, a literal pool is placed at every END directive.

    • Directive LTORG forces the assembler to build literal pool 1 between the two subroutines;
    • Literal Pool 2 is automatically inserted after the END directive;
  2. the first LDR r3 in func2 could use Literal Pool 1 to create a PC-relative offset, whereas the second LDR r4 couldn't share Literal Pool 1, it would place in Literal Pool 2.

  3. distance between LDR r4 and Literal Pool 2 is farther than 8000, overflow 4095.

; refer to ARM Assembly Language
;   6.3 LOADING CONSTANTS INTO REGISTERS, Example
;   6.5 LOADING ADDRESSES INTO REGISTERS, EXAMPLE 6.5

        AREA   LDRlabel, CODE, READONLY
        ENTRY                     ; Mark first instruction to execute
start
        BL     func1              ; Branch to first subroutine
        BL     func2              ; Branch to second subroutine
stop
        MOV    r0, #0x18          ; angel_SWIreason_ReportException
        LDR    r1, =0x20026       ; ADP_Stopped_ApplicationExit
        SVC    #0x123456          ; AArch32 semihosting (formerly SWI)
func1
        LDR    r0, =start         ; => LDR r0,[PC, #offset into Literal Pool 1]
        LDR    r1, =Darea + 12    ; => LDR r1,[PC, #offset into Literal Pool 1]
        LDR    r2, =Darea + 6000  ; => LDR r2,[PC, #offset into Literal Pool 1]
        BX     lr                 ; Return
        LTORG                     ; Literal Pool 1
func2
        LDR    r3, =Darea + 6000  ; => LDR r3,[PC, #offset into Literal Pool 1]
                                  ; (sharing with previous literal)
        ; LDR   r4, =Darea + 6004 ; If uncommented, produces an error because
                                  ; Literal Pool 2 is out of range.
        BX     lr                 ; Return
Darea   SPACE  8000               ; Starting at the current location, clears
                                  ; a 8000 byte area of memory to zero.
        END                       ; Literal Pool 2 is automatically inserted
                                  ; after the END directive.
                                  ; It is out of range of all the LDR
                                  ; pseudo-instructions in this example.

Example of string copy

The example also shows how, unlike the ADR and ADRL pseudo-instructions, you can use the LDR pseudo-instruction with labels that are outside the current section. The assembler places a relocation directive in the object code when the source file is assembled. The relocation directive instructs the linker to resolve the address at link time. The address remains valid wherever the linker places the section containing the LDR and the literal pool.

; refer to ARM Assembly Language, 5.4 OPERAND ADDRESSING, EXAMPLE 5.4

        AREA    StrCopy, CODE, READONLY
        ENTRY                       ; Mark first instruction to execute
start
        LDR     r1, =srcstr         ; Pointer to first string
        LDR     r0, =dststr         ; Pointer to second string
        BL      strcopy             ; Call subroutine to do copy
stop
        MOV     r0, #0x18           ; angel_SWIreason_ReportException
        LDR     r1, =0x20026        ; ADP_Stopped_ApplicationExit
        SVC     #0x123456           ; AArch32 semihosting (formerly SWI)
strcopy
        LDRB    r2, [r1],#1         ; Load byte and update address
        STRB    r2, [r0],#1         ; Store byte and update address
        CMP     r2, #0              ; Check for zero terminator
        BNE     strcopy             ; Keep going if not
        MOV     pc,lr               ; Return
        AREA    Strings, DATA, READWRITE
srcstr  DCB     "First string - source",0
dststr  DCB     "Second string - destination",0
        END

classic textbooks#

Pseudo load#

ARM 64-Bit Assembly Language | 3 Load/store and branch instructions - 3.3 Instruction components - 3.3.3 Addressing modes

The pseudo addressing mode allows an immediate data value or the address of a label to be loaded into a register, and may result in the assembler generating more than one instruction.

Pseudo load: =<immediate|symbol>

This is a pseudo-instruction. The assembler will generate a mov instruction if possible. Otherwise it will store the value of immediate or the address of symbol in a “literal pool”, or “literal table”, and generate a load instruction, using one of the previous addressing modes, to load the value into a register. This addressing mode can only be used with the ldr instruction.

An example pseudo-instruction and its disassembly are shown in Listing 3.1 and Listing 3.2.

Listing 3.1 LDR pseudo-instruction.

.text
ldr x0, =0x123456789abcdef0
ret

Listing 3.2 Disassembly of LDR pseudo-instruction

// little endian
0: 58000040 ldr x0, 8 <.text+0x8>
4: d65f03c0 ret
8: 9abcdef0 .word 0x9abcdef0
c: 12345678 .word 0x12345678

PC Relative Addressing#

Programming with 64-Bit ARM Assembly Language | Chapter 5: Thanks for the Memories - Loading a Register with an Address - PC Relative Addressing

PC relative addressing has one more trick up its sleeve; it gives us a way to load any 64-bit quantity into a register in only one instruction, for example, consider

LDR X1, =0x1234ABCD1234ABCD

This assembles into

ldr X1, #8
// here missed an instruction ?
.quad 0x1234abcd1234abcd

The GNU Assembler is helping us out by putting the constant we want into memory, then creating a PC relative instruction to load it.

For PC relative addressing, it really becomes addressing relative to the current instruction. In the preceding example, “ldr X1, #8” means 8 words(instructions) from the current instruction.

In fact, this is how the Assembler handles all data labels. When we specified

LDR X1, =helloworld

the Assembler did the same thing; it created the address of the hellostring in memory and then loaded the contents of that memory location, not the helloworld string.

These constants the Assembler creates are placed at the end of the .text section which is where the Assembly instructions go, not in the .data section. This makes them read-only in normal circumstances, so they can't be modified. Any data that you want to modify should go in a .data section.

Literal (PC-Relative) Addressing#

Arm Assembly Internals and Reverse Engineering | Chapter 6 Memory Access Instructions - Addressing Modes and Offset Forms - Literal (PC-Relative) Addressing - Loading Constants

LDR can also load a constant value or the address of a label using the specialized syntax LDR Rn,=value. This syntax is also useful for cases when you write assembly and a constant cannot be directly encoded into a MOV instruction.

; A32
_start:
    ldr r0, =0x55555555 // Set r0 to 0x55555555
    ldr r1, =_start     // Set r1 to address of _start

; A64
_start:
    ldr x1, =0xaabbccdd99887766 // Set x1 to 0xaabbccdd99887766
    ldr x2, =_start             // Set x2 to address of _start

This syntax is a directive to the assembler to place the constant in a nearby literal pool and to translate the instruction into a PC-relative load of this constant at runtime, as you can see in this disassembly output:

Disassembly of section .text:

0000000000400078 <_start>:
    400078:     58000041    ldr         X1, 400080 <_start+0x8>
    40007C:     58000062    ldr         x2, 400088 <_start+0x10>
    400080:     99887766    .word       0х99887766 // <literal pool>
    400084:     aabbccdd    .word       0xaabbccdd
    400088:     00400078    .word       0х00400078
    40008C:     00000000    .word       0x00000000 // </literal pool>

The assembler groups and deduplicates the constants in the literal pool and writes them at the end of the section, or “spills” them explicitly when it encounters an LTORG directive in the assembly file.

Literal pools cannot be placed anywhere in memory; they must be close to the instruction using it. How close, and the direction, depends on the instruction and architecture using it, given in Table 6.15.

Table 6.15: LDR Literal Pool Locality Requirements

INSTRUCTION SET INSTRUCTION LITERAL POOL LOCALITY REQUIREMENT
A32 LDR PC ± 4KB
T32 LDR.W PC ± 4KB
idem LDR (16-bit) Within 1KB strictly forwards from PC
A64 LDR PC ± 1MB

By default, an assembler will try to rewrite literal loads into an equivalent MOV or MVN instruction. A PC-relative LDR instruction will be used only if this is not possible.

LDR load address and value#

Modern Arm Assembly Language Programming: Covers Armv8-A 32-bit, 64-bit, and SIMD | Chapter 11: Armv8-64 Core Programming – Part 1 - Integer Operations - Load and Store Instructions

Listing 11-4 shows the source code for example Ch11_04, which explains how to use the LDR (load register) instruction. It also illustrates the use of several Armv8-64 memory addressing modes.

Ch11_04.cpp + Ch11_04.s
//------------------------------------------------
// Ch11_04.cpp
//------------------------------------------------
#include <iostream>
using namespace std;

extern "C" int TestLDR1_(unsigned int i, unsigned long j);
extern "C" long TestLDR2_(unsigned int i, unsigned long j);
extern "C" short TestLDR3_(unsigned int i, unsigned long j);

void TestLDR1(void)
{
    const char nl = '\n';

    unsigned int i = 3;
    unsigned long j = 6;
    int test_ldr1 = TestLDR1_(i, j);
    cout << "TestLDR1_(" << i << ", " << j << ") = " << test_ldr1 << nl;
}

void TestLDR2(void)
{

    const char nl = '\n';

    unsigned int i = 2;
    unsigned long j = 7;
    long test_ldr2 = TestLDR2_(i, j);
    cout << "TestLDR2_(" << i << ", " << j << ") = " << test_ldr2 << nl;
}

void TestLDR3(void)
{

    const char nl = '\n';

    unsigned int i = 5;
    unsigned long j = 1;
    short test_ldr3 = TestLDR3_(i, j);
    cout << "TestLDR3_(" << i << ", " << j << ") = " << test_ldr3 << nl;
}

int main(int argc, char **argv)
{
    TestLDR1();
    TestLDR2();
    TestLDR3();

    return 0;
}
//------------------------------------------------
// Ch11_04_.s
//------------------------------------------------

// Test arrays
        .data
A1:     .word 1, 2, 3, 4, 5, 6, 7, 8
A2:     .quad 10, -20, 30, -40, 50, -60, 70, -80

        .text
A3:     .short 100, 200, -300, 400, 500, -600, 700, 800

// extern "C" int TestLDR1_(unsigned int i, unsigned long j);

        .global TestLDRI_
TestLDR1_:
        ldr x2, = A1                // x2 = ptr to A1

        ldr w3, [x2, w0, uxtw 2]    // w3 = A1[i]
        ldr w4, [x2, x1, lsl 2]     // w4 = A1[j]

        add w0, w3, w4              // w0 = A1[i] + A1[j]
        ret

// extern "C" long TestLDR2_(unsigned int i, unsigned long j);

        .global TestLDR2_
TestLDR2_:
        ldr x2, = A2 // x2 = ptr to A2

        ldr x3, [x2, w0, uxtw 3]    // x3 = A2[i]
        ldr x4, [x2, x1, lsl 3]     // x4 = A2[j]

        add x0, x3, x4 // w0 = A2[i] + A2[j]
        ret

// extern "C" short TestLDR3_(unsigned int i, unsigned long j);

        .global TestLDR3_
TestLDR3_:
        adr x2, A3 // x2 = ptr to A3
        ldrsh w3, [x2, w0, uxtw 1]  // w3 = A3[i]
        ldrsh w4, [x2, x1, lsl 1]   // w4 = A3[j]

        add wO, w3, w4 // w0 = A3[i] + A3[j]
        ret

The assembly language function TestLDR1_ begins its execution with a ldr x2,=A1 that loads the address array A1 into register X2. Recall from the discussions in Chapter 2 that this form of the LDR instruction is a pseudo instruction. The assembler replaces the ldr x2,=A2 instruction with a ldr x2,offset instruction that loads the address of array A1 from a literal pool. Figure 11-1 illustrates this in greater detail. This figure contains output from the GNU debugger (with minor edits to improve readability) that shows the machine code for the TestLDRx_ functions. Note that the GNU debugger output displays runtime addresses for the LDR pseudo instructions instead of the offsets that are embedded in the instruction encodings.

Figure 11-1. Machine code for TestLDRx_ functions
Dump of .text section
    0x0000aaaaaaaaae80 <A3>:              100 200 -300 400 500 -600 700 800

    0x0000aaaaaaaaae90 <TestLDR1_+0>      02 02 00 58 ldr x2, 0xaaaaaaaaaed0    // ldr x2, = A1
    0x0000aaaaaaaaae94 <TestLDR1_+4>      43 58 60 b8 ldr w3, [x2, w0, uxtw #2]
    0x0000aaaaaaaaae98 <TestLDR1_+8>:     44 78 61 b8 ldr w4, [x2, x1, lsl #2]
    0x0000aaaaaaaaae9c <TestLDR1_+12>:    60 00 04 0b add w0, w3, w4
    0x0000aaaaaaaaaea0 <TestLDR1_+16>:    c0 03 5f d6 ret
    0x000baaaaaaaaaea4 <TestLDR2_+0>:     a2 01 00 58 ldr x2, 0xaaaaaaaaaed8    // ldr x2, = A2
    0x0000aaaaaaaaaea8 <TestLDR2_+4>:     43 58 60 f8 ldr x3, [x2, w0, uxtw #3]
    0x0000aaaaaaaaaeac <TestLDR2_+8>:     44 78 61 f8 ldr x4, [x2, x1, lsl #3]
    0x0000aaaaaaaaaeb0 <TestLDR2_+12>:    60 00 04 8b add x0, x3, x4
    0x0000aaaaaaaaaeb4 <TestLDR2_+16>:    c0 03 5f d6 ret
    0x0000aaaaaaaaaeb8 <TestLDR3_+0>:     42 fe ff 10 adr x2, 0xaaaaaaaaae80 <- PC relative address (A3)
    0x0000aaaaaaaaaebc <TestLDR3_+4>:     43 58 e0 78 ldrsh w3, [x2, w0, uxtw #1]
    0x0000aaaaaaaaaec0 <TestLDR3_+8>:     44 78 e1 78 ldrsh w4, [x2, x1, 1s1 #1]
    0x0000aaaaaaaaaec4 <TestLDR3_+12>:    60 00 04 0b add w0, w3, w4
    0x0000aaaaaaaaaec8 <TestLDR3_+16>:    03 5f d6 ret
    0x0000aaaaaaaaaecc <TestLDR3_+20>:    00 00 00 00
    0x0000aaaaaaaaaed0 <TestLDR3_+24>:    10 c0 ab aa aa aa 00 00 <— Literal pool address (A1)
    0x0000aaaaaaaaaed8 <TestLDR3_+32>:    30 c0 ab aa aa aa 00 00 <— Literal pool address (A2)

Dump of .data section
    0x0000aaaaaaabc010 <A1>:              1 2 3 4
    0x0000aaaaaaabc020 <A1+16>:           5 6 7 8
    0x0000aaaaaaabc030 <A2>:              10 -20
    0x0000aaaaaaabc040 <A2+16>            30 -40
    0x0000aaaaaaabc050 <A2+32>            50 -60
    0x0000aaaaaaabc060 <A2+48>            70 -80

Function TestLDR3_ uses an adr x2,A3 (form PC relative address) instruction to load the address of array A3 into register X2. An adr instruction can be used here since array A3 is defined in the same .text section (just before function TestLDR1_) as the executable code. Note that in Figure 11-1, there is no literal pool entry for array A3 since the ADR instruction uses PC relative offsets instead of literal pools.


Copyright clarification

Copyright belongs to the original author. 🫡
Excerpt/quotation for study only, non-commercial.

Comments