这是一个Clang错误
...当内联包含无限循环的函数时。当while(1);
直接出现在main中时,行为是不同的,这对我来说很臭。
有关摘要和链接,请参见@Arnavion的答案。该答案的其余部分是在我确认这是一个错误之前编写的,更不用说一个已知的错误了。
要回答标题问题:如何制作不会被优化的无限空循环??-
创建die()
宏而不是函数,以解决Clang 3.9及更高版本中的此错误。(早期的Clang版本要么保留循环,要么call
使用无限循环将其发送到函数的非内联版本。)即使该print;while(1);print;
函数内联到其调用方(Godbolt)中,这似乎也是安全的。 -std=gnu11
vs. -std=gnu99
没有任何改变。
如果您只关心GNU C,则循环内的P__J____asm__("");
也可以使用,并且不会影响任何了解它的编译器的周围代码的优化。GNU C Basic asm语句是隐式的volatile
,因此这被视为可见的副作用,必须像在C抽象机中那样“执行”多次。(是的,Clang实现了C的GNU方言,如GCC手册所记录。)
有人认为,优化空的无限循环可能是合法的。我不同意1,但是即使我们接受,Clang在循环不可到达之后承担语句,并让执行从函数的末尾落入下一个函数或垃圾中,这也是不合法的解码为随机指令。
(这符合Clang ++的标准(但仍然不是很有用);没有任何副作用的无限循环在C ++中是UB,但在C中不是UB。is
(1); C? UB中的未定义行为使编译器基本上可以发出任何东西对于肯定会遇到UB的执行路径上的代码,asm
循环中的语句将避免使用UB for C ++。但实际上,Clang编译为C ++不会删除常量表达式无限空循环,除非在内联时与编译为C。)
手动内联while(1);
更改Clang的编译方式:asm中存在无限循环。 这就是我们从规则律师POV所期望的。
#include <stdio.h>
int main() {
printf("begin\n");
while(1);
//infloop_nonconst(1);
//infloop();
printf("unreachable\n");
}
在Godbolt编译器资源管理器上,将Clang 9.0 -O3编译为C(-xc
)用于x86-64:
main: # @main
push rax # re-align the stack by 16
mov edi, offset .Lstr # non-PIE executable can use 32-bit absolute addresses
call puts
.LBB3_1: # =>This Inner Loop Header: Depth=1
jmp .LBB3_1 # infinite loop
.section .rodata
...
.Lstr:
.asciz "begin"
具有相同选项的相同编译器将首先编译到的a main
,然后在此之后停止发出指令。因此,正如我所说,执行只是落在函数的末尾,然后进入下一个函数(但堆栈未对齐函数入口,因此它甚至不是有效的尾调用)。infloop() { while(1); }
puts
main
有效的选择是
- 发出
label: jmp label
无限循环
- 或(如果我们接受无限循环可以删除的话)会发出另一个调用以打印第二个字符串,然后
return 0
从发出main
。
对于C11实现,崩溃或以其他方式继续而没有打印“ unreachable”显然不行,除非我没有注意到UB。
脚注1:
记录下来,我同意@Lundin的回答,该回答引用了标准,以证明C11不允许假设常量表达式无限循环的终止,即使它们为空(没有I / O,volatile,同步或其他)可见的副作用)。
这是一组条件,可以将循环编译为正常CPU 的空asm循环。(即使源代码中的主体不为空,在循环运行时,没有数据争用UB,其他线程或信号处理程序也看不到变量的赋值。因此,如果需要的话,一致的实现可以删除此类循环主体然后留下了是否可以删除循环本身的问题。ISOC11明确表示否。)
鉴于C11指出了这种情况,即实现无法假定循环终止(并且它不是UB),因此显然他们希望循环在运行时出现。以执行模型为目标的CPU的实现无法在有限时间内完成无限量工作的实现,没有理由删除空的常数无限循环。甚至一般来说,确切的用语是关于是否可以“假定终止”。如果循环无法终止,则意味着无论您对数学和无穷大采用什么参数,以及在某个假设的机器上进行无数次工作需要花费多长时间,都无法访问更高版本的代码。
除此之外,Clang不仅是符合ISO C的DeathStation 9000,它还旨在用于现实世界的低级系统编程,包括内核和嵌入式内容。 因此,无论您是否接受有关允许删除的C11的论点while(1);
,Clang都不愿意实际执行。如果您编写while(1);
,那可能不是偶然的。删除意外终止的无限循环(使用运行时变量控制表达式)可能会很有用,并且编译器可以这样做。
很少旋转直到下一个中断是很少见的,但是如果用C编写,那肯定是您期望发生的事情。(并且在GCC和Clang 中会发生什么,除了当无限循环位于包装函数内部时的Clang之外)。
例如,在原始OS内核中,当调度程序没有要运行的任务时,它可能会运行空闲任务。的第一个实现可能是while(1);
。
或者对于没有任何节能空闲功能的硬件,这可能是唯一的实现。(直到2000年代初,我认为这在x86上并不少见。尽管该hlt
指令确实存在,但IDK会在CPU开始具有低功耗空闲状态之前节省大量电量。)