Rust: Calling Functions With Different Calling Conventions From Same File
The other day I had some fun with assembly and wanted to try to call assembly functions from Rust. Each compiler follows a certain calling convention, that specifies how parameters are passed between functions. The calling convention is part of an ABI, an application binary interface. There are two major calling conventions out there: System V ABI and “Microsoft/PE x64”. The latter has no real official name, at least I can’t find one in the Microsoft docs.
Each processor has a set of registers (on x86_64
for example rax, rcx, rdx, rsi, rdi
) which are mainly used for transferring parameters. For example, on Microsoft Windows (or UEFI, which uses the same convention) the first two integer arguments are passed in rcx
and rdx
. On UNIX on the other hand, they get passed via rdi
and rsi
.
I wrote a simple add(u64, u64) -> u64
function in assembly, that takes two 64-bit integers and calculates the sum. There are two implementations, where one uses the System V ABI and the other the “Microsoft/PE x64” convention to access the parameters. From Rust this is really easy to manage, because we can tell the compiler what calling convention it should use. This results in a compiler output by Rust, where the parameters are passed the right way. But first let’s look at my assembly code.
# written in GAS (GNU Assembly) .section .text # Adds two 64-bit numbers and returns the 64-bit result by using the # Microsoft calling convention used in the "PE" format on Windows # and UEFI. # # More info: https://docs.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-160 # # First param: RCX # Second param: RDX # Return: RAX pe_abi__asm_add: mov rax, rcx add rax, rdx ret # See Rust code. Thin wrapper to demonstrate, # that UEFI indeed uses the calling convention used in the "PE" format. efi_abi__asm_add: call pe_abi__asm_add ret # See Rust code. Thin wrapper to demonstrate, # that Windows indeed uses the calling convention used in the "PE" format. win64_abi__asm_add: call pe_abi__asm_add ret # Adds two 64-bit numbers and returns the 64-bit result by using the # "System V ABI" calling convention present in Linux and MacOS. # # More info: https://www.uclibc.org/docs/psABI-x86_64.pdf # # First param: RDI # Second param: RSI # Return: RAX system_v_abi__asm_add: mov rax, rdi add rax, rsi ret
If we want to call this from Rust, we need some “extern” function definitions, which tells Rust about the calling convention. Down below you can see how this looks like:
// Behind the scenes, Windows uses "Microsoft/PE" calling convention. // https://docs.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-160 extern "win64" { fn win64_abi__asm_add(a: i64, b: i64) -> i64; } // Behind the scenes, UEFI uses "Microsoft/PE" calling convention. // https://docs.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-160 extern "efiapi" { fn efi_abi__asm_add(a: i64, b: i64) -> i64; } // Defaults to System V ABI (64 bit), i.e. the calling convention used on // Linux or MacOS. // https://www.uclibc.org/docs/psABI-x86_64.pdf extern "sysv64" { fn system_v_abi__asm_add(a: i64, b: i64) -> i64; } /// Calculates two numbers by using functions of two different calling conventions. /// Furthermore, this example shows that "win64" and "efiapi" use indeed the "Microsoft/PE" /// calling convention. fn main() { println!("win64_abi__asm_add(2,7) = {}", unsafe { win64_abi__asm_add(2, 7) }); println!("efi_abi__asm_add(2,7) = {}", unsafe { efi_abi__asm_add(2, 7) }); println!("system_v_abi__asm_add(2,7)= {}", unsafe { system_v_abi__asm_add(2, 7) }); }
Site note: If we would write extern "C"
the compiler would use the default C
calling convention of the target (UNIX/Windows). Inside the Rust compiler source code we can find possible values for the extern "<calling convention/abi">
. That’s all the magic. This tells the compiler in what registers things should be put. If you run it, it correctly calculates the sums. I have a demo project on Github where you can find the source code. If you want to see how it works to compile assembly functions along with Rust code, check out my previous blog post.
PS: This is nothing new but my past self (>5 years) would find this extreeeeemly cool.
Additional Resources
- Microsoft/PE calling convention: https://docs.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-160
- System V ABI: https://www.uclibc.org/docs/psABI-x86_64.pdf
1 Comment
Paul · 2024-02-20 at 22:49
Thanks for sharing.
I find this extreeeeemly cool 🙂