【问题标题】:Remove needless assembler statements from g++ output从 g++ 输出中删除不必要的汇编语句
【发布时间】:2020-01-27 14:56:51
【问题描述】:

我正在调查本地二进制文件的一些问题。我注意到 g++ 创建了很多对我来说似乎没有必要的 ASM 输出。以-O0 为例:

Derived::Derived():
    pushq   %rbp
    movq    %rsp, %rbp
    subq    $16, %rsp          <--- just need 8 bytes for the movq to -8(%rbp), why -16?
    movq    %rdi, -8(%rbp)
    movq    -8(%rbp), %rax
    movq    %rax, %rdi         <--- now we have moved rdi onto itself.
    call    Base::Base()
    leaq    16+vtable for Derived(%rip), %rdx
    movq    -8(%rbp), %rax     <--- effectively %edi, does not point into this area of the stack
    movq    %rdx, (%rax)       <--- thus this wont change -8(%rbp)
    movq    -8(%rbp), %rax     <--- so this statement is unnecessary
    movl    $4712, 12(%rax)
    nop
    leave
    ret

选项-O1 -fno-inline -fno-elide-constructors -fno-omit-frame-pointer:

Derived::Derived():
    pushq   %rbp
    movq    %rsp, %rbp
    pushq   %rbx
    subq    $8, %rsp       <--- reserve some stack space and never use it.
    movq    %rdi, %rbx
    call    Base::Base()
    leaq    16+vtable for Derived(%rip), %rax
    movq    %rax, (%rbx)
    movl    $4712, 12(%rbx)
    addq    $8, %rsp       <--- release unused stack space.
    popq    %rbx
    popq    %rbp
    ret

此代码用于Derived 的构造函数,该构造函数调用Base 基本构造函数,然后覆盖位置0 处的vtable 指针,并将常量值设置为它所包含的int 成员以及Base 包含的内容。

问题

  • 我能否在翻译程序时尽量减少优化并去掉这些东西?我必须设置哪些选项?还是编译器无法使用-O0-O1 检测到这些情况,并且无法绕过它们?
  • 为什么会生成subq $8, %rsp 语句?您不能优化一开始就没有意义的语句。为什么编译器会生成它呢?即使使用 O0,寄存器分配算法也不应该为不存在的东西生成代码。那为什么会这样呢?

【问题讨论】:

  • AFAIK,编译器专门这样做是为了改善您的调试体验。
  • 没有效果的循环移动如何改善调试体验。请详细说明。
  • 调试器可以查看-8(%rbp) 以查看%rdi(某个局部变量?)的值,即使%rdi 稍后被重用(以保存一些其他局部变量)。至少如果我正确解释了程序集(我不习惯这种语法)。调试器此时更改此值也很简单,因为它会被再次读回。
  • %rdi 实际上是指向堆栈上方的Derived 对象的指针。该函数没有局部变量(或者不应该有——谁知道g++在内部做了什么)。
  • 改进C调试。如果您正在阅读/调试 asm,请至少使用 -Og。顺便说一句,你的函数是一个非静态类成员函数,所以它有一个隐含的 arg:rdi 中的this。由于-O0,哪个 g++ 溢出到堆栈中。请参阅Why does clang produce inefficient asm with -O0 (for this simple floating point sum)? 了解原因。

标签: c++ assembly compiler-construction g++


【解决方案1】:

编译器无法使用-O0-O1 检测这些情况是否有原因

正是因为您告诉编译器不要这样做。这些是优化级别,需要关闭或关闭才能进行正确调试。您还在用编译时间换取运行时间。

您以错误的方式通过望远镜观察,看看当您启动up优化时,您的编译器会为您做哪些很棒的优化。

【讨论】:

  • @hochl 实验表明你的假设是错误的。你有什么问题?
  • @hochl 编译器的工作是减少抽象机器上指定的抽象。但是在低优化级别,它会留下更多的抽象,这可能只是将自己暴露为次优的组装(例如,即使在不再需要“变量”之后仍然保留它们) - 不足为奇,因为你没有 要求 进行最佳组装。在低优化级别,它还会做某些事情来使调试符号更有意义并帮助调试器(例如保留仍在范围内但之后未使用的变量的值)。
  • 出于调试目的引入这个有很多原因。 OTOH 它是一个有用的金丝雀(尽管不是在这种情况下),我相信其他有更多调试经验的人可以提供更好的答案。我认为在设置 O0 甚至 O1 之后对此抱怨太多是不合理的。你根本没有要求非常积极的优化,所以你不会得到它。您已经找到了一个例子,人们很难证明这个额外的堆栈空间是合理的,但在某些情况下证明它的合理性会容易得多。
  • 这就是重点——即使没有优化,这也不应该是任何输出的一部分。编译器如何创建它?不是-O0 意味着please add some bogus code that does nothing for an entity that is not there。我想这个问题确实需要深入了解 G++ 的内部知识才能回答:(顺便说一句,我尝试使用 clang++,情况更糟。
  • 这不是“请添加一些对不存在的实体不起作用的虚假代码”,而是“嘿,您知道您使用的那些标准技术在某些情况下可能看起来完全无用吗?我不在乎你是否会在它没有取得任何成果的情况下删除它们”这不需要对编译器实现有深入的了解,它要求你了解代码生成器执行的一般实践来帮助调试。如果您说您不在乎是否在不需要时将其删除,则在不需要时不会将其删除。这可能需要转移到聊天中
【解决方案2】:

我在您的-O1 输出中没有看到任何明显的优化遗漏。当然除了将 RBP 设置为帧指针,但您使用 -fno-omit-frame-pointer 如此清楚地知道为什么 GCC 没有优化它。

函数没有局部变量

你的函数是一个非静态类成员函数,所以它有一个隐含的参数:this in rdi。由于-O0,哪个 g++ 溢出到堆栈中。函数 args 算作局部变量。

没有效果的循环移动如何改善调试体验。请详细说明。

为了改进 C/C++ 调试:调试信息格式只能描述 C 变量相对于 RSP 或 RBP 的位置,而不是它当前所在的寄存器。此外,您可以修改 任何带有调试器的变量并继续,得到预期的结果,就好像你在 C++ 抽象机中那样。每条语句都被编译成一个单独的 asm 块,寄存器中没有有效值(有趣的事实:register int foo 除外:该关键字确实会影响调试模式代码生成)。

Why does clang produce inefficient asm with -O0 (for this simple floating point sum)? 也适用于 G++ 和其他编译器。

我必须设置哪些选项?

如果您正在阅读/调试 asm,请至少使用 -Og 或更高版本来禁用 -O0 的调试模式溢出所有语句之间的行为。最好是-O2-O3,除非你喜欢看到比完全优化更多的错过的优化。但是-Og-O1 将进行寄存器分配并进行合理的循环(条件分支在底部),以及各种简单的优化。虽然仍然不是 xor-zeroing 的标准窥视孔。

How to remove "noise" from GCC/clang assembly output? 解释了如何编写接受 args 并返回值的函数,这样您就可以编写不会优化掉的函数。

加载到 RAX 然后 movq %rax, %rdi 只是 -O0 的副作用。 GCC 只花很少的时间优化程序逻辑的 GIMPLE 和/或 RTL 内部表示(在发出 x86 asm 之前),它甚至没有注意到它可能已经加载到 RDI 中。 -O0的部分意义在于编译快,调试一致。

为什么会生成subq $8, %rsp 语句?

因为 ABI 要求在 call 指令之前进行 16 字节堆栈对齐,而此函数执行偶数个 8 字节 pushes。 (call 本身会推送一个返回地址)。没有-fno-omit-frame-pointer,它会在-O1 消失,因为您不会强制g++ 推送/弹出RBP 以及它实际需要的调用保留寄存器。

Why does System V / AMD64 ABI mandate a 16 byte stack alignment?

有趣的事实:clang 经常使用虚拟的 push %rcx/pop 或其他东西,这取决于 -mtune 选项,而不是 8 字节的 sub。

如果它是一个叶函数,g++ 只会将 RSP 下面的红色区域用于本地,即使在 -O0Why is there no "sub rsp" instruction in this function prologue and why are function parameters stored at negative rbp offsets?


在未优化的代码中,G++ 分配一个它从未使用过的额外 16 个字节并不罕见。即使有时启用优化,g++ 在瞄准 16 字节边界时也会将其堆栈分配大小四舍五入。这是一个错过优化的错误。例如Memory allocation and addressing in Assembly

【讨论】:

    猜你喜欢
    • 2018-11-17
    • 1970-01-01
    • 2017-03-31
    • 2016-03-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-05-08
    相关资源
    最近更新 更多