load single byte from label to register in arm64 using GAS

Viewed 658

I would like to use the ldrb instruction to load a single byte from memory into a register. However, this does not seem possible if the 2nd operand is a label. A complete minimal reproducible commented example:

// GNU Assembly, aarch64 Linux

.data

.equ SYS_EXIT, 93
.equ SUCCESS, 0

CHAR:
    .byte 1

.text

.global _start

_start:
    // none of these work, but why?
    // ldrb w19, CHAR       // invalid addressing mode at operand 2
    // ldrb w19, =CHAR      // invalid addressing mode at operand 2
    // ldrb w19, [CHAR]     // 64-bit integer or SP register expected at operand 2
    // ldr w19, CHAR        // loads 4 bytes instead of 1 byte

    // I have to do this but it's verbose and clunky
    ldr x20, =CHAR
    ldrb w19, [x20]
    // is there any way to coalesce the above 2 instructions into 1?

    mov x8, SYS_EXIT
    mov x0, SUCCESS
    svc 0

Ideally I'd like to write ldrb <reg>, <label>, e.g. ldrb w19, CHAR, and have it just load a single byte from CHAR's memory address like I would expect, instead of throwing an assembler error.

2 Answers

As Siguza says, the CPU simply doesn't have any such instruction. The only addressing modes supported by LDRB are register base + register-or-immediate offset, as you can see in the architecture reference manual (required reading for any assembly programmer). The assembler is quite rightly refusing to assemble a nonexistent instruction.

So you can't achieve what you want in a single instruction; you need two. However, those two instructions can be more efficient than what you've chosen. The standard idiom used by compilers seems to be:

adrp x19, CHAR
ldrb w19, [x19, #:lo12:CHAR]

Here adrp will load x19 with the top 52 bits of the address of CHAR; the instruction encodes the offset of the page on which CHAR is located, relative to the page of the current instruction (this offset is computed and filled in by the linker or loader), so that it produces position-independent code. Then the low 12 bits of the address are added via the offset in the addressing mode (which is limited to 12 bits); this offset is known at link time, since load-time relocation happens only in units of pages or more. Note that we can use the same register for the address computation as for the destination of the load; we don't need a second one.

Compared to your approach, this avoids the need to use the literal pool, saving you an extra load from memory and 8 bytes of space in the pool.

If you still find it too "verbose", you could always wrap it in a macro.

none of these work, but why?

Because the only load instructions that take a PC-relative literal are ldr and ldrsw:

screenshot of table C3-16

See Table C3-16 in the ARMv8 Reference Manual.

Related