函数参数在内存中是如何存储的?

4
尝试创建自己的stdarg.h宏变量参数函数的替代方案时,也就是具有未知数量参数的函数,我尝试理解参数在内存中的存储方式。以下是一个最小化可执行代码示例:
#include <stdio.h>

void    foo(int num, int bar1, int bar2)
{
  printf("%p %p %p %p\n", &foo, &num, &bar1, &bar2);
}

int main ()
{
  int     i, j;

  i = 3;
  j = -5;
  foo(2, i, j);
  return 0;
}

我很清楚地理解函数的地址和参数的地址不在同一位置。但是后者并不总是以相同的方式组织。

在x86_32架构(mingw32)上,我得到了这样的结果:

004013B0 0028FEF0 0028FEF4 0028FEF8

这意味着地址的顺序与参数的顺序相同。

但是当我在x86_64上运行它时,输出结果如下:

0x400536 0x7fff53b5f03c 0x7fff53b5f038 0x7fff53b5f034

地址与参数的顺序明显相反。

因此我的问题是 (简短概括) :

这些参数的地址是否取决于架构,还是编译器也会影响?


它实际上取决于ABI(应用程序二进制接口)。 - Some programmer dude
是的。参数是如何传递的取决于调用约定、架构和参数的类型。你可以参考http://en.wikipedia.org/wiki/X86_calling_conventions。 - David Hoelzer
我认为参数的位置只取决于架构,但编译器不应更改此位置,因为生成的汇编代码必须始终可读,而不管使用哪个编译器。 - Guillaume Munsch
关于Linux ABI:http://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64/ - olivecoder
4个回答

3
它取决于编译器。编译器供应商必须遵守CPU架构的规则。编译器通常也会遵循平台ABI,至少对于可能与另一个编译器生成的代码进行交互的代码是如此。平台ABI是给定平台的调用约定、链接语义等规范。
例如,在Linux和其他类Unix操作系统上的编译器遵循System V应用程序二进制接口,您可以在第3.2.3章找到有关如何将参数传递给函数的信息(寄存器中传递的参数从左到右传递,内存中传递的参数(在堆栈上)从右到左传递)。在Windows上,规则在这里记录。

确实非常清楚!不过我想知道为什么这个 Fedora 发行版(在 x86_64 上)分配地址时明显是从右到左,而不是按照规定的从左到右。我有什么遗漏吗? - Aulo
@Aulo 嗯,第3.2.3章节(第20页底部)指定应该从右到左,而不是你所说的从左到右。 - nos
我的错!我混淆了寄存器分配和参数推送。谢谢! - Aulo

2
他们依赖于ABI。在不影响的情况下(只会以已知方式调用的函数),这完全取决于编译器,通常意味着使用寄存器,这些寄存器没有地址(如果您要求该地址,则这些参数将具有地址,看起来好像所有东西都有地址)。被内联的函数甚至不再真正具有参数,因此它们的地址问题是无关紧要的——尽管当您强制发生时,它们将似乎存在并具有地址。

2
就像薛定谔的猫一样,参数地址的观察值可能取决于您选择或观察与否。 - Clifford

1

参数可能根本不存储在内存中,而是通过寄存器传递;但是,对于任何符号操作数&,语言都需要返回一个地址,因此您的观察结果可能是您实际尝试观察并且编译器只是将值复制到那些地址中,以便它们是可寻址的。

如果您请求与传递顺序不同的地址,则可能会发生有趣的事情,例如:

printf("%p %p %p %p\n", &num, &bar1, &bar2, &foo) ;

你可能会或者不会得到相同的结果;关键是你观察到的地址可能是观测结果的产物,而不是传递的结果。在ARM ABI中,函数的前四个参数通过寄存器R0、R1、R2和R3传递,之后通过堆栈传递。


当在printf中交换bar1和bar2时,两个相应的地址会被简单地交换。即它显示为: 004013B0 0028FEF0 0028FEF8 0028FEF4 和 0x400536 0x7fff4ac19a4c 0x7fff4ac19a44 0x7fff4ac19a48 因此,在两种架构上都是如此。 - Aulo
@Aulo:是的,在这种情况下是可以的;但我的观点是,您不能依赖跨平台(甚至可能在不同编译器或使用不同编译器选项时)的值,并且这些值仍可能受到您观察它们的方式的影响。关键是通过编写代码进行观察是不可靠的;要么使用您平台/编译器的ABI文档,要么通过观察编译器生成的汇编代码进行反向工程。 - Clifford

0
在x86_64上,由于实际上并没有将参数传递到任何内存中,因此您会以“奇怪”的顺序获得参数。它们是通过CPU寄存器传递的。通过获取它们的地址,您实际上强制编译器生成代码,将参数存储在内存中(在您的情况下是堆栈),以便您可以获取它们的地址。
您无法实现stdarg宏而不与编译器交互。在gcc中,stdarg宏只是包装了一个内置结构,因为您无法知道在需要它们时参数可能在哪里(编译器可能已经将寄存器重新用于其他内容)。 gcc中的内置stdarg支持可以显着改变使用它们的函数的代码生成,以使参数可用。我认为其他编译器也是如此。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接