Jee*_*ang 15 c x86 stack calling-convention argument-passing
似乎最先进的编译器将堆栈传递的参数视为只读.请注意,在x86调用约定中,调用者将参数压入堆栈,并且被调用者使用堆栈中的参数.例如,以下C代码:
extern int goo(int *x);
int foo(int x, int y) {
goo(&x);
return x;
}
Run Code Online (Sandbox Code Playgroud)
clang -O3 -c g.c -S -m32在OS X 10.10中编译成:
.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
Run Code Online (Sandbox Code Playgroud)
这里,首先加载参数x(8(%ebp))%eax; 然后存储-4(%ebp); 并且地址-4(%ebp)存储在%eax; 并%eax传递给函数goo.
我不知道为什么锵生成复制存储在值代码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
Run Code Online (Sandbox Code Playgroud)
我搜索了文件,如果x86调用约定要求传递的参数是只读的,但我找不到任何关于这个问题的东西.有没有人对这个问题有任何想法?
Bre*_*dan 13
C的规则是参数必须按值传递.编译器将一种语言(使用一组规则)转换为另一种语言(可能使用完全不同的规则集).唯一的限制是行为保持不变.C语言的规则不适用于目标语言(例如汇编).
这意味着如果编译器感觉像生成汇编语言,其中参数通过引用传递并且不通过值传递; 那么这是完全合法的(只要行为保持不变).
真正的限制与C完全没有关系.真正的限制是链接.因此,可以将不同的目标文件链接在一起,需要标准来确保一个目标文件中的调用者所期望的匹配另一个目标文件中提供的被调用者.这就是所谓的ABI.在某些情况下(例如64位80x86),对于完全相同的架构,存在多个不同的ABI.
您甚至可以创建自己完全不同的ABI(并实现自己的工具,支持您自己完全不同的ABI),就C标准而言,这是完全合法的; 即使您的ABI要求所有内容都"通过引用"(只要行为保持不变).
实际上,我刚刚使用GCC编译了这个函数:
int foo(int x)
{
goo(&x);
return x;
}
Run Code Online (Sandbox Code Playgroud)
它生成了这段代码:
_foo:
pushl %ebp
movl %esp, %ebp
subl $24, %esp
leal 8(%ebp), %eax
movl %eax, (%esp)
call _goo
movl 8(%ebp), %eax
leave
ret
Run Code Online (Sandbox Code Playgroud)
这是使用GCC 4.9.2(如果重要的话,在32位cygwin上),没有优化.事实上,GCC完全按照你的想法做了,直接从调用者把它推到堆栈的地方使用了参数.
在C编程语言是参数传递任务的价值.因此,对参数的任何修改(如x++;作为您的第一个语句foo)都是函数的本地修饰,并且不会传播给调用者.
因此,一般调用约定应该要求在每个调用站点复制参数.对于未知调用,调用约定应该足够通用,例如通过函数指针!
当然,如果将地址传递给某个内存区域,则被调用的函数可以自由地取消引用该指针,例如
int goo(int *x) {
static int count;
*x = count++;
return count % 3;
}
Run Code Online (Sandbox Code Playgroud)
顺便说一句,你可以使用链接时优化(由编译和链接用clang -flto -O2或gcc -flto -O2)或许使编译器能够提高或内联翻译单元之间的一些电话.
请注意,Clang/LLVM和GCC都是免费软件编译器.如果你愿意,可以随意向他们提出改进补丁(但由于两者都是非常复杂的软件,你需要工作几个月来制作补丁).
NB.在查看生成的汇编代码时,请传递-fverbose-asm给您的编译器!