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

似乎最先进的编译器将堆栈传递的参数视为只读。 请注意,在x86调用约定中,调用者将参数压入堆栈,并且被调用者使用堆栈中的参数。 例如,以下C代码:

extern int goo(int *x); int foo(int x, int y) { goo(&x); return x; } 

由OS X 10.10中的clang -O3 -c gc -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 

这里,参数x8(%ebp) )首先被加载到%eax ; 然后存储在-4(%ebp) ; 地址-4(%ebp)存储在%eax ; 和%eax传递给函数goo

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

  .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调用约定要求传递的参数是只读的,但我找不到任何问题。 有没有人对这个问题有任何想法?

实际上,我刚刚使用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 

这是使用GCC 4.9.2(如果重要的话,在32位cygwin上),没有优化。 事实上,GCC完全按照你的想法做了,并直接从调用者将其推送到堆栈的位置使用了参数。

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

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

真正的限制与C完全没有关系。 真正的限制是链接。 因此,可以将不同的目标文件链接在一起,需要标准来确保一个目标文件中的调用者所期望的匹配另一个目标文件中提供的被调用者。 这就是所谓的ABI。 在某些情况下(例如64位80×86),完全相同的架构有多种不同的ABI。

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

C编程语言要求参数按值传递。 因此,对参数的任何修改(如x++;作为foo的第一个语句)都是函数的本地,并且不会传播给调用者。

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

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

 int goo(int *x) { static int count; *x = count++; return count % 3; } 

顺便说一句,您可以使用链接时优化(通过编译和链接 clang -flto -O2gcc -flto -O2 )来使编译器能够改进或内联翻译单元之间的某些调用。

请注意, Clang / LLVM和GCC都是免费软件编译器。 如果你愿意,可以随意向他们提出改进补丁(但由于两者都是非常复杂的软件,你需要工作几个月来制作补丁)。

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