在x86-64上,gcc参数寄存器溢出问题

7

我正在进行一些x86-64汇编的实验。编译了这个虚拟函数:

long myfunc(long a, long b, long c, long d,
            long e, long f, long g, long h)
{
    long xx = a * b * c * d * e * f * g * h;
    long yy = a + b + c + d + e + f + g + h;
    long zz = utilfunc(xx, yy, xx % yy);
    return zz + 20;
}

使用gcc -O0 -g编译后,我在函数汇编的开头发现了以下内容:

0000000000400520 <myfunc>:
  400520:       55                      push   rbp
  400521:       48 89 e5                mov    rbp,rsp
  400524:       48 83 ec 50             sub    rsp,0x50
  400528:       48 89 7d d8             mov    QWORD PTR [rbp-0x28],rdi
  40052c:       48 89 75 d0             mov    QWORD PTR [rbp-0x30],rsi
  400530:       48 89 55 c8             mov    QWORD PTR [rbp-0x38],rdx
  400534:       48 89 4d c0             mov    QWORD PTR [rbp-0x40],rcx
  400538:       4c 89 45 b8             mov    QWORD PTR [rbp-0x48],r8
  40053c:       4c 89 4d b0             mov    QWORD PTR [rbp-0x50],r9
  400540:       48 8b 45 d8             mov    rax,QWORD PTR [rbp-0x28]
  400544:       48 0f af 45 d0          imul   rax,QWORD PTR [rbp-0x30]
  400549:       48 0f af 45 c8          imul   rax,QWORD PTR [rbp-0x38]
  40054e:       48 0f af 45 c0          imul   rax,QWORD PTR [rbp-0x40]
  400553:       48 0f af 45 b8          imul   rax,QWORD PTR [rbp-0x48]
  400558:       48 0f af 45 b0          imul   rax,QWORD PTR [rbp-0x50]
  40055d:       48 0f af 45 10          imul   rax,QWORD PTR [rbp+0x10]
  400562:       48 0f af 45 18          imul   rax,QWORD PTR [rbp+0x18]

gcc非常奇怪地将所有参数寄存器都溢出到堆栈上,然后从内存中取出进行进一步操作。

这仅发生在-O0时(使用-O1没有问题),但是为什么?对我来说,这看起来像是反优化 - 为什么gcc要这样做?


6
我认为你可能颠倒了。我很确定上述内容是GCC始终(最初)生成代码的方式,只是通常你看不到它,因为它被轻松优化掉了(当然,只有在启用优化时才会这样)。 - user786653
这不是反优化,只是没有优化。 - Gunther Piez
我刚刚在某个地方看到了这个例子:http://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64/ :-) - Ciro Santilli OurBigBook.com
@GuntherPiez:我会称之为反优化;寄存器足以轻松容纳所有本地变量,并且它们已经在寄存器中启动,因此溢出它们仅是为了支持一致的调试(和使编译器内部算法更简单)。另请参见为什么clang使用-O0(对于这个简单的浮点和)会产生低效的汇编代码? - Peter Cordes
@user786653:GCC在生成汇编代码之前进行优化,使用GIMPLE等程序的内部表示形式。我认为优化构建将意识到这些变量不需要地址,并且不会在第一次分配时给它们一个地址,而是直接优化掉。 - Peter Cordes
@PeterCordes:我已经有一段时间没有看过这个问题了(我猜大约9年——时间飞逝),我可以同意我的最初评论并不是100%准确的(我知道,也认为我知道,GCC实际上不会生成溢出代码,然后在以后删除它),但我仍然认为我的评论和答案的精神仍然是正确的。也许我们只是对“反优化”是什么意思有不同的看法(在我看来,这将是为3*4生成一个imul指令之类的东西,而不是编译器做最容易/最快/最易于调试的事情)。 - user786653
2个回答

9
我绝不是GCC内部专家,但我会尽力而为。不幸的是,大多数关于GCC寄存器分配和溢出的信息似乎已经过时(引用了像local-alloc.c这样的不存在的文件)。
我正在查看gcc-4.5-20110825的源代码。
GNU C编译器内部中提到,初始函数代码由gcc/function.c中的expand_function_start生成。在那里,我们找到了处理参数的以下内容:
4462   /* Initialize rtx for parameters and local variables.
4463      In some cases this requires emitting insns.  */
4464   assign_parms (subr);

assign_parms 中,处理每个参数存储位置的代码如下:
3207       if (assign_parm_setup_block_p (&data))
3208         assign_parm_setup_block (&all, parm, &data);
3209       else if (data.passed_pointer || use_register_for_decl (parm))
3210         assign_parm_setup_reg (&all, parm, &data);
3211       else
3212         assign_parm_setup_stack (&all, parm, &data);

assign_parm_setup_block_p 处理聚合数据类型,在此情况下不适用,由于数据未作为指针传递,因此GCC检查use_register_for_decl

这里的相关部分是:

1972   if (optimize)
1973     return true;
1974 
1975   if (!DECL_REGISTER (decl))
1976     return false;

DECL_REGISTER测试变量是否使用了register关键字进行声明。现在我们有了答案:当未启用优化时,大多数参数都存在于堆栈上,并由assign_parm_setup_stack处理。对于指针参数,它们在溢出值之前通过源代码所采取的路线略微复杂,但是如果您感兴趣,可以在同一文件中跟踪。

为什么GCC在禁用优化时会将所有参数和局部变量溢出?为了帮助调试。考虑这个简单的函数:

1 extern int bar(int);
2 int foo(int a) {
3         int b = bar(a | 1);
4         b += 42;
5         return b;
6 }

使用gcc -O1 -c编译,我的电脑上生成了以下内容:

 0: 48 83 ec 08             sub    $0x8,%rsp
 4: 83 cf 01                or     $0x1,%edi
 7: e8 00 00 00 00          callq  c <foo+0xc>
 c: 83 c0 2a                add    $0x2a,%eax
 f: 48 83 c4 08             add    $0x8,%rsp
13: c3                      retq   

除非你在第5行断点并尝试打印a的值,否则这很好,你会得到

(gdb) print a
$1 = <value optimized out>

由于在调用bar后未使用该参数,因此该参数将被覆盖。

7
一些原因如下:
  1. 一般情况下,函数的参数必须像局部变量一样处理,因为它可能会在函数内被存储或其地址被取出。因此,最简单的方法就是为每个参数分配一个堆栈空间。
  2. 使用堆栈位置可以使调试信息更加简单:参数的值始终位于某个特定位置,而不是在寄存器和内存之间移动。

当您查看-O0代码时,请注意编译器的首要任务是尽可能减少编译时间并生成高质量的调试信息。


1
是的,并且在没有优化的情况下,编译器会明确使所有行独立,始终从实际变量重新加载并立即存储,这允许您将CPU移动到另一行,或更改调试器中任何变量的值,并使其正确运行。 - doug65536
是的,没错。将寄存器参数溢出到具有地址的内存中是-O0的一部分,除非您将它们声明为register int foo或其他内容。为什么clang使用-O0(对于这个简单的浮点求和)会生成低效的汇编代码? - Peter Cordes

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