assemblyarmapple-m1arm64

Struggling with using the LDR instruction in ARM assembly


I use an M1 MacBook Pro.

I am trying the load a value I put in the .data segment in my assembly code, but when I look at the value of the register using LLDB, it shows me 0x0.

Here's the code:

.global _main
.extern print
.extern printInt
.extern exit
.align 4
    
.text
_main:
    ldr x0, =num
    b exit
   
.data
num: .word 5

Don't worry about the exit label I branch to, I have seperately defined that in another assembly file.

And here's the register read just before executing the LDR instruction:

General Purpose Registers:
        x0 = 0x0000000000000001
        x1 = 0x000000016fdff2d0

And here's the register read just after executing the LDR instruction:

General Purpose Registers:
        x0 = 0x0000000000000000
        x1 = 0x000000016fdff2d0

I am also really struggling with learning Assembly on this machine, so if you have any helpful tools I can learn using, it would be really helpful if you share them.

I have tried searching up what the LDR instruction does, but the ARM's website is really obscure and difficult to process and other tools aren't helpful. I have no idea what's going on half the time.


Solution

  • This is the fault of Apple's new linker.

    For a long time, Apple had been using their ld64 linker for all of their platforms. And in this answer, I explain why using ldr xN, =... is broken on macOS, and what to use instead.

    Now, I do think that some choices made about the internal workings of the Mach-O LLVM pipeline (specifically about segment relocations) make it really hard to properly support this "emit-a-pointer-and-load-it-by-pc-relative-label" for Darwin targets. But the specific way in which this failure manifests depends on choices made by the linker. Ideally the linker would identify that this was gonna be a problem and error out at link-time. But ld64 seems to be blissfully unaware of runtime restrictions and simply emits a pointer in a segment where trying to rebase it will crash the process.

    But that was last year.

    In Xcode 15, Apple has deprecated ld64 and replaced it with a new default that I've been calling dyld-ld for lack of a better name.

    Running what on this new ld or invoking it with -v used to identify it as part of the dyld source tree back in Xcode 15.0:

    PROGRAM:ld  PROJECT:dyld-1015.7
    

    Though it seems to have since been broken out into its own project:

    PROGRAM:ld PROJECT:ld-1115.7.2
    

    Unfortunately it's closed source, so we can't just go and examine it. But from the behaviour it exhibits, I'm pretty sure it's not a fork of ld64, but a rewrite from scratch.

    Now, ld64 is still shipped as ld-classic and can be selected on the clang command line with -ld_classic. If you compile your code with that and then run dyld_info -fixups on your binary, you will see the rebase that is supposed to get applied (which will make the binary crash on launch).

    If you link with dyld-ld (which can be explicitly selected with -ld_new, though that is the default anyway), then dyld_info -fixups will just not show a rebase for that pointer at all. And if you examine it in a disassembler, you will find that it simply has the value 0. This means that such binaries will now launch... but then very likely just crash on some NULL pointers. And that's what you're seeing in lldb.

    So yeah, different symptom, same broken feature. You should still use adrp+add as I explain in my other answer.