为何Rust中函数指针用rsi传参而非普通函数的rdi?
Great question! Let's break down the register usage difference you're observing between regular functions and the closure called via a function pointer.
Key Background: Two Calling Conventions
First, we need to distinguish between two critical calling conventions at play here:
- x86-64 SysV ABI: This is the standard convention Rust (and C) uses for regular function calls. Under this rule, the first function argument is passed in the
rdiregister, the second inrsi, and so on. - Rust's
rust-callConvention: This is Rust's internal convention for trait methods likeFnOnce::call_once, designed specifically to handle closures and function objects. For a method likecall_once(self, args), theselfparameter (the closure instance) goes intordi, and theargstuple (your function's parameters) goes intorsi.
Breaking Down Your Code & Assembly
Let's walk through what's happening in your example, using the assembly you provided.
1. Regular Function foo
Your foo function accepts a fn(u64)—a "bare" function pointer that follows the C ABI. When foo calls f(10), it strictly adheres to the x86-64 SysV ABI, as shown in its assembly:
playground::foo: sub rsp, 24 mov qword ptr [rsp + 16], rdi mov eax, 10 mov qword ptr [rsp + 8], rdi mov rdi, rax ; Move the parameter 10 into `rdi` (SysV first argument register) mov rax, qword ptr [rsp + 8] call rax ; Call the function pointer stored in rax add rsp, 24 ret
This is exactly why you see rdi used here—it's following the standard system ABI.
2. Closure & Indirect call_once Call
Your closure |i| { i + 1; } has no captured environment, so it could be directly converted to a fn(u64) pointer that follows the C ABI. However, in debug mode, Rust prioritizes debuggability over optimization, so it preserves the trait-based closure machinery:
- In
main, the value passed tofoois actually the address ofcore::ops::function::FnOnce::call_once(as seen in thelea rdi, [rip + core::ops::function::FnOnce::call_once]line). call_onceuses therust-callconvention. Its assembly shows it preparing the closure instance and parameter:core::ops::function::FnOnce::call_once: sub rsp, 40 mov qword ptr [rsp + 16], rdi ; Store the closure instance (self) mov rsi, qword ptr [rsp + 16] lea rdi, [rsp + 8] call playground::main::{{closure}} ; Invoke the closure ; ... cleanup ...- When
call_oncecalls your closure, the parameter10is inrsi(perrust-callrules, sincerdiis reserved for the closure instance). Your closure's assembly confirms it usesrsifor the parameter:playground::main::{{closure}}: sub rsp, 24 mov qword ptr [rsp + 8], rdi mov qword ptr [rsp + 16], rsi add rsi, 1 ; Operate on the parameter in `rsi` setb al test al, 1 jne .LBB11_2 add rsp, 24 ret
What Happens in Release Mode?
If you compile your code in release mode (cargo build --release), Rust's optimizer will eliminate this indirect trait call layer. It will generate a bare function for your closure that strictly follows the x86-64 SysV ABI, so calling the function pointer will use rdi for the parameter—just like a regular function. You can verify this yourself by checking the assembly in release mode on the Rust Playground.
Summary
The difference boils down to debug vs. release compilation choices and Rust's internal trait handling:
- Regular functions and optimized function pointers follow the standard x86-64 SysV ABI (using
rdifor the first argument). - Debug-mode closures use Rust's
rust-callconvention viaFnOnce::call_once, which passes the function parameter inrsi(sincerdiis reserved for the closure instance itself).
内容的提问来源于stack exchange,提问作者Austaras

