x86-64寄存器传递参数的顺序

7
我对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手册,rdirsi用于传递前两个参数enter image description here

然而,根据反汇编代码,rdi负责处理位于左侧的var a,也就是第二个参数。

有人能帮我指出我的错误吗?


2
左边的参数是第一个参数。 - user3386109
2
x86-64 System V不是“fastcall”。它没有除Sys V之外的名称。该术语描述了Windows调用约定。 - Peter Cordes
请指定正在使用的编译器版本和开关。 - M.M
@M.M:Ubuntu 16.04具有gcc5.3(https://packages.ubuntu.com/xenial/gcc),而OP指定使用`gcc -c -fno-stack-protector a.c b.c进行编译。我不知道你为什么要问,但这是完全正常的-O0未优化的编译器输出,并且在任何x86-64 gcc版本上基本相同,除了使用--enable-default-pie构建的gcc将使用lea shared(%rip), %rsi`。(显然不适用于Ubuntu 16.04。) - Peter Cordes
@PeterCordes,无论我们是否处于C89模式下,都会影响调用未声明函数的行为。 - M.M
@M.M:根据我的经验,即使在C99/C11模式下,gcc也通过应用C89 arg promotion规则来定义行为(并将其视为可变参数以设置AL)。https://godbolt.org/z/41dfgG 无论启用还是禁用优化,gcc5.3都会将EAX清零,即使使用-std=c11(而不是gnu11)。https://gcc.gnu.org/onlinedocs/gcc/PowerPC-SPE-Options.html#index-mno-prototype说PowerPC gcc有一个-mprototype选项,可以承诺对可变参数函数的调用进行适当的原型化,因此它可以避免在未原型化的调用之前执行PPC等效操作(在CR中清除/设置位)。没有提到仅限于C89。 - Peter Cordes
2个回答

6

参数从左到右编号(感谢@R.指出这是您实际上的困惑;我以为您在谈论asm指令的顺序,并错过了问题的最后一段。)

在我看来,看起来很正常。当call swap指令运行时,

  • rdi保存指向a的指针(堆栈上的本地变量),由
    lea -0x4(%rbp),%raxmov %rax,%rdi设置。

    (而不是直接将lea加载到rdi中,因为您没有启用优化。)

  • rsi保存指向shared的指针,由mov $shared,%esi设置
  • al保存0,因为您在调用函数之前没有定义或原型化该函数。(即使没有-Wall,gcc也应该警告您。)

.o 的反汇编显示 $shared 为0,因为它尚未链接,所以它是来自符号的占位符(和0偏移量)。 使用 objdump -drwC 查看重定位符号。 (我也喜欢使用 -Mintel,而不是AT&T语法。)

另外,更容易查看的是编译器的汇编输出,其中你将看到 $shared 而不是数字和符号引用。请参见如何从GCC / clang汇编输出中去除“噪音”?


寄存器的写入顺序无关紧要,只要它们在调用函数时的值相同。

对于堆栈参数也是一样的:如果编译器选择使用mov将参数写入堆栈,它可以按任意顺序执行。

只有当您选择使用push时,您才必须从右到左,以便将第一个(最左边)参数留在最低地址处,这是所有主流C调用约定所要求的,适用于未通过寄存器传递的参数(如果有)。

这种从右到左的顺序可能是为什么gcc -O0(没有优化,加上反优化以进行调试)选择以那种顺序设置寄存器,即使这并不重要。


顺便说一句,即使正确实现没有UB,xor-swap也是无用和毫无意义的(表达式a^=b^=a^=b中是否有序列点,或者它是未定义的?)。

if(a==b) { *a = *b = 0; } else { int tmp = *a; *a=*b; *b=tmp; } 是一个更有效的交换方法,如果两个指针都指向同一个对象,则保留了安全的xor-swap的行为。我猜你想要这样做吧?否则你为什么要使用xor-swap呢?

如果不启用优化,任何一种方法的编译器生成的汇编代码基本上都会很糟糕,就像main的代码一样。如果启用优化,swap通常可以内联并且成为零指令,或者只在寄存器之间花费多达3个mov指令;有时会更少。(编译器可以改变其寄存器分配并决定现在ab在相反的寄存器中。)


先生,UB是什么意思? - BecomeBetter
1
@BecomeBetter 【每个程序员都应该了解的未定义行为】(http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html)。另请参见:[XOR交换算法中运算符的未定义行为?](https://dev59.com/y4jca4cB1Zd3GeqPuk7s),以了解您的“swap”函数存在什么问题,该问题使用相同的定义。 - Peter Cordes

3

你对first/second的理解是错误的。计数从左边开始。

此外,你的代码存在一些问题,至少包括:

  1. 你在没有声明/原型的情况下调用了swap,因此虽然不需要,但GCC选择生成可以作为可变参数函数工作的代码(将0存储在%rax中)。

  2. swap的定义是一堆未定义行为。


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