被调用者如何识别参数通过寄存器而非栈传递?附汇编案例疑问
Great questions! Let's break this down in a way that makes sense for your Ubuntu x64 (WSL) setup.
This all comes down to calling conventions—a shared rulebook that compilers, assemblers, and CPUs strictly follow. Think of it like a pre-agreed protocol:
- Every platform has its own convention. For x86-64 Linux/Unix systems (like your WSL Ubuntu), we use the System V AMD64 Calling Convention. Windows uses a different one, but that’s not relevant here.
- When a function is compiled, it’s built to expect parameters exactly where the convention specifies. For System V, that means the first 6 integer/pointer parameters live in registers
%rdi,%rsi,%rdx,%rcx,%r8,%r9—not the stack. - If the caller breaks this rule (say, pushing a parameter to the stack when it should go in a register), the callee will look in the wrong place, and your program will crash immediately. This is why hand-written assembly or cross-language calls must stick strictly to the convention.
First, let’s recap your code and the generated assembly for context:
Original C Program
#include <cstdio> int main(int argc, char* argv[]) { int n = argc; if (n > 1) { n = 1; }else { n = -1; } printf("%d\n", n); return 0; }
Compiled Assembly (with g++ main.cpp -S -O1)
subq $8, %rsp cmpl $1, %edi setg %dl movzbl %dl, %edx leal -1(%rdx,%rdx), %edx movl $.LC0, %esi movl $1, %edi movl $0, %eax call __printf_chk movl $0, %eax addq $8, %rsp ret
Core Context: System V AMD64 Calling Convention Rules
For your Ubuntu x64 setup, the key rules here are:
- The first 6 integer/pointer parameters go into registers (in order:
%rdi,%rsi,%rdx,%rcx,%r8,%r9). Only parameters beyond the 6th get pushed to the stack. - Before calling a function, the stack pointer
%rspmust be 16-byte aligned. This is a hard requirement for x86-64.
Line-by-Line Breakdown
subq $8, %rsp
Whenmainis called, thecallinstruction pushes an 8-byte return address to the stack, making%rspmisaligned by 8 bytes. Subtracting 8 bytes brings it back to 16-byte alignment—this is just setup for the upcomingcall __printf_chk, not parameter passing.Calculating the value of
ncmpl $1, %edi: Compareargc(which lives in%rdi, per the calling convention formain's first parameter) to 1.setg %dl: Ifargc > 1, set the 8-bit register%dlto 1; otherwise set it to 0.movzbl %dl, %edx: Zero-extend%dlto the 32-bit%edx(so we have a proper integer value: 1 or 0).leal -1(%rdx,%rdx), %edx: A clever compiler optimization! This calculatesrdx*2 -1. Ifrdxis 1, we get 1; if it's 0, we get -1. This replaces the originalif-elsewith a single arithmetic operation—faster and cleaner.
Setting up parameters for
__printf_chk
You’ll notice this isn’t the standardprintf—it’s__printf_chk, a security-hardened version used by GCC. Its signature is roughlyint __printf_chk(int flag, const char *format, ...). Following the calling convention:movl $1, %edi: Put theflagparameter (set to 1 for standard usage) into%rdi(first parameter register).movl $.LC0, %esi: Put the address of the format string"%d\n"(stored at.LC0) into%rsi(second parameter register).%edxalready holds our calculatednvalue—this is the third parameter, which goes into%rdxper the convention.movl $0, %eax: For variadic functions (likeprintfand its variants), we set%eaxto 0 to tell the callee we aren’t passing any floating-point parameters (those use XMM registers).
Why no push instructions?
All parameters fit into the first 3 registers specified by the calling convention—there’s no need to push anything to the stack. The only stack operations are for maintaining alignment, not passing data.
How __printf_chk identifies parameters
Since __printf_chk was compiled to follow the System V convention, it knows exactly where to look:
- It grabs the
flagparameter from%rdi. - It grabs the format string from
%rsi. - It grabs the integer to print from
%rdx. - For any additional variadic parameters, it would follow the convention to pull them from the next registers or the stack (though there are none here).
内容的提问来源于stack exchange,提问作者kayochin

