gcc删除内联汇编程序代码
似乎gcc 4.6.2删除了它认为从函数中未使用的代码。
test.c的
int main(void) { goto exit; handler: __asm__ __volatile__("jmp 0x0"); exit: return 0; }
main()
拆卸
0x08048404 : push ebp 0x08048405 : mov ebp,esp 0x08048407 : nop # <-- This is all whats left of my jmp. 0x08048408 : mov eax,0x0 0x0804840d : pop ebp 0x0804840e : ret
编译器选项
没有启用优化,只有gcc -m32 -o test test.c
( -m32
因为我在64位机器上)。
我怎么能阻止这种行为?
编辑:最好通过使用编译器选项,而不是通过修改代码。
更新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
(这看起来很狡猾,所以我希望有更好的方法来做到这一点。 编辑只是在x
前面放一个volatile
就可以了,所以不需要做内联asm技巧。)
看起来就是这样 – 当gcc
看到函数中的代码无法访问时,它会删除它。 其他编译器可能不同。
在gcc
,编译的早期阶段是构建“控制流图” – 一个“基本块”的图形,每个条件都没有条件,通过分支连接。 在发出实际代码时,将丢弃无法从根访问的图形部分。
这不是优化阶段的一部分,因此不受编译选项的影响。
所以任何解决方案都会让gcc
认为代码是可以访问的。
我的建议:
您可以将它放在一个可到达的地方,并跳过有问题的指令,而不是将汇编代码放在一个无法到达的地方(GCC可能会删除它)。
int main(void) { goto exit; exit: __asm__ __volatile__ ( "jmp 1f\n" "jmp $0x0\n" "1:\n" ); return 0; }
另外,请参阅此主题有关该问题 。
我不相信有一种可靠的方法只使用编译选项来解决这个问题。 无论用于编译的选项如何,优选的机制都可以完成工作并在未来版本的编译器上工作。
关于接受的答案的评论
在接受的答案中,对原始文件进行了编辑,建议使用此解决方案:
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转到标签
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__
over asm
因为如果使用-ansi
或-std=?
编译它不会发出警告-std=?
选项。 在clobbers之后,您可以列出内联汇编可能使用的跳转目标。 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
具有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
语句是隐式volatile。 这条线__asm__ __volatile__("jmp 0x0");
可以写成:
__asm__ ("jmp 0x0");
-
asm goto
语句被认为是隐式不稳定的。 它们也不需要volatile
改性剂。
这会有用吗,让它如此gcc无法知道它无法到达
int main(void) { volatile int y = 1; if (y) goto exit; handler: __asm__ __volatile__("jmp 0x0"); exit: return 0; }
如果编译器认为它可以欺骗你,只需作弊:(仅限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; }
我从来没有听说过防止gcc删除无法访问的代码的方法; 似乎无论你做什么,一旦gcc检测到无法访问的代码,它总是将其删除(使用gcc的-Wunreachable-code
选项来查看它认为无法访问的内容)。
也就是说,您仍然可以将此代码放在静态函数中,并且不会对其进行优化:
static int func() { __asm__ __volatile__("jmp $0x0"); } int main(void) { goto exit; handler: func(); exit: return 0; }
PS
如果您希望在原始代码中的多个位置植入相同的“处理程序”代码块时避免代码冗余,则此解决方案特别方便。
gcc可以在函数内复制asm语句并在优化期间删除它们(即使在-O0),因此这将永远无法可靠地工作。
可靠地执行此操作的一种方法是使用全局asm语句(即任何函数之外的asm语句)。 gcc会将此直接复制到输出中,您可以毫无问题地使用全局标签。