Given main.c
:
#include <stdio.h>
void (*fn_ptr)(void);
void foo(void);
void bar(void) {
printf("bar\n");
}
int main(void) {
fn_ptr = bar;
foo();
}
and foo.s
:
extern fn_ptr
global foo
foo:
mov rax, fn_ptr
call [rax]
ret
we can run and compile it like so:
clear &&
nasm foo.s -f elf64 -O0 &&
clang main.c foo.o -z noexecstack &&
./a.out
which successfully prints bar
, but also prints a warning:
/usr/bin/ld: foo.o: warning: relocation against `fn_ptr' in read-only section `.text'
/usr/bin/ld: warning: creating DT_TEXTREL in a PIE
bar
I couldn't find any online examples of calling global function pointers in NASM, and changing it to mov rax, fn_ptr wrt ..plt
or call [fn_ptr wrt ..plt]
prints this error:
foo.s:7: error: ELF format cannot produce non-PC-relative PLT references
(For anyone wondering what warning the -z noexecstack
gets rid of):
/usr/bin/ld: warning: foo.o: missing .note.GNU-stack section implies executable stack
/usr/bin/ld: NOTE: This behaviour is deprecated and will be removed in a future version of the linker
Same as any other case of accessing static data from things you're linking into the same ELF object (PIE executable): directly with a RIP-relative addressing mode.
You don't need and don't want to go through the PLT unless you want to do symbol-interposition tricks with LD_PRELOAD
with a library that defines a different global variable. But I'm not sure that can do anything for an executable (not another shared library), and since this is a variable not a function, at most it could change the initial value to be something other than 0
.
extern fn_ptr
default rel ;;;; Added this
global foo
foo:
call [fn_ptr] ;;;; equivalent to call qword [rel fn_ptr]
ret
I could also have done mov rbx, [fn_ptr]
/ call rbx
/ call rbx
if I wanted to make multiple calls with the same function pointer without reloading the function pointer every time, also saving code-size. Or lea rbx, [fn_ptr]
/ call [rbx]
/ call [rbx]
if I wanted to just save a bit of code-size but still reload the function pointer for every call.
This is a variable so it doesn't have a PLT entry; those are for functions. It will have a GOT entry if one is required, like if you were making a shared library and you want fn_ptr
to be ELF visibility global
instead of hidden
. (global is the default unless you use compiler options to change that.)
mov rax, [rel fn_ptr wrt ..got] ; load a pointer to the variable from the GOT
call [rax] ; memory-indirect call, loading a new RIP from memory
This is similar to how you use GOT entries to call library functions the way gcc/clang -fno-plt
does: Can't call C standard library function on 64-bit Linux from assembly (yasm) code
If you don't need code outside your shared library to be able to read and modify this function pointer, make it __attribute__((hidden))
so you can access it directly with [rel fn_ptr]
, without the unnecessary level of indirection.
As Joshua's answer shows, you actually can use wrt ..plt
, which makes an R_X86_64_PLT32
relocation instead of R_X86_64_PC32
, but it does still resolve to addressing the global variable directly, not a GOT or PLT entry for it. I don't know what PLT32
relocations are for. There isn't a different machine instruction, it's still call qword [rel32]
with a RIP-relative addressing mode, just I guess a different way of getting the linker to calculate the right offset from the end of this instruction to the global variable. Maybe the distinction mattered for 32-bit code which didn't have RIP-relative addressing so would want to manually add an offset from the PLT instead of from itself.
0: ff 15 00 00 00 00 call QWORD PTR [rip+0x0] # 6 <foo+0x6> 2: R_X86_64_PC32 fn_ptr-0x4
6: ff 15 00 00 00 00 call QWORD PTR [rip+0x0] # c <foo+0xc> 8: R_X86_64_PLT32 fn_ptr-0x4
is disassembly from
call [fn_ptr] ; (with default rel so this is [rel fn_ptr]
call [rel fn_ptr wrt ..plt]
Without default rel
and/or a [rel
in the addressing mode, see
mov reg, imm64
with an absolute address needs a text-relocation in a PIE executable or shared object, hence the warning.From comments; I can now type answer.
The following assembly instructions don't work:
mov rax, fn_ptr wrt ..plt
call fn_ptr wrt ..plt
call [fn_ptr wrt ..plt]
But these do:
mov rax, [rel fn_ptr wrt ..plt]
call [rel fn_ptr wrt ..plt]
The ELF format error is true. I'm surprised it can't handle mov rax, fn_ptr wrt ..plt
but call [fn_ptr wrt ..plt]
definitely cannot work. There's no such instruction as call [qword]
but only call rel dword
and call [rel dword]
.
The mov
instruction that surprises me that it doesn't work wouldn't get you out of the problem anyway; that's asking for an absolute fixup in the text segment to refer to the plt by absolute address. So if it did work that's still a warning.
What you want to access functions elsewhere is always something with rel
so you get a PIC-relative reference to the fixup area. Basic forms:
lea rax, [rel fn_ptr wrt ..plt] ; gets the absolute address of the address of fn_ptr
mov rax, [rel fn_ptr wrt ..plt] ; gets the absolute address of fn_ptr
call [rel fn_ptr wrt ..plt] ; calls fn_ptr
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With