C x86调用约定:堆栈传递的参数应该是只读的吗?

C x86调用约定:堆栈传递的参数应该是只读的吗?,c,x86,stack,argument-passing,calling-convention,C,X86,Stack,Argument Passing,Calling Convention,最先进的编译器似乎将堆栈传递的参数视为只读。注意,在x86调用约定中,调用方将参数推送到堆栈上,被调用方使用堆栈中的参数。例如,以下C代码: extern int goo(int *x); int foo(int x, int y) { goo(&x); return x; } 由OS X 10.10中的clang-O3-cg.c-S-m32编译成: .section __TEXT,__text,regular,pure_instructions .mac

最先进的编译器似乎将堆栈传递的参数视为只读。注意,在x86调用约定中,调用方将参数推送到堆栈上,被调用方使用堆栈中的参数。例如,以下C代码:

extern int goo(int *x);
int foo(int x, int y) {
  goo(&x);
  return x;
}
由OS X 10.10中的
clang-O3-cg.c-S-m32
编译成:

    .section    __TEXT,__text,regular,pure_instructions
    .macosx_version_min 10, 10
    .globl  _foo
    .align  4, 0x90
_foo:                                   ## @foo
## BB#0:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $8, %esp
    movl    8(%ebp), %eax
    movl    %eax, -4(%ebp)
    leal    -4(%ebp), %eax
    movl    %eax, (%esp)
    calll   _goo
    movl    -4(%ebp), %eax
    addl    $8, %esp
    popl    %ebp
    retl


.subsections_via_symbols
这里,参数
x
8(%ebp)
)首先加载到
%eax
;然后存储在
-4(%ebp)
中;地址
-4(%ebp)
存储在
%eax
中;并将
%eax
传递给函数
goo

我想知道为什么Clang会生成代码,将
8(%ebp)
中存储的值复制到
-4(%ebp)
,而不是将地址
8(%ebp)
传递到函数
goo
。它将节省内存操作并产生更好的性能。我在GCC中也观察到类似的行为(在osx下)。更具体地说,我想知道为什么编译器不生成:

  .section  __TEXT,__text,regular,pure_instructions
    .macosx_version_min 10, 10
    .globl  _foo
    .align  4, 0x90
_foo:                                   ## @foo
## BB#0:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $8, %esp
    leal    8(%ebp), %eax
    movl    %eax, (%esp)
    calll   _goo
    movl    8(%ebp), %eax
    addl    $8, %esp
    popl    %ebp
    retl


.subsections_via_symbols
如果x86调用约定要求传递的参数是只读的,我搜索了文档,但找不到任何关于此问题的内容。有人对这个问题有什么想法吗?

通过辩论的授权。因此,对参数的任何修改(如
x++;
作为
foo
的第一条语句)都是函数的局部修改,不会传播到调用方

因此,一般调用约定应该要求在每个调用站点复制参数。调用约定对于未知调用应该足够通用,例如通过函数指针

当然,如果您将地址传递给某个内存区域,则被调用的函数可以自由地取消引用该指针,例如

int goo(int *x) {
    static int count;
    *x = count++;
    return count % 3;
}
顺便说一句,您可以使用链接时间优化(通过编译并使用
clang-flto-O2
gcc-flto-O2
链接),使编译器能够改进或内联翻译单元之间的一些调用

请注意,和都是编译器。如果您愿意,可以随时向他们推荐一个改进补丁(但由于两者都是非常复杂的软件,您需要花几个月的时间来制作该补丁)


注意。查看生成的汇编代码时,将
-fverbose asm
传递给编译器

C的规则是参数必须按值传递。编译器从一种语言(具有一组规则)转换为另一种语言(可能具有完全不同的规则)唯一的限制是行为保持不变。C语言的规则不适用于目标语言(如汇编)。

这意味着,如果编译器想要生成汇编语言,其中参数是通过引用传递的,而不是通过值传递的;那么这是完全合法的(只要行为保持不变)

真正的限制与C无关。真正的限制是链接。为了将不同的对象文件链接在一起,需要标准来确保一个对象文件中的调用者期望的内容与另一个对象文件中的调用者提供的内容相匹配。这就是所谓的ABI。在某些情况下(例如64位80x86),对于完全相同的体系结构,存在多个不同的ABI


你甚至可以发明你自己的完全不同的ABI(并实现你自己的工具来支持你自己的完全不同的ABI),就C标准而言,这是完全合法的;即使您的ABI对所有内容都要求“通过引用传递”(只要行为保持不变)。

实际上,我只是使用GCC编译了此函数:

int foo(int x)
{
    goo(&x);
    return x;
}
它生成了以下代码:

_foo:
        pushl       %ebp
        movl        %esp, %ebp
        subl        $24, %esp
        leal        8(%ebp), %eax
        movl        %eax, (%esp)
        call        _goo
        movl        8(%ebp), %eax
        leave
        ret

这是使用GCC4.9.2(在32位cygwin上,如果有必要的话),没有优化。所以事实上,GCC做了您认为它应该做的事情,并直接从调用方在堆栈上推送参数的位置使用参数。

由于参数的空间是通过递减
%esp
分配的,因此参数的空间与调用方的堆栈框架是不相交的。因此,我认为修改堆栈中的参数不会影响调用方的stackframe。我不确定是否遵循您的想法(看起来您正在打破按值调用的要求)。但是,如果您愿意,可以向或提出修补程序。这将花费你几个月的时间。太好了,所以深入研究编译器内部并开始你的补丁。您将需要几个月的工作。很抱歉在评论您的答案时给您带来干扰。我把我的问题编辑得更具体一些。我对Clang/GCC这样做的原因相当感兴趣。(事实上,我正在改进CompCert,一个经过验证的C编译器,w.r.t.链接。)谢谢你的回答@BasileStarynkevitch调用函数推送到堆栈上的值已经是该值的副本。OP的问题是为什么被叫人不直接使用该副本?取而代之的是,被叫人复制了另一份。你说得对
8(%ebp)
位于调用方的堆栈帧中,但它是专门分配给
foo
的空间。调用方在
foo
返回后是否会将该空间用于自己的目的,而不仅仅是通过调整堆栈指针来销毁它?如果是这样,则需要将值复制到
foo
的堆栈帧中。如果没有,则
foo
可以安全地“借用”调用方堆栈帧中的空间,而不是复制。所以,要知道你的想法是否好,你需要看看
foo
的调用方的代码是什么样子的。@AlexD谢谢你的评论!由于
foo
可以被任意函数调用,我认为这是一个关于调用约定的问题,而不是关于调用
foo
的特定上下文的问题。这是一个有趣的问题。我发现它声称gcc-O2确实修改了调用方推送堆栈参数。@JS1