GCC移除内联汇编代码

17

似乎gcc 4.6.2会从函数中删除它认为未使用的代码。

test.c

int main(void) {
  goto exit;
  handler:
    __asm__ __volatile__("jmp 0x0");
  exit:
  return 0;
}

主函数 main() 的反汇编代码

   0x08048404 <+0>:     push   ebp
   0x08048405 <+1>:     mov    ebp,esp
   0x08048407 <+3>:     nop    # <-- This is all whats left of my jmp.
   0x08048408 <+4>:     mov    eax,0x0
   0x0804840d <+9>:     pop    ebp
   0x0804840e <+10>:    ret

编译器选项

没有启用任何优化,只是使用gcc -m32 -o test test.c-m32是因为我在64位机器上)。

如何停止这种行为?

编辑: 最好使用编译器选项来实现,而不是修改代码。


你是否开启了优化? - RedX
4
你想这样做有特别的原因吗?是GCC错误地消除了代码吗? - huon
1
我正在开发一个调试工具,需要修改代码并在函数内部放置陷阱处理程序,该函数将从其他地方调用。这段代码片段仅用于演示使用gcc时的问题。 - iblue
请问为什么您在__asm__中指定了__volatile__?这似乎是多余的,不是吗? - Eitan T
属性怎么样?usedalways_inline。将asm存根放置在一个always_inline,used函数中。 - artless noise
7个回答

7
看起来这就是现实 - 当 gcc 看到函数中的代码不可达时,它会将其移除。其他编译器可能不同。
gcc 中,编译的早期阶段是构建“控制流图” - 由“基本块”构成的图表,每个基本块都没有条件,并通过分支连接。当生成实际代码时,从根节点无法到达的图的部分将被丢弃。
这不是优化阶段的一部分,因此不受编译选项的影响。
因此,任何解决方案都需要让 gcc 认为代码是可达的。
我的建议:
不要把汇编代码放到一个不可达的地方(GCC 可能会将其删除),而是将其放到一个可达的地方,并跳过问题指令。
int main(void) {
     goto exit;

     exit:
     __asm__ __volatile__ (
        "jmp 1f\n"
        "jmp $0x0\n"
        "1:\n"
    );
    return 0;
}

此外,参见这个问题的讨论

在这段代码中,使用 goto exit;exit: 的目的是什么? - Michael Petch
1
@MichaelPetch,看起来毫无意义,不确定当时我在想什么。我可能只是从OP的程序中留下了它。 - ugoren

6
我不认为仅通过编译选项就有可靠的方法来解决这个问题。更好的机制是能够完成任务并且可以在将来编译器的任何选项下工作。

对接受答案的评论

在被接受的答案中,对原始回答进行了编辑,建议采用以下解决方案:

int main(void) {
  __asm__ ("jmp exit");

  handler:
      __asm__ __volatile__("jmp $0x0");
  exit:
  return 0;
}

首先,jmp $0x0 应该改为 jmp 0x0。其次,C标签通常会被翻译成本地标签。 jmp exit 实际上不会跳转到 C 函数中的 exit 标签,它会跳转到 C 库中的 exit 函数,从而绕过了 main 底部的 return 0。使用Godbolt 和 GCC 4.6.4,我们可以得到这个未经优化的输出(我已经删除了我们不关心的标签):

main:
        pushl   %ebp
        movl    %esp, %ebp
        jmp exit
        jmp 0x0
.L3:
        movl    $0, %eax
        popl    %ebp
        ret

.L3实际上是exit的本地标签。在生成的汇编中,您将找不到exit标签。如果存在C库,则可能会编译和链接。请勿在内联汇编中使用C本地goto标签。


使用asm goto作为解决方案

自GCC 4.5以来(OP正在使用4.6.x),支持asm goto扩展汇编模板asm goto允许您指定内联汇编可能使用的跳转目标:

6.45.2.7 Goto Labels

asm goto允许汇编代码跳转到一个或多个C标签。 asm goto语句中的GotoLabels部分包含一个逗号分隔的所有C标签列表,汇编器代码可以跳转到这些标签。 GCC假定asm执行落入下一条语句(如果不是这种情况,请考虑在asm语句之后使用__builtin_unreachable内置函数)。通过使用热和冷标签属性(请参见标签属性),可以改进asm goto的优化。

asm goto语句不能具有输出。这是由于编译器的内部限制:控制转移指令不能具有输出。如果汇编器代码确实进行了修改,请使用"memory" clobber强制优化器将所有寄存器值刷新到内存,并在必要时重新加载它们。

还请注意,asm goto语句始终被隐式视为易失性。

要在汇编模板中引用标签,请在标签前加上“%l”(小写的“L”),后跟其在GotoLabels中的(从零开始的)位置加上输入操作数的数量。例如,如果asm具有三个输入并引用两个标签,请将第一个标签称为“%l3”,第二个标签称为“%l4”。

或者,您可以使用括号括起来的实际C标签名称引用标签。例如,要引用名为carry的标签,可以使用“%l [carry]”。使用此方法时,标签仍必须在GotoLabels部分中列出。

代码可以这样编写:

int main(void) {
  __asm__ goto ("jmp %l[exit]" :::: exit);
  handler:
      __asm__ __volatile__("jmp 0x0");
  exit:
  return 0;
}

我们可以使用asm goto。我更喜欢__asm__而不是asm,因为它不会在使用-ansi-std =?选项编译时引发警告。 在输入输出参数之后,您可以列出内联汇编可能使用的跳转目标。C实际上不知道我们是否跳过了,因为GCC不分析内联汇编模板中的实际代码。它不能删除此跳转,也不能假定其后面是死代码。使用Godbolt使用GCC 4.6.4,未经优化的代码(已削减)如下:
main:
        pushl   %ebp
        movl    %esp, %ebp
        jmp .L2                   # <------ this is the goto exit
        jmp 0x0
.L2:                              # <------ exit label
        movl    $0, %eax
        popl    %ebp
        ret

The Godbolt与GCC 4.6.4输出仍然是正确的,显示为:
main:
        jmp .L2                   # <------ this is the goto exit
        jmp 0x0
.L2:                              # <------ exit label
        xorl    %eax, %eax
        ret

无论您打开还是关闭优化,这种机制都应该起作用,并且不应该影响您为64位或32位x86目标进行编译。


其他观察

  • When there are no output constraints in an extended inline assembly template the asm statement is implicitly volatile. The line

    __asm__ __volatile__("jmp 0x0");
    

    Can be written as:

    __asm__ ("jmp 0x0");
    
  • asm goto statements are considered implicitly volatile. They don't require a volatile modifier either.


如果您使用-std=gnu11或其他选项进行编译,asm将不会产生警告; 仅在使用-std=c11编译时才会产生警告。依赖GNU扩展才能正常工作的代码不是ISO C11代码,因此您不应使用std=c11来编译它。 - Peter Cordes
1
想法:将处理程序放在行外(在 return 0; 后面),但使用 asm goto ("" :::: handler)。这样编译器就不能删除 handler:,因为 asm 可能会跳转到那里(但实际上并没有)。在函数的正常执行路径中没有额外的指令。(我忘记了让编译器将处理程序代码放在行外是否适用于 OP 的用例。) - Peter Cordes

4

这个方案行得通,可以让gcc无法知道它是不可达的。

int main(void)  
{ 
    volatile int y = 1;
    if (y) goto exit;
handler:
    __asm__ __volatile__("jmp 0x0");  
exit:   
    return 0; 
}

2
如果编译器认为它可以欺骗你,那就反过来欺骗它:(仅适用于GCC)
int main(void) {
    {
        /* Place this code anywhere in the same function, where
         * control flow is known to still be active (such as at the start) */
        extern volatile unsigned int some_undefined_symbol;
        __asm__ __volatile__(".pushsection .discard" : : : "memory");
        if (some_undefined_symbol) goto handler;
        __asm__ __volatile__(".popsection" : : : "memory");
    }
    goto exit;
handler:
    __asm__ __volatile__("jmp 0x0");
    exit:
    return 0;
}

这个解决方案不会为无意义指令增加任何额外开销,但只适用于使用AS的GCC(AS是默认设置)。
解释:.pushsection将编译器的文本输出切换到另一个部分,这里是.discard(默认情况下在链接时删除)。"memory" clobber可以防止GCC尝试移动其他将被丢弃的部分中的文本。然而,GCC并没有意识到(也永远不可能因为__asm__是__volatile__),在两个语句之间发生的任何事情都将被丢弃。
至于some_undefined_symbol,它实际上就是任何从未被定义过的符号(或者实际上已经被定义了,这应该没有关系)。由于使用它的代码部分将在链接期间被丢弃,因此它也不会产生任何未解决的引用错误。
最后,对所需标签的条件跳转确保其看起来像是可达的。除了它根本不会出现在输出二进制文件中之外,GCC意识到它无法了解some_undefined_symbol的任何信息,这意味着它别无选择,只能假设if的两个分支都是可达的,这意味着在它看来,控制流可以通过到达goto exit或跳转到handler(即使没有任何代码能够这样做)。
但是,在启用链接器的垃圾回收功能ld --gc-sections时要小心(默认情况下禁用),否则它可能会想要摆脱仍未使用的标签。
编辑:忘记上面的所有内容。只需执行以下操作:
int main(void) {
    __asm__ __volatile__ goto("" : : : : handler);
    goto exit;
handler:
    __asm__ __volatile__("jmp 0x0");
exit:
    return 0;
}

在您的更新/不同的答案中,您不能简化一下吗?只需在正常的return 0;之后加上handler:即可。这将鼓励gcc将其放在函数末尾,而不是跳过它。(即您可以删除goto exit;语句和exit:标签。) - Peter Cordes
这不是重点。重点是这是一种可行的方法,可以欺骗GCC认为一个分支是可达的,即使实际上没有任何代码可以到达它。另外:我必须通过艰苦的方式学习,一旦你开始打开各种优化选项,GCC将在源代码语句顺序与文本(汇编)顺序与逻辑执行顺序(是的:3个不同的顺序)方面对你进行反击(一旦你开始比较标签的地址,情况会变得非常丑陋...) - user3296587
是的,handler:被视为可达,因为它将asm goto视为能够跳转到那里。 goto exit;只会使源代码更加复杂而毫无意义。最好在真正的return 0;之后放置handler:,让编译器以其所喜欢的方式布置分支。执行handler:后是否落入常规return 0;可能重要,也可能不重要。(但在除main之外的函数中,请勿忘记在handler:asm()之后包含一个return 0;,因为从非空函数掉落是UB,因此编译器可能得出结论它永远不会被执行。) - Peter Cordes

1

更新于2012/6/18

想一想,可以将goto exit放在一个asm块中,这意味着只需要更改一行代码:

int main(void) {
  __asm__ ("jmp exit");

  handler:
    __asm__ __volatile__("jmp $0x0");
  exit:
  return 0;
}

这比我下面的其他解决方案干净得多(可能比 @ugoren 目前的解决方案更好看)。


这个方法有点hacky,但似乎可以工作:将处理程序隐藏在一个条件语句中,该条件语句在正常情况下永远不会被执行,但通过使用一些内联汇编阻止编译器能够正确分析从而防止其被消除。
int main (void) {
  int x = 0;
  __asm__ __volatile__ ("" : "=r"(x));
  // compiler can't tell what the value of x is now, but it's always 0

  if (x) {
handler:
    __asm__ __volatile__ ("jmp $0x0");
  }

  return 0;
}

即使使用了-O3jmp仍然被保留:
    testl   %eax, %eax   
    je      .L2     
.L3:
    jmp $0x0
.L2:
    xorl    %eax, %eax 
    ret

这看起来非常靠不住,所以我希望有更好的方法来解决这个问题。编辑只需在x前面放置一个volatile即可工作,因此不需要进行内联汇编技巧。


1
你可能想看一下我的答案,它恰好展示了你的其中一个方法存在的问题。 - Michael Petch
1
我是其中一个点踩的人,但当答案被修正后,我将非常乐意撤销它。 - Michael Petch
1
如果你想从一个汇编语句跳转到一个本地标签,你需要使用 asm goto。正如MichaelPetch所指出的那样,你的代码正在跳转到libc的 exit 函数!尝试使用一个不是标准库函数的本地标签名称来查看链接错误。 - Peter Cordes

1

我从未听说过有一种方法可以防止gcc删除无法到达的代码;似乎无论你做什么,一旦gcc检测到无法到达的代码,它总是会将其删除(使用gcc的-Wunreachable-code选项查看它认为哪些代码是无法到达的)。

话虽如此,您仍然可以将此代码放入静态函数中,这样它就不会被优化掉:

static int func()
{
    __asm__ __volatile__("jmp $0x0");
}

int main(void)
{
    goto exit;

handler:
    func();

exit:
    return 0;
}

附言
如果您想在原始代码的多个位置实现相同的“处理程序”代码块,那么这个解决方案特别方便,可以避免代码冗余。


2
到目前为止,这是我见过的最好的。静态函数的技巧不错!我曾尝试使用 __attribute__((__noinline__)) 来获得类似的行为,但徒劳无功。唯一的真正问题是,我怀疑在调试时 jmp $0x0 需要实际放在函数中。虽然我可能是错的。无论如何,我喜欢这个答案的优美! :) - nixeagle

0

gcc 可能会在函数内部复制 asm 语句,并在优化期间将其删除(即使在 -O0 下也是如此),因此这永远不会可靠地工作。

一种可靠的方法是使用全局 asm 语句(即在任何函数外部的 asm 语句)。gcc 将直接将其复制到输出中,您可以使用全局标签而不会遇到任何问题。


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