以下程序如果永远不会调用never_called
在代码中调用?
#include <cstdio>
static void never_called()
{
std::puts("formatting hard disk drive!");
}
static void (*foo)() = nullptr;
void set_foo()
{
foo = never_called;
}
int main()
{
foo();
}
这与编译器不同。用Clang编译
优化时,函数never_called
在运行时执行。
$ clang++ -std=c++17 -O3 a.cpp && ./a.out
formatting hard disk drive!
使用GCC进行编译时,此代码只会崩溃:
$ g++ -std=c++17 -O3 a.cpp && ./a.out
Segmentation fault (core dumped)
编译器版本:
$ clang --version
clang version 5.0.0 (tags/RELEASE_500/final)
Target: x86_64-unknown-linux-gnu
Thread model: posix
InstalledDir: /usr/bin
$ gcc --version
gcc (GCC) 7.2.1 20171128
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
答案 0 :(得分:36)
程序包含未定义的行为,因为取消引用空指针
(即在main中调用foo()
而不为其分配有效地址
事先)是UB,因此标准没有要求。
在运行时执行never_called
是完全有效的情况
未定义的行为已经被击中,它就像崩溃一样有效(比如
用GCC编译时)。好的,但为什么Clang这样做?如果你
在优化关闭的情况下编译它,程序将不再输出
&#34;格式化硬盘驱动器&#34;,并且只会崩溃:
$ clang++ -std=c++17 -O0 a.cpp && ./a.out
Segmentation fault (core dumped)
此版本的生成代码如下:
main: # @main
push rbp
mov rbp, rsp
call qword ptr [foo]
xor eax, eax
pop rbp
ret
它尝试调用foo
指向的函数和foo
使用nullptr
进行初始化(或者如果它没有进行任何初始化,
这仍然是这种情况),其值为零。在这里,未定义
行为已经受到打击,所以任何事情都可能发生在程序中
变得无用。通常,调用此类无效地址
导致分段错误错误,因此我们得到的消息
执行程序。
现在让我们检查相同的程序,但是在优化时编译它:
$ clang++ -std=c++17 -O3 a.cpp && ./a.out
formatting hard disk drive!
此版本的生成代码如下:
set_foo(): # @set_foo()
ret
main: # @main
push rax
mov edi, .L.str
call puts
xor eax, eax
pop rcx
ret
.L.str:
.asciz "formatting hard disk drive!"
有趣的是,某种程度上的优化修改了程序
main
直接致电std::puts
。但为什么Clang这样做呢?为什么呢
set_foo
编译为单个ret
指令?
让我们暂时回到标准(N4660,具体而言)。什么 它是否说未定义的行为?
3.27未定义的行为[defns.undefined]
本文档没有要求的行为
[注意:本文档遗漏时可能会出现未定义的行为 任何明确的行为定义或当程序使用错误时 构造或错误的数据。允许的未定义行为范围 从完全忽略这种情况与不可预知的结果,到 在翻译期间表现或以文档化方式执行程序 环境的特征(有或没有发行 诊断消息),终止翻译或执行(使用 发布诊断消息)。许多错误的程序构造 不要产生未定义的行为;他们需要被诊断出来。 对常量表达式的评估从不明确地表现出行为 指定为undefined([expr.const])。 - 结束说明]
强调我的。
展示未定义行为的程序变得毫无用处
到目前为止它已经完成并且如果它包含的话将会进一步没有意义
错误的数据或结构。考虑到这一点,请记住这一点
编译器可能会完全忽略未定义行为的情况
被击中,这实际上被用作优化a时发现的事实
程序。例如,像x + 1 > x
这样的构造(其中x
是有符号整数)将被编译为
是的,即使x
的值在编译时未知。推理
是编译器想要优化有效案例,并且是唯一的
该构造有效的方法是它是否触发算术
溢出(即如果x != std::numeric_limits<decltype(x)>::max()
)。这个
是优化器中一个新的学习事实。基于此,构造是
被证明永远是真实的。
注意:对于无符号整数,不会发生同样的优化,因为溢出的不是UB。也就是说,编译器需要保持表达式不变,因为它在溢出时可能有不同的评估(unsigned是模块2 N ,其中N是位数)。对无符号整数进行优化将不符合标准(感谢aschepler。)
这很有用,因为它允许tons of optimizations to kick
in。所以
远,那么好,但如果x
在运行时保持其最大值会发生什么?
嗯,这是未定义的行为,所以试图推理它是无稽之谈
它,因为任何事情都可能发生,标准没有要求。
现在我们有足够的信息来更好地检查你的错误 程序。我们已经知道访问空指针是未定义的 行为,以及在运行时导致有趣行为的原因。 因此,让我们尝试理解为什么Clang(或技术上的LLVM)进行了优化 程序就像它一样。
static void (*foo)() = nullptr;
static void never_called()
{
std::puts("formatting hard disk drive!");
}
void set_foo()
{
foo = never_called;
}
int main()
{
foo();
}
请记住,可以在set_foo
条目之前调用main
开始执行。例如,当顶层声明变量时,
您可以在初始化该变量的值时调用它:
void set_foo();
int x = (set_foo(), 42);
如果您在main
之前编写此代码段,则程序编号为
更长时间展示未定义的行为,并且消息&#34;格式化很难
显示磁盘驱动器!&#34; ,优化开启或关闭。
那么这个程序有效的唯一方法是什么?这是set_foo
将never_called
的地址分配给foo
的函数,我们可能会这样做
在这找到一些东西请注意,foo
标记为static
,这意味着它
有内部联系,无法从此翻译外部访问
单元。相反,函数set_foo
具有外部链接,并且可以
从外面访问。如果另一个翻译单元包含一个片段
如上所述,那么这个程序就有效了。
很酷,但没有人从外面打电话给set_foo
。即使这样
事实上,优化器看到了这个程序的唯一方法
如果在set_foo
之前调用main
,则有效,否则为有效
只是未定义的行为。这是一个新的学到的事实,并假设set_foo
实际上是被称为。基于那些新知识,其他优化
踢进去可能会利用它。
例如,当constant
folding时
应用后,它会看到构造foo()
仅在foo
可以正确初始化时才有效。唯一的方法是在此翻译单元之外调用set_foo
,foo = never_called
。
Dead code elimination和interprocedural optimization可能会发现如果foo == never_called
,则set_foo
内的代码不需要,
所以它被转换成一条ret
指令。
Inline expansion优化
看到foo == never_called
,因此可以替换对foo
的调用
用它的身体。最后,我们最终会得到这样的结论:
set_foo():
ret
main:
mov edi, .L.str
call puts
xor eax, eax
ret
.L.str:
.asciz "formatting hard disk drive!"
这有点等同于优化的Clang输出。当然,Clang真正做到的可能(并且可能)会有所不同,但优化仍然能够得出相同的结论。
通过优化检查GCC的输出,似乎它没有打扰调查:
.LC0:
.string "formatting hard disk drive!"
never_called():
mov edi, OFFSET FLAT:.LC0
jmp puts
set_foo():
mov QWORD PTR foo[rip], OFFSET FLAT:never_called()
ret
main:
sub rsp, 8
call [QWORD PTR foo[rip]]
xor eax, eax
add rsp, 8
ret
执行该程序会导致崩溃(分段错误),但如果您在执行main之前在另一个翻译单元中调用set_foo
,则该程序不再显示未定义的行为。
随着越来越多的优化被设计,所有这些都可能会疯狂地改变,所以不要依赖于编译器会处理包含未定义行为的代码的假设,它可能只是搞砸了你(并格式化你的硬盘驱动器)真的!)
我建议你阅读What every C programmer should know about Undefined Behavior和A Guide to Undefined Behavior in C and C++,这两篇文章都非常有用,可以帮助你理解艺术现状。
答案 1 :(得分:0)
除非实现指定尝试调用空函数指针的效果,否则它可以表现为对任意代码的调用。这样的任意代码可以很好地表现得像对函数的调用&#34; foo()&#34;。虽然C标准的附件L将邀请实施来区分&#34;关键UB&#34;和&#34;非关键的UB&#34;,以及一些C ++实现可能会应用类似的区别,在任何情况下调用无效的函数指针都将是关键的UB。
请注意,此问题的情况与例如非常不同。
unsigned short q;
unsigned hey(void)
{
if (q < 50000)
do_something();
return q*q;
}
在后一种情况下,编译器不声称是可分析的&#34;可能会认识到,如果q在执行到达return
语句时大于46,340,则会调用代码,因此它也可以无条件地调用do_something()
。虽然附件L编写得很糟糕,但似乎打算禁止这样的优化&#34;。但是,在调用无效函数指针的情况下,即使在大多数平台上直接生成的代码也可能有任意行为。