17

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

测试.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 位机器上)。

我怎样才能阻止这种行为?

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

4

7 回答 7

6

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

因此,任何解决方案都会涉及gcc到认为代码是可访问的。

我的建议:

与其把你的汇编代码放在一个无法到达的地方(GCC 可能会删除它),你可以把它放在一个可以到达的地方,并跳过有问题的指令:

int main(void) {
     goto exit;

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

另外,请参阅有关此问题的线程

于 2012-06-13T13:54:18.693 回答
5

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


关于已接受答案的评论

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

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

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

首先jmp $0x0应该是jmp 0x0。其次, C标签通常被翻译成本地标签。jmp exit实际上并没有跳转到Cexit函数中的标签,而是跳转到C库中的函数,有效地绕过了. 将Godbolt 与 GCC 4.6.4 一起使用,我们得到了这个未优化的输出(我已经修剪了我们不关心的标签):exitreturn 0main

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 转到标签

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

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

另请注意,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__,因为如果使用or选项asm编译它不会抛出警告。在clobbers 之后,您可以列出内联程序集可能使用的跳转目标。C实际上并不知道我们是否跳转,因为 GCC 不会分析内联汇编模板中的实际代码。它不能删除这个跳转,也不能假设后面是死代码。将Godbolt 与 GCC 4.6.4 一起使用,未优化的代码(修剪)如下所示:-ansi-std=?

main:
        pushl   %ebp
        movl    %esp, %ebp
        jmp .L2                   # <------ this is the goto exit
        jmp 0x0
.L2:                              # <------ exit label
        movl    $0, %eax
        popl    %ebp
        ret

带有 GCC 4.6.4 输出的Godbolt看起来仍然正确,并显示为:

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修饰符。

于 2018-02-12T21:59:18.727 回答
4

这行得通吗,让它让 gcc 无法知道它无法访问

int main(void)  
{ 
    volatile int y = 1;
    if (y) goto exit;
handler:
    __asm__ __volatile__("jmp 0x0");  
exit:   
    return 0; 
}
于 2012-06-19T16:20:54.543 回答
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__s 是__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;
}
于 2018-02-12T11:36:30.263 回答
1

更新 2012/6/18

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

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

即使保留-O3了:jmp

    testl   %eax, %eax   
    je      .L2     
.L3:
    jmp $0x0
.L2:
    xorl    %eax, %eax 
    ret

(这看起来真的很狡猾,所以我希望有更好的方法来做到这一点。编辑只需在作品volatile前面放一个,这样就不需要做内联 asm 诡计了。)x

于 2012-06-13T13:22:27.160 回答
1

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

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

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

int main(void)
{
    goto exit;

handler:
    func();

exit:
    return 0;
}

PS
如果您想在原始代码中的多个位置植入相同的“处理程序”代码块时避免代码冗余,此解决方案特别方便。

于 2012-06-17T16:16:34.300 回答
0

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

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

于 2013-03-01T08:39:56.087 回答