Skip to content

Instantly share code, notes, and snippets.

@iximeow
Created May 27, 2025 09:32
Show Gist options
  • Save iximeow/4e83c245f507d251d4e6feabbebd2bbe to your computer and use it in GitHub Desktop.
Save iximeow/4e83c245f507d251d4e6feabbebd2bbe to your computer and use it in GitHub Desktop.
tiny demo of rustc not calling static library functions as well as it could
global testfn
testfn:
xor eax, eax
add rax, rdi
add rax, rax
ret
#![no_std]
#![no_main]
// a minimal example of an unfortunate Rust linkage weirdness. if a function is provided via a
// statically linked library on Linux/BSD/Solaris targets, it's called via an indirect call through
// the function's GOT entry, rather than as a direct call to the statically linked function.
//
// here, `testfn` comes from a statically linked `libbinary.a`. `libbinary.a` is assembled from
// the colocated `binary.asm`:
//
// nasm -f elf64 -o binary.o binary.asm
// ar -r libbinary.a binary.o
//
// if `test.rs` is built with the following command line, `testfn` will be indirectly called
// (`ff 15 ..`):
//
// rustc +nightly -C panic=abort -C opt-level=3 -L . test.rs
//
// if `test.rs` is built with an additional `-Z plt=yes` argument, `testfn` will be directly called
// (`e8 ..`):
//
// rustc +nightly -Z plt=yes -C panic=abort -C opt-level=3 -L . test.rs
//
// this post from 2022 describes the same issue as shown here:
// https://internals.rust-lang.org/t/compiler-emits-indirect-call-through-got-for-statically-linked-extern-functions/16541
//
// in the former case, `main` will look like:
// ```
// 0000000000001720 <main>:
// 1720: 53 push %rbx
// 1721: 48 8d 3d b8 ee ff ff lea -0x1148(%rip),%rdi # 5e0 <__abi_tag+0x31c>
// 1728: ff 15 7a 12 00 00 call *0x127a(%rip) # 29a8 <puts@GLIBC_2.2.5>
// 172e: bf 03 00 00 00 mov $0x3,%edi
// 1733: ff 15 77 12 00 00 call *0x1277(%rip) # 29b0 <_DYNAMIC+0x1e0> ; this is the call to `testfn`
// 1739: 04 30 add $0x30,%al
// 173b: 0f b6 f8 movzbl %al,%edi
// 173e: 48 8b 1d 73 12 00 00 mov 0x1273(%rip),%rbx # 29b8 <putchar@GLIBC_2.2.5>
// 1745: ff d3 call *%rbx
// 1747: bf 0a 00 00 00 mov $0xa,%edi
// 174c: 48 89 d8 mov %rbx,%rax
// 174f: 5b pop %rbx
// 1750: ff e0 jmp *%rax
// ```
//
// where in the latter main will be more straightforward:
// ```
// 0000000000001710 <main>:
// 1710: 50 push %rax
// 1711: 48 8d 3d b0 ee ff ff lea -0x1150(%rip),%rdi # 5c8 <__abi_tag+0x304>
// 1718: e8 83 00 00 00 call 17a0 <puts@plt>
// 171d: bf 03 00 00 00 mov $0x3,%edi
// 1722: e8 19 00 00 00 call 1740 <testfn> # this is what used to be a call through _DYNAMIC
// 1727: 04 30 add $0x30,%al
// 1729: 0f b6 f8 movzbl %al,%edi
// 172c: e8 7f 00 00 00 call 17b0 <putchar@plt>
// 1731: bf 0a 00 00 00 mov $0xa,%edi
// 1736: 58 pop %rax
// 1737: e9 74 00 00 00 jmp 17b0 <putchar@plt>
// ```
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_: &PanicInfo) -> ! {
loop {}
}
// note that even though we've declared right here in the source that the extern block will be
// statically linked, that doesn't make it to codegen!
#[link(name="binary", kind="static")]
extern "C" {
pub fn testfn(arg: u64) -> u64;
}
#[link(name="c", kind="dylib")]
extern "C" {
pub fn puts(s: *const u8) -> u32;
pub fn putchar(c: u8) -> u32;
}
#[no_mangle]
pub extern "C" fn main() {
unsafe {
puts("testfn(3) = ".as_ptr());
}
let res = unsafe { testfn(3) as u8 };
unsafe {
putchar(0x30 + res);
putchar(b'\n');
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment