2

I wanted to load the address of function to a register. I have written the following inline assembly. The inline assembler returns the error "Error: undefined symbol x0 used as an immediate value". What is wrong with this code?. I am using ARM GNU toolchain on ubuntu.

void MyFunction()
{
  ...
}

int main()
{
   __asm volatile("mov x2, %[FnAddr] \n\t"::[FnAddr]"m"(MyFunction));
}

Aim: In newly booted AArch64 system (which is in EL2), I wanted to go to EL1. For this ELR_EL2 register is to be loaded with the target address to continue the execution

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
Little Tree
  • 63
  • 1
  • 6
  • If your assembler thinks `x0` is a symbol name, not a register, you're almost certainly compiling for something other than AArch64. For example, maybe you used 32-bit ARM, like `arm-none-eabi-gcc` instead of `aarch64-linux-gnu-gcc`, since you said you used the Ubuntu packages for "ARM GNU". – Peter Cordes Sep 05 '22 at 12:01
  • This is for bare-metal, I am using aarch64-none-elf-gcc. My compile statement is as follows: aarch64-none-elf-gcc -I. -nostartfiles -ffreestanding --specs=rdimon.specs -L. -Wl,-T,qemu-virt-aarch64.ld -o output.elf startup.s main.c – Little Tree Sep 05 '22 at 12:08
  • 1
    Oh, I see, the problem is with your inline asm statement, not your compiler. You asked for a `"m"` memory operand, so GCC fills in the template with `mov x2, [x0]`, expanding `%[FnAddr]` to an addressing mode that will work with `ldr`. But `mov` can only read and write registers. For some reason the `[x0]` syntax gets GAS to think of `x0` as a symbol name, not a register. Not the most helpful error message. – Peter Cordes Sep 05 '22 at 12:13
  • 1
    (Use `gcc -S` or `gcc -save-temps -c` to see the asm it fed to the assembler.) If you want an address in `x2`, ask for it there in the first place. You can't safely write a register like `x2` without telling the compiler about it via a clobber or output operand anyway. Inline asm should usually have just one instruction, the special one that the compiler can't generate, with the constraints asking for the right operands in the right places. If your template starts or ends with `mov` or `ldr`, usually that means you're doing it wrong. – Peter Cordes Sep 05 '22 at 12:20

1 Answers1

2

There are several problems with what you have so far:

First, as Peter Cordes pointed out in comments, m is the wrong constraint and expands to something like [x0].

But thinking further, materializing a function's address in a register is actually somewhat complicated. A single mov instruction generally won't work, since the immediate is limited to 16 bits. So even if you can get the compiler to emit mov x2, #MyFunction, your program will compile but then fail to link. Usually you would do it with an adrp/add sequence, see Understanding ARM relocation (example: str x0, [tmp, #:lo12:zbi_paddr]).

The best approach, though, is to have the compiler do it for you. By using an input operand with an r constraint, the compiler will precede your inline asm with a sequence of adrp, mov or whatever is appropriate for your program's code model, get the address in some general-purpose register, and expand the operand to the name of that register (x0,x1, etc). You don't care here which register is chosen, because you're just going to use it as input for a msr instruction, so you'd want to avoid hardcoding x2 anyway. (In case it did have to be x2, you could use a local register variable to tell the compiler which register to use.)

Thus your code can simply become:

__asm volatile ("msr ELR_EL2, %[FnAddrReg]" : : [FnAddrReg] "r" (MyFunction));

Try it on godbolt (scroll down the assembly output to see the code in question).

Technically the volatile keyword is not necessary, as an asm with no outputs is always considered volatile, but it doesn't hurt and might be a good hint to someone reading the code.

Nate Eldredge
  • 48,811
  • 6
  • 54
  • 82