C(和C++)标准未定义参数传递的顺序,或者它们在内存中应该如何组织。这取决于编译器开发人员(通常与操作系统开发人员合作)来想出适用于特定处理器架构的解决方案。
在大多数架构中,堆栈(和寄存器)用于将参数传递给函数,并且同样,在大多数C实现中,堆栈从“高到低”地址增长,传递的参数顺序为“左后右先”,因此如果我们有一个函数
void test( int a, int b, int c )
然后,参数按顺序传递:
c, b, a
然而,当参数的值被传递到寄存器中,并且使用这些参数的代码正在获取这些参数的地址时,情况变得更加复杂 - 寄存器没有地址,因此您无法获取寄存器变量的地址。因此,编译器将生成一些代码在函数本地将地址存储在栈上[从中我们可以获得该值的地址]。这完全取决于编译器的决定,它以什么顺序执行此操作,我相信这就是您所看到的。
如果我们将您的代码通过clang编译,我们会看到:
define void @test(i32 %a, i32 %b, i32 %c) #0 {
entry:
%a.addr = alloca i32, align 4
%b.addr = alloca i32, align 4
%c.addr = alloca i32, align 4
store i32 %a, i32* %a.addr, align 4
store i32 %b, i32* %b.addr, align 4
store i32 %c, i32* %c.addr, align 4
%call = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([10 x i8], [10 x i8]* @.str, i32 0, i32 0), i32* %a.addr, i32* %b.addr, i32* %c.addr)
%add.ptr = getelementptr inbounds i32, i32* %b.addr, i64 -1
%0 = load i32, i32* %add.ptr, align 4
%add.ptr1 = getelementptr inbounds i32, i32* %b.addr, i64 1
%1 = load i32, i32* %add.ptr1, align 4
%call2 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([7 x i8], [7 x i8]* @.str.1, i32 0, i32 0), i32 %0, i32 %1)
ret void
}
虽然可能不是完全容易阅读的,但您可以看到测试函数的前几行:
%a.addr = alloca i32, align 4
%b.addr = alloca i32, align 4
%c.addr = alloca i32, align 4
store i32 %a, i32* %a.addr, align 4
store i32 %b, i32* %b.addr, align 4
store i32 %c, i32* %c.addr, align 4
这实际上是在堆栈上创建空间 (%alloca
),并将变量 a
、b
和 c
存储到这些位置。
gcc 生成的汇编代码更难阅读,但您可以看到类似的操作:
subq $16, %rsp
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl %edx, -12(%rbp)
leaq -12(%rbp), %rcx
leaq -8(%rbp), %rdx
leaq -4(%rbp), %rax
movq %rax, %rsi
movl $.LC0, %edi
movl $0, %eax
call printf
你可能会想知道为什么它要分配4个整数的空间 - 这是因为在x86-64中,堆栈应该始终对齐到16字节。
*(&b-1)
会导致未定义的行为。 - M.M