Skip to content

This example shows how you can call functions that use different calling conventions from Rust code (in the same file).

Notifications You must be signed in to change notification settings

phip1611/rust-different-calling-conventions-example

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Using Different Calling Conventions Inside The Same Rust Project

THIS PROJECT ONLY BUILDS ON x86_64-PLATFORMS AND REQUIRES RUST NIGHTLY 1.59.

About Calling Conventions

A calling convention specifies properties like how parameters are passed between functions on a specific architecture and a specific runtime system (e.g. firmware or operating system). This is necessary on the one hand so that code generated by one compiler can fulfill its work but also that code generated by different compilers or different object files (e.g older version of other compiler) can be linked together. The calling convention is always architecture-dependent. For example, you can't use the same registers on x86 and ARM.

There are two major calling conventions for x86_64. The System V ABI and the Microsoft PE calling convention ("Microsoft/PE"). I'm not 100% sure about the official name of the latter. These calling conventions can be related to an executable format but this is not a requirement. Due to my knowledge, and I'm not 100% into this, ELF can support different ABIs and this information is stored in the header, whereas the PE format, the default executable format for Microsoft Windows and UEFI firmware, only supports the Microsoft/PE calling convention.

The System V ABI is used on UNIX systems with x86_64, therefore actively used on every Linux or MacOS machine (at least Intel Macs). It's hard to find good references. MacOS follows System V ABI perhaps by convention, but I can't find an official document. On ARM systems (Apple Silicon, new Macbooks, iPhones, and iPads (iOS)) they use their own calling convention, which is similar to the standard ARM64 calling convention.

I think on ARM all compilers and systems use the default ARM64 calling convention, probably with minor adjustments.

Example

Let's look at the following function: add(a: i64, b: i64) -> i64 on x86_64. Without smart compiler optimizations, this would result in a machine code that gets two arguments passed, moves one of the parameters into the/one accumulator register and then add the other value to it.

Parameter Register (System V ABI) Register (Microsoft/PE)
a rdi rcx
b rsi rdx
(return) rax rax

What's cool about Rust is that it is relatively easy for us to specify the calling convention that the compiler should use for a specific function. For this, please look into the code and execute it with cargo run. The code shows all relevant parts with comments. However, the most essential part is:

// PE => microsoft 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;
}

// PE => Microsoft/Windows calling convention. In UEFI spec. Same as "win64".
//   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 (x86_64).
extern "sysv64" {
    fn system_v_abi__asm_add(a: i64, b: i64) -> i64;
}

This tells Rust what calling convention should be used. Possible values can be found in the Rust compiler source: https://github.com/rust-lang/rust/blob/b09dad3eddfc46c55e45f6c1a00bab09401684b4/compiler/rustc_target/src/spec/abi.rs

If we look into the Rust compiler output, therefore:

$ cargo build
$ objdump -d target/debug/example-different-calling-conventions | less

we find for the first function call (Win64)

    7829:       b9 02 00 00 00          mov    $0x2,%ecx
    782e:       ba 07 00 00 00          mov    $0x7,%edx
    7833:       ff 15 97 c1 03 00       callq  *0x3c197(%rip)        # 439d0 <_GLOBAL_OFFSET_TABLE_+0x40>

for the second (EFI ABI)

    78c0:       b9 02 00 00 00          mov    $0x2,%ecx
    78c5:       ba 07 00 00 00          mov    $0x7,%edx
    78ca:       ff 15 f0 c0 03 00       callq  *0x3c0f0(%rip)        # 439c0 <_GLOBAL_OFFSET_TABLE_+0x30>

and for the last (System V ABI)

    795d:       bf 02 00 00 00          mov    $0x2,%edi
    7962:       be 07 00 00 00          mov    $0x7,%esi
    7967:       ff 15 5b c0 03 00       callq  *0x3c05b(%rip)        # 439c8 <_GLOBAL_OFFSET_TABLE_+0x38>

We can clearly see what registers are used for the argument passing.

More Resources

About

This example shows how you can call functions that use different calling conventions from Rust code (in the same file).

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published