gcc删除内联汇编程序代码

时间:2012-06-13 13:16:39

标签: c gcc x86 compiler-optimization inline-assembly

似乎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位计算机)。

如何阻止此行为?

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

7 个答案:

答案 0 :(得分:6)

看起来就是这样 - 当gcc看到函数中的代码无法访问时,它会将其删除。其他编译器可能不同。
gcc中,编译的早期阶段是构建“控制流图” - 一个“基本块”的图形,每个条件都没有条件,通过分支连接。在发出实际代码时,将丢弃无法从根访问的图形部分 这不是优化阶段的一部分,因此不受编译选项的影响。

所以任何解决方案都会让gcc认为代码是可以访问的。

我的建议:

不是将汇编代码放在无法到达的地方(GCC可能会将其删除),而是将其置于可到达的位置,并跳过有问题的指令:

int main(void) {
     goto exit;

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

另请参阅this thread about the issue

答案 1 :(得分:5)

我不相信有一种可靠的方法只使用编译选项来解决这个问题。无论用于编译的选项如何,优选的机制都可以完成工作并在未来版本的编译器上工作。

关于接受的答案的评论

在接受的答案中,对原始内容进行了编辑,建议使用此解决方案:

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函数库有效地绕过return 0底部的main。使用Godbolt with 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 extended assembly templatesasm goto允许您指定内联汇编可能使用的跳转目标:

  

6.45.2.7转到标签

     

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

     

asm goto语句不能有输出。这是由于编译器的内部限制:控制传输指令不能有输出。如果汇编代码确实修改了任何内容,请使用“memory”clobber强制优化器将所有寄存器值刷新到内存,并在asm语句之后根据需要重新加载它们。

     

另请注意,asm goto语句始终隐式被视为volatile。

     

要在汇编程序模板中引用标签,请在其前面加上'%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=?选项进行编译,它不会发出警告。 在clobbers之后,您可以列出内联汇编可能使用的跳转目标。 C 实际上并不知道我们是否跳过,因为GCC不会分析内联汇编模板中的实际代码。它不能删除这个跳转,也不能假设死代码之后的内容。使用Godbolt with 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

Godbolt with GCC 4.6.4输出看起来仍然正确,显示为:

main:
        jmp .L2                   # <------ this is the goto exit
        jmp 0x0
.L2:                              # <------ exit label
        xorl    %eax, %eax
        ret

无论您是打开还是关闭优化,此机制也应该起作用,无论您是要编译64位还是32位x86目标都无关紧要。

其他观察

  • 当扩展内联汇编模板中没有输出约束时,asm语句是隐式易失性的。这条线

    __asm__ __volatile__("jmp 0x0");
    

    可以写成:

    __asm__ ("jmp 0x0");
    
  • asm goto语句被认为是隐式不稳定的。它们也不需要volatile修饰符。

答案 2 :(得分:4)

这会有用吗,让gcc无法知道它无法到达

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

答案 3 :(得分: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(默认情况下)。

解释:.pushsection将编译器的文本输出切换到另一个部分,在本例中为.discard(默认情况下在链接期间删除)。 "memory" clobber阻止GCC尝试移动将被丢弃的部分中的其他文本。但是,GCC没有意识到(并且永远不可能因为__asm____volatile__)2个语句之间发生的任何事情都将被丢弃。

对于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;
}

答案 4 :(得分: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

(这似乎真的狡猾,所以我希望有更好的方法来做到这一点。编辑只需将volatile放在{{1}前面}} works所以不需要内联asm技巧。)

答案 5 :(得分:1)

我从未听说过阻止gcc删除无法访问的代码的方法;似乎无论你做什么,一旦gcc检测到无法访问的代码,它总是将其删除(使用gcc的-Wunreachable-code选项来查看它认为无法访问的内容。)

也就是说,您仍然可以将此代码放在静态函数中,并且不会对其进行优化:

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

int main(void)
{
    goto exit;

handler:
    func();

exit:
    return 0;
}

<强> P.S
如果您希望在原始代码中的多个位置植入相同的“处理程序”代码块时避免代码冗余,则此解决方案特别方便。

答案 6 :(得分:0)

gcc可能会在函数内复制asm语句并在优化期间删除它们(即使在-O0),因此这将无法可靠地工作。

可靠地执行此操作的一种方法是使用全局asm语句(即任何函数之外的asm语句)。 gcc会将此直接复制到输出中,您可以毫无问题地使用全局标签。