这是来自this tweet的代码的变体,只是较短的代码而不会对新手造成任何损害。我们有这个代码:
typedef int (*Function)();
static Function DoSmth;
static int Return7()
{
return 7;
}
void NeverCalled()
{
DoSmth = Return7;
}
int main()
{
return DoSmth();
}
你看到代码中从未调用NeverCalled()
,不是吗?以下是使用
-Os -std=c++11 -Wall
发出的代码是:
NeverCalled():
retq
main:
movl $7, %eax
retq
好像在NeverCalled()
之前实际调用DoSmth()
并将DoSmth
函数指针设置为Return7()
函数。
如果从NeverCalled()
内部删除了函数指针赋值,如下所示:
void NeverCalled() {}
然后发出的代码就是:
NeverCalled():
retq
main:
ud2
后者是非常期待的。编译器知道函数指针肯定是null并且使用null函数指针调用函数是未定义的行为。
以前的代码并不是真正的预期。不知怎的,编译器决定调用Return7()
,虽然它不是直接调用任何地方,函数指针赋值是在未调用的函数内部。
是的,我知道面向具有未定义行为的代码的编译器允许通过C ++ Standard执行此操作。它是如何做到的?
clang是如何发出这个特定的机器代码的?
答案 0 :(得分:3)
NeverCalled
用词不当。可能会调用任何全局函数(例如,通过不同转换单元中的全局对象的构造函数)。
顺便说一句,这是这个TU可能被合并到一个没有UB的程序中的唯一方法。在这种情况下,main
返回7.
使NeverCalled
为静态,main
将编译为空代码。
答案 1 :(得分:2)
clang执行此操作的路径可能与“
”类似DoSmth
是static
,因此初始化为零。因为它是一个指针(对函数)具有初始化为NULL
指针(或nullptr
)main()
确实return DoSmth()
因此DoSmth
不能NULL
的原因,因为这会导致return DoSmth()
展示未定义的行为; DoSmth = Return7
中有作业NeverCalled()
; DoSmth
设置为非NULL的语句,并且它认为DoSmth
不是NULL,因此clang假定NeverCalled()
必须是不知何故; DoSmth
必须等于Return7
的地址; DoSmth == Return7
,clang会将return DoSmth()
转换为return Return7()
; Return7()
在同一个编译单元中,所以clang内联它。clang在内部如何做到这一点的具体细节是任何人的猜测。但是,代码优化的各个步骤可能会导致类似上述的推理链。
关键是您的代码 - 就其本身而言 - 具有未定义的行为。未定义行为的一个可爱特征是允许编译器(与要求不同)来推断您的代码实际上具有明确定义的行为。反过来,这允许编译器推断某些确保行为定义良好的代码已被神奇地执行。