为什么函数参数的顺序颠倒了?

时间:2015-06-28 13:56:20

标签: c++ function

我已经尝试了一些功能,我发现参数的顺序在内存中是相反的。那是为什么?

堆叠TEST.CPP:

#include <stdio.h>

void test( int a, int b, int c ) {
    printf("%p %p %p\n", &a, &b, &c);
    printf("%d %d\n", *(&b - 1), *(&b + 1) );
}

int main() {
    test(1,2,3);
    return 0;
}

铛:

$ clang++ stack-test.cpp && ./a.out
0x7fffb9bb816c 0x7fffb9bb8168 0x7fffb9bb8164
3 1

GCC

$ g++ stack-test.cpp && ./a.out
0x7ffe0b983b3c 0x7ffe0b983b38 0x7ffe0b983b34
3 1

编辑:不重复:评估顺序可能与内存布局不同,因此它是一个不同的问题。

6 个答案:

答案 0 :(得分:8)

此行为是特定于实现的。

在你的情况下,这是因为参数被推到了堆栈上。这里有一个interesting article,它显示了进程的典型内存布局,显示了堆栈如何减少。因此,在堆栈上推送的第一个参数将具有最高地址。

答案 1 :(得分:7)

调用约定取决于实现。

但是为了支持C变量函数(在形式参数列表中用...椭圆表示的C ++中),通常按从右到左的顺序推送参数,或者为它们保留堆栈空间。这通常称为(1) C调用约定。根据这个约定,以及机器堆栈在内存中向下增长的常见约定,第一个参数应该以最低地址结束,与结果相反。

当我使用MinGW g ++ 5.1(64位)编译程序时,我得到了

000000000023FE30 000000000023FE38 000000000023FE40

当我使用32位Visual C ++ 2015编译程序时,我得到了

00BFFC5C 00BFFC60 00BFFC64

这两个结果都与C调用约定一致,与您的结果相反。

所以结论似乎是你的编译器默认不是C调用约定,至少对于非可变函数。

您可以通过在正式参数列表的末尾添加...来测试这一点。

1) C调用约定还包括调用函数在函数返回时调整堆栈指针,但这与此无关。 功能

答案 2 :(得分:2)

C(和C ++)标准没有定义传递的参数的顺序,或者它们应该如何在内存中组织。编译器开发人员(通常与操作系统开发人员合作)可以提供适用于特定处理器体系结构的东西。

在MOST体系结构中,堆栈(和寄存器)用于将参数传递给函数,同样,对于MOST体系结构,堆栈从&#34;高到低&#34;地址,并且在大多数C实现中,传递的参数的顺序是&#34;最后一个&#34;,所以如果我们有一个函数

 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)并将变量abc存储到这些位置。

更难以阅读的是gcc生成的汇编代码,但你可以看到类似的事情发生在这里:

subq    $16, %rsp           ; <-- "alloca" for 4 integers.
movl    %edi, -4(%rbp)      ; Store a, b and c. 
movl    %esi, -8(%rbp)
movl    %edx, -12(%rbp)
leaq    -12(%rbp), %rcx     ; Take address of ... 
leaq    -8(%rbp), %rdx
leaq    -4(%rbp), %rax
movq    %rax, %rsi
movl    $.LC0, %edi
movl    $0, %eax
call    printf              ; Call printf.

您可能想知道为什么它为4个整数分配空间 - 这是因为堆栈应始终在x86-64中与16个字节对齐。

答案 3 :(得分:1)

C(和C ++)代码使用处理器堆栈将参数传递给函数。

堆栈的运行方式取决于处理器。堆栈可以(理论上)向下或向上增长。因此,如果地址增大或缩小,您的处理器就会定义。最后,并不是单独的处理器体系结构对此负责,但在体系结构上运行的代码有calling conventions

调用约定说,如何将参数放在一个特定处理器体系结构的堆栈上。这些约定是必要的,来自不同编译器的库可以链接在一起。

基本上,对于你作为C用户,如果堆栈中变量的地址增大或缩小,它通常没有区别。

<强>详细信息:

答案 4 :(得分:1)

ABI定义了如何传递参数。

在你的例子中,由于gcc和clang的x86_64 ABI默认值在寄存器(*)上传递了参数,因此它有点复杂,没有地址。

然后引用参数,因此编译器被迫为这些变量分配本地存储,并且排序和内存布局也是特定于实现的。

  • 注意:最多6个平凡参数,如果有更多,则传递给堆栈。
  • 参考:x86_64 ABI

答案 5 :(得分:0)

谈到32位x86 Windows

简短回答:指向函数参数的指针不是指向实际函数调用推送的堆栈的指针,但可以是编译器重定位变量的任何位置。

答案很长: 在将我的代码从bcc32(Embarcadero经典编译器)转换为CLANG时遇到了同样的问题。 MIDL编译器生成的RPC代码被破坏,因为RPC函数参数通过将指针指向第一个函数参数来转换序列化参数,假设所有后面的参数将遵循例如序列化(&安培;一个)

BCC32和CLANG生成的调试cdecl函数调用:

  • BCC32 :函数参数在堆栈上以正确的顺序传递,然后当需要参数地址时,直接给出堆栈地址。

  • CLANG :函数参数在堆栈上以正确的顺序传递,但是在实际函数中,所有参数的副本都在内存中以堆栈的相反顺序进行,并且函数参数地址是必需的,内存地址是直接给出的,导致恢复顺序。

否则说,不要假设函数C / C ++代码中的函数参数如何处理在内存中。它依赖于编译器。

在我的例子中,一个可能的解决方案是使用pascal调用约定(Win32)声明RPC函数,强制MIDL编译器单独解析参数。不幸的是,MIDL生成的代码很繁重而且代码需要进行大量的调整才能编译,但仍未完成。