我对x86-64环境中的参数传递过程感到好奇,因此我编写了一段代码。
//a.c
extern int shared;
int main(){
int a=100;
swap(&a, &shared);
}
//b.c
int shared=1;
void swap(int* a, int* b){
*a ^= *b ^= *a ^= *b;
}
我使用以下命令编译两个文件:gcc -c -fno-stack-protector a.c b.c
然后我objdump -d a.o
检查a.o的反汇编代码。
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: c7 45 fc 64 00 00 00 movl $0x64,-0x4(%rbp)
f: 48 8d 45 fc lea -0x4(%rbp),%rax
13: be 00 00 00 00 mov $0x0,%esi
18: 48 89 c7 mov %rax,%rdi
1b: b8 00 00 00 00 mov $0x0,%eax
20: e8 00 00 00 00 callq 25 <main+0x25>
25: b8 00 00 00 00 mov $0x0,%eax
2a: c9 leaveq
2b: c3 retq
由于我的工作环境是Ubuntu 16.04 x86-64,我发现很难理解传递参数的顺序。
在我看来,默认的调用约定是fastcall
,因此参数从右向左传递。
我从x86-64 System V ABI手册中了解到,rdi
和rsi
用于传递前两个参数
但是,根据反汇编代码,rdi
负责var a
,这是左边的参数,这意味着它应该是第二个参数。
有人能帮助我指出我的错误吗?
Args从左到右编号(归功于@R。因为发现这是你的实际混淆;我以为你在谈论asm指令的顺序,并且错过了问题的最后一段。)
看起来很正常。当call swap
指令运行时,
rdi
拥有一个指向a
(堆栈中的本地)的指针,由
lea -0x4(%rbp),%rax
和mov %rax,%rdi
。
(而不仅仅是lea
进入rdi
,因为你没有启用优化。)rsi
指向由shared
建立的mov $shared,%esi
al
持有0
,因为在调用函数之前没有定义或原型化函数。 (即使没有-Wall
,gcc也应该警告过你).o
的反汇编显示$shared
为0,因为它尚未链接,因此它是符号的占位符(和0偏移量)。使用objdump -drwC
查看重定位符号。 (我也喜欢-Mintel
,而不是AT&T语法。)
也更容易看到编译器的asm输出,你会看到$shared
而不是数字和符号引用。见How to remove "noise" from GCC/clang assembly output?。
写入什么顺序寄存器并不重要,只有它们在进入被调用函数时的值。
堆栈参数也是如此:如果编译器选择使用mov
将args写入堆栈,它可以按任何顺序执行。
只有当你选择使用push
时,你必须从右到左将第一个(最左边)的arg留在最低地址,这是所有主要的C调用约定所要求的args没有在寄存器中传递(如果有的话)。
这种从右到左的顺序可能就是为什么gcc -O0
(没有优化,再加上用于调试的反优化)选择按顺序设置寄存器,即使它无关紧要。
顺便说一下,即使没有UB也能正确实现,xor-swap也毫无用处且毫无意义。 (Are there sequence points in the expression a^=b^=a^=b, or is it undefined?)。
if(a==b) { *a = *b = 0; } else { int tmp = *a; *a=*b; *b=tmp; }
是一个更高效的交换,如果两个指针都指向同一个对象,则保留了安全xor-swap of zeroing的行为。我想你想要那个?为什么你会使用xor-swap?
如果不启用优化,编译器生成的asm将基本上很糟糕,就像main
的代码因为同样的原因而糟透了。如果你这样做,swap
通常可以内联并且是零指令,或者只需要在寄存器之间花费3个mov
指令;有时少。 (编译器只需更改其寄存器分配并确定a
和b
现在位于相反的寄存器中。)
你对第一/第二的想法是错误的。计数从左侧开始。
另外你的代码有一些问题,至少:
swap
,所以虽然没有,但GCC选择生成代码,如果它是一个可变函数(在%rax
中存储0),它将起作用。swap
的定义是一堆未定义的行为。