1

I have a multiboot2-compliant ELF file for x86_64, where the start-symbol is defined in start.asm, a NASM assembly file. The multiboot2 header contains the relocatable tag.

Because GRUB doesn't support multiboot2 + a relocatable ELF (at least in July 2021 [3]), I want to resolve some relocations by myself to work around this and just load a static ELF.

For this I need to get the offset during runtime in my very first entry-symbol (specified in ELF header) in order to resolve relocations manually. With offset I mean the difference where GRUB located the binary in memory compared to the static address of the symbol in the ELF file.

In my entry symbol I'm in 64-bit long mode. It is not possible to directly access rip in NASM syntax, therefore I need some kind of workaround.

Solutions like [1] [2] do not work, because the rip keyword/register is not usable in NASM. Therefore I can't use

lea    rax,[rip+0x1020304]
; rax contains offset
sub    rax,0x1020304

How can I solve this?

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
phip1611
  • 5,460
  • 4
  • 30
  • 57
  • Don't use `code formatting` to highlight names that aren't code. If you're talking about the `nasm` shell command, like `nasm -f elf64`, it can make sense to write `nasm`, but certainly not `x86_64`. – Peter Cordes Jul 21 '21 at 17:19

1 Answers1

2

The only way to access rip in nasm is through the rel-keyword [1]. Without an odd workaround, it can't take an immediate but only a symbol. To solve it with a symbol, the following code works:

; the function we want to jump to. We need to calculate the runtime
; address manually, because the ELF file is not relocatable, therefore static.
EXTERN entry_64_bit

; start symbol must be globally available (linker must find it, don't discard it)
; Referenced in ELF-Header.
GLOBAL start

SECTION .text

; always produce x-bit x86 code (even if this would be compiled to an ELF-32 file)
[BITS 64]

    ; very first entry point; this is the address where GRUB loads the binary
    start:
        ; save values provided by multiboot2 bootloader (removed here)
        ; ...

        ; Set stack top (removed here)
        ; ...

        ; rbx: static link address
        mov     rbx, .eff_addr_magic_end

        ; rax: runtime address (relative to instruction pointer)
        lea     rax, [rel + .eff_addr_magic_end]
    
    .eff_addr_magic_end:
        ; subtract address difference => offset
        sub     rax, rbx
        ; rax: address of Rust entry point (static link address + runtime offset)
        add     rax, entry_64_bit
        jmp     rax

Note that this is really tricky and needs deep expertise about several low level topics. Use with caution.

phip1611
  • 5,460
  • 4
  • 30
  • 57
  • Yes, using the address of a label is certainly the sensible thing. But why can't you just `jmp entry_64_bit` to do a relative jump there? You'd only need to calculate the relocation distance if you were going to actually do some runtime relocations to fix up some absolute addresses. If you have pure position-independent code (gcc/clang `-fPIE`, maybe also rustc), your executable image Just Works at any base address it's loaded to. (As long as it doesn't try to use a non-existent GOT, so maybe not as easy as I first thought.) – Peter Cordes Jul 21 '21 at 17:29
  • If for some reason `entry_64_bit` needed its own absolute address in RAX, you could `lea rax, [rel entry_64_bit]` / `jmp rax`. – Peter Cordes Jul 21 '21 at 17:37
  • The problem is my kernel gets booted by GRUB via multiboot2. GRUB only supports relocation of static ELF files (source [3] in question). Therefore I can't let the ELF loader do the relocations, when I use a relocatable ELF. Colleagues in my company wrote working logic that does the "live relocation patching" during runtime and it works. It is written in C++ and I my task is to prototype everything in Rust. But even there, I need a few assembly code first, before I can jump into the Rust code (which is luckily almost completely position independent with my setup). – phip1611 Jul 21 '21 at 17:56
  • Sure, makes sense you'd need to set up a stack before calling/jumping to compiled code. But it's not clear why `jmp entry_64_bit` wouldn't work. A PC-relative jump (`jmp rel32`) should be resolved at link time and not need any runtime fixups, because the distance between the jump and `entry_64_bit` is constant, no matter what base address your ELF object is mapped at. Even the distance between mappings of code and data sections is fixed in the ELF metadata; that's why RIP-relative addressing of global vars and constants (like string literals) works. – Peter Cordes Jul 21 '21 at 18:14
  • Note that you can use `$` as a symbol that refers to the current location. No need to place your own symbol. – fuz Jul 22 '21 at 10:47
  • @PeterCordes `jmp entry_64_bit` is not working, because at link time `GNU ld` replaces the label with the static address of the symbol. During boot time `GRUB` places my multiboot2-binary somewhere, because of the `relocatable` tag. `GRUB` patches only the `address` in the multiboot2 header where it should jump to (my start-routine written in assembly) but no other addresses. Therefore in my start routine I have to calculate the address of the entry into Rust. – phip1611 Jul 22 '21 at 14:26
  • `jmp symbol` doesn't involve an absolute address, only relative, exactly like `lea rax, [rel entry_64_bit]`. A direct near `jmp` like that works by doing RIP += rel32. In fact, it's impossible in x86-64 machine code to do an absolute direct near jump even if you wanted to. https://www.felixcloutier.com/x86/jmp. If it's not working, there's some other reason. (Like possibly being mapped to an address more than 2GiB away? But that seems unlikely.) – Peter Cordes Jul 22 '21 at 17:01