Search code examples
assemblyclangllvmarm64addressing-mode

Correct immediate range of ldr in apple arm, possible clang assmbler bug


Running clang-as on this code produces "error: index must be an integer in range [-256, 255]"

        .section        __TEXT,__text,regular,pure_instructions
        .p2align        2
_foo:
        sub     sp, sp, #1000
        ldr     x0, [sp, #320]  ; ok
        ldr     x0, [sp, #332]  ; error: index must be an integer in range [-256, 255]
        ldr     x0, [sp, #360]  ; ok
        add     sp, sp, #1000
        ret

MacBook-Air-4:src crb$ as bug2.s
bug2.s:6:19: error: index must be an integer in range [-256, 255].
        ldr     x0, [sp, #332]  ; error: index must be an integer in range [-256, 255]

MacBook-Air-4:src crb$ as --version
Apple clang version 16.0.0 (clang-1600.0.26.6)
Target: arm64-apple-darwin24.3.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin

Am I missing something? Thanks.


Solution

  • You've given an operand that is not encodable. The assembler is correct to reject it, though its error message doesn't explain the issue as fully as it might.

    ARM64 has two forms for load instructions that accept a register base plus immediate offset:

    • One encodes a 12-bit immediate which is zero-extended and scaled by the size of the data being loaded (here 8 bytes), and is intended to be used for aligned data. It appears in the Architecture Reference Manual as "LDR (immediate)", and this is the "Unsigned offset" form. In the assembly source, you specify the offset value that is to be the result of this calculation, and the assembler computes the proper encoding. So for an 8-byte load, this means you can specify an offset between 0 and (2^12-1)*8 = 32760, which must be a multiple of 8.

      ldr x0, [sp, #332] cannot be encoded with this form because 332 is not divisible by 8.

    • The other encodes a 9-bit immediate which is sign-extended and not scaled, intended to be used for negative offsets or possibly unaligned data. So the offset you specify can be any integer between -256 and 255. The official mnemonic for this form is LDUR, but when you write LDR in your code with an operand that doesn't meet the criteria for the first form, the assembler will automatically try to use LDUR instead.

      ldr x0, [sp, #332] cannot be encoded with this form either because 332 is greater than 255.

    In principle, the error message could be stated more precisely as "Index must be an integer multiple of 8 in the range [0, 32760] OR an integer in the range [-256,255]". I presume clang tries the LDR unsigned offset form first, and when it doesn't fit, it silently falls back to try the LDUR form. When that doesn't fit either, you get an error message that really applies to the LDUR form only.

    If this is really what you want to do, then you'll need to use an extra instruction to calculate the address in a scratch register:

    add x1, sp, #332  // ok: immediate can be any integer in [0,4095]
    ldr x0, [x1]
    

    It seems a bit unlikely though, since you'd be loading a misaligned 8-byte value, and ARM64 code is usually pretty careful about keeping things aligned.


    On that note, recall that another requirement of ARM64 is that the stack pointer sp remain aligned to 16 bytes at all times (or at least, whenever it's used as the base register for a memory access instruction). This alignment would be satisfied on entry to your function, so sub sp, sp, #1000 would misalign it because 1000 is not divisible by 16. So your subsequent load instructions would fault when executed.