THIS PROJECT ONLY BUILDS ON x86_64-PLATFORMS AND REQUIRES RUST NIGHTLY 1.59.
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.
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.