在代码中未调用的函数在运行时被调用 [英] Function not called in code gets called at runtime

查看:230
本文介绍了在代码中未调用的函数在运行时被调用的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

如果以下程序从未在代码中调用
,如何调用 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 
格式化硬盘驱动器!

然而,使用GCC进行编译时,这段代码崩溃了:

  $ g ++ -std = c ++ 17 -O3 a.cpp&& ./a.out 
分段错误(核心转储)

编译器版本:

  $ clang --version 
ng版本5.0.0(tags / RELEASE_500 / final)
目标:x86_64-未知-linux-gnu
线程模型:posix
InstalledDir:/ usr / bin
$ gcc --version
gcc(GCC)7.2.1 20171128
版权所有C)2017免费软件基金会,Inc.
这是免费软件;请参阅复制条件的来源。没有任何b $ b保修;甚至不适用于适销性或针对特定用途的适用性。


解决方案

程序包含未定义的行为,
(即在main中调用 foo()而不预先给它赋予一个有效地址
)为UB,因此标准没有要求。

在运行时执行 never_called 是一个完美的有效情况,当
未定义行为被触发时,就像碰撞一样有效(就像用GCC编译时的
一样)。好的,但是Clang为什么这样做呢?如果您的
在优化关闭的情况下进行编译,程序将不再输出
格式化硬盘驱动器,并且只会崩溃:

  $ clang ++ -std = c ++ 17 -O0 a.cpp&& ./a.out 
分段错误(核心转储)

此版本的生成代码如下所示:

  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& amp ;&安培; ./a.out 
格式化硬盘驱动器!

此版本的生成代码如下所示:

  set_foo():#@set_foo()
ret
main:#@main
push rax
mov edi,。 L.str
call puts
xor eax,eax
pop rcx
ret
.L.str:
.asciz格式化硬盘驱动器!

有趣的是,某种程度上的优化修改了程序,使
main 直接调用 std :: puts 。但为什么铿锵会这样做呢?为什么
set_foo 编译成单个 ret 指令?



让我们回到标准(特别是N4660)一会儿。对于未定义的行为,说什么



3.27未定义的行为 [defns.undefined]



这个文件没有要求的行为

p>

[注意:未定义的行为可能是预期的,当此文档省略
的任何显式行为定义或当程序使用错误的
结构或错误的数据。
允许的未定义行为的范围
完全忽略情况,结果不可预知,
/ strong>或程序以文件记录的方式执行
特征环境(无论是否发行
诊断消息),终止翻译或执行(在
发行a诊断消息)。许多错误的程序结构
不会导致未定义的行为;他们需要被诊断。
对常量表达式的评估决不会显式地将
指定为undefined([expr.const])。 - 结束注释]

强调我的功能。



一个展示未定义行为变得毫无用处,因为迄今为止所做的所有
,如果它包含
的错误数据或构造,那么它就没有意义。考虑到这一点,请记住
编译器可能会完全忽略未定义行为
被触发的情况,并且在优化
程序时,这实际上被用作发现事实。例如,像 x + 1> x (其中 x 是一个有符号整数)将被编译为
true,即使 x 在编译时是未知的。推理
是编译器想要针对有效的情况进行优化,并且该构造的唯一
方法是有效的,如果它不触发算术
溢出(即如果 x!= std :: numeric_limits< decltype(x)> :: max())。这
是优化器中一个新的学习事实。基于这一点,构造是
被证明始终是正确的。



注意:对于无符号整数,因为溢出一个不是UB。也就是说,编译器需要保持表达式的原样,因为它在溢出时可能有不同的评估(无符号是模块2 ,其中N是位数)。优化它的无符号整数将与标准不兼容(谢谢aschepler。)



这非常有用,因为它允许大量的优化措施可以激活
。所以
远远好,但如果 x 在运行时保持最大值会发生什么?
好​​吧,这是未定义的行为,因此试图推断
it是无稽之谈,因为可能发生任何事情,标准没有要求。



现在我们有足够的信息,以便更好地检查您有问题的
程序。我们已经知道访问一个空指针是未定义的
行为,这就是在运行时导致有趣的行为。
所以我们来试试并理解为什么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 主要条目
开始执行之前,c $ c>。例如,当顶层声明变量
时,可以在初始化该变量的值时调用它:

  void set_foo(); 
int x =(set_foo(),42);

如果你在 main 之前写这段代码,程序no
会显示未定义的行为,并显示消息格式化硬
磁盘驱动器!
,并优化打开或关闭。



那么这个程序有效的唯一途径是什么?这是 set_foo
函数,它将 never_called 的地址赋给 foo ,所以我们可能
在这里找到一些东西。请注意, foo 被标记为 static ,这意味着
具有内部链接并且无法访问从外部翻译
单位。相反,函数 set_foo 具有外部链接,并且可以从外部访问
。如果另一个翻译单元包含一个像上面那样的片段
,那么这个程序将变得有效。



很酷,但没有人调用来自外部的set_foo 。尽管这个
是事实,但优化器认为该程序的
有效的唯一方法是在<$ c之前调用 set_foo $ c> main ,否则它的
只是未定义的行为。这是一个新的学问,它假定实际上调用了 set_foo
。基于这些新知识,其他优化可能会利用



例如,当常量
折叠
是应用
,它看到构造 foo()只有在 foo 可以正确初始化时才有效。发生这种情况的唯一方法是在该翻译单元之外调用 set_foo ,因此 foo = never_called



消除死码 interprocedural optimization 可能会发现如果 foo == never_called ,那么 set_foo 中的代码是不需要的,
,因此它被转换成单个 ret 指令。



内联展开式优化
可以看到 foo == never_called ,所以对 foo 的调用可以用它的主体替换
。最后,我们得到了这样的结果:

  set_foo():
ret
main :
mov edi,.L.str
调用放置
xor eax,eax
ret
.L.str:
.asciz格式化硬盘驾驶!

这与Clang的优化结果相当。当然,Clang真的可以(也可能)会有所不同,但优化仍然能够得出相同的结论。



通过优化检查GCC的输出,看起来好像它没有打扰调查:

  .LC0:
.string格式化硬盘驱动器!
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

执行该程序会导致崩溃(分段故障),但如果您致电















$随着越来越多的优化被设计出来,这可能会变得很疯狂,所以不要依赖于你的编译器会处理包含未定义行为的代码,它可能也会让你失望(并且将你的硬盘格式化为真正的!)。





我建议您阅读每个C程序员应该知道的有关未定义行为 C和C ++中的未定义行为指南,这两个系列文章都非常翔实,可能会帮助您了解最新技术。


How can the following program be calling never_called if it's never called in code?

#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();
}

This differs from compiler to compiler. Compiling with Clang with optimizations on, the function never_called executes at runtime.

$ clang++ -std=c++17 -O3 a.cpp && ./a.out
formatting hard disk drive!

Compiling with GCC, however, this code just crashes:

$ g++ -std=c++17 -O3 a.cpp && ./a.out
Segmentation fault (core dumped)

Compilers version:

$ 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.

解决方案

The program contains undefined behavior, as dereferencing a null pointer (i.e. calling foo() in main without assigning a valid address to it beforehand) is UB, therefore no requirements are imposed by the standard.

Executing never_called at runtime is a perfect valid situation when undefined behavior has been hit, it's as valid as just crashing (like when compiled with GCC). Okay, but why is Clang doing that? If you compile it with optimizations off, the program will no longer output "formatting hard disk drive", and will just crash:

$ clang++ -std=c++17 -O0 a.cpp && ./a.out
Segmentation fault (core dumped)

The generated code for this version is as follows:

main:                                   # @main
        push    rbp
        mov     rbp, rsp
        call    qword ptr [foo]
        xor     eax, eax
        pop     rbp
        ret

It tries to make a call to a function to which foo points, and as foo is initialized with nullptr (or if it didn't have any initialization, this would still be the case), its value is zero. Here, undefined behavior has been hit, so anything can happen at all and the program is rendered useless. Normally, making a call to such invalid address results in segmentation fault errors, hence the message we get when executing the program.

Now let's examine the same program but compiling it with optimizations on:

$ clang++ -std=c++17 -O3 a.cpp && ./a.out
formatting hard disk drive!

The generated code for this version is as follows:

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!"

Interestingly, somehow optimizations modified the program so that main calls std::puts directly. But why did Clang do that? And why is set_foo compiled to a single ret instruction?

Let's get back to the standard (N4660, specifically) for a moment. What does it say about undefined behavior?

3.27 undefined behavior [defns.undefined]

behavior for which this document imposes no requirements

[Note: Undefined behavior may be expected when this document omits any explicit definition of behavior or when a program uses an erroneous construct or erroneous data. Permissible undefined behavior ranges from ignoring the situation completely with unpredictable results, to behaving during translation or program execution in a documented manner characteristic of the environment (with or without the issuance of a diagnostic message), to terminating a translation or execution (with the issuance of a diagnostic message). Many erroneous program constructs do not engender undefined behavior; they are required to be diagnosed. Evaluation of a constant expression never exhibits behavior explicitly specified as undefined ([expr.const]). — end note]

Emphasis mine.

A program that exhibits undefined behavior becomes useless, as everything it has done so far and will do further has no meaning if it contains erroneous data or constructs. With that in mind, do remember that compilers may completely ignore for the case when undefined behavior is hit, and this actually is used as discovered facts when optimizing a program. For instance, a construct like x + 1 > x (where x is a signed integer) will be compiled to true, even if the value of x is unknown at compile-time. The reasoning is that the compiler wants to optimize for valid cases, and the only way for that construct to be valid is if it doesn't trigger arithmetic overflow (i.e. if x != std::numeric_limits<decltype(x)>::max()). This is a new learned fact in the optimizer. Based on that, the construct is proven to always be true.

Note: this same optimization can't occur for unsigned integers, because overflowing one is not UB. That is, the compiler needs to keep the expression as it is, because it might have a different evaluation when it overflows (unsigned is module 2N, where N is number of bits). Optimizing it away for unsigned integers would be incompliant with the standard (thanks aschepler.)

This is useful as it allows for tons of optimizations to kick in. So far, so good, but what happens if x holds its maximum value at runtime? Well, that is undefined behavior, so it's nonsense to try to reason about it, as anything may happen and the standard imposes no requirements.

Now we have enough information in order to better examine your faulty program. We already know that accessing a null pointer is undefined behavior, and that's what's causing the funny behavior at runtime. So let's try and understand why Clang (or technically LLVM) optimized the program the way it did.

static void (*foo)() = nullptr;

static void never_called()
{
  std::puts("formatting hard disk drive!");
}

void set_foo()
{
  foo = never_called;
}

int main()
{
  foo();
}

Remember that it's possible to call set_foo before the main entry starts executing. For example, when top-level declaring a variable, you can call it while initializing the value of that variable:

void set_foo();
int x = (set_foo(), 42);

If you write this snippet before main, the program no longer exhibits undefined behavior, and the message "formatting hard disk drive!" is displayed, with optimizations either on or off.

So what's the only way this program is valid? There's this set_foo function that assigns the address of never_called to foo, so we might find something here. Note that foo is marked as static, which means it has internal linkage and can't be accessed from outside this translation unit. In contrast, the function set_foo has external linkage, and may be accessed from outside. If another translation unit contains a snippet like the one above, then this program becomes valid.

Cool, but there's no one calling set_foo from outside. Even though this is the fact, the optimizer sees that the only way for this program to be valid is if set_foo is called before main, otherwise it's just undefined behavior. That's a new learned fact, and it assumes set_foo is in fact called. Based on that new knowledge, other optimizations that kick in may take advantage of it.

For instance, when constant folding is applied, it sees that the construct foo() is only valid if foo can be properly initialized. The only way for that to happen is if set_foo is called outside of this translation unit, so foo = never_called.

Dead code elimination and interprocedural optimization might find out that if foo == never_called, then the code inside set_foo is unneeded, so it's transformed into a single ret instruction.

Inline expansion optimization sees that foo == never_called, so the call to foo can be replaced with its body. In the end, we end up with something like this:

set_foo():
        ret
main:
        mov     edi, .L.str
        call    puts
        xor     eax, eax
        ret
.L.str:
        .asciz  "formatting hard disk drive!"

Which is somewhat equivalent to the output of Clang with optimizations on. Of course, what Clang really did can (and might) be different, but optimizations are nonetheless capable of reaching the same conclusion.

Examining GCC's output with optimizations on, it seems it didn't bother investigating:

.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

Executing that program results in a crash (segmentation fault), but if you call set_foo in another translation unit before main gets executed, then this program doesn't exhibit undefined behavior anymore.

All of this can change crazily as more and more optimizations are engineered, so do not rely on the assumption that your compiler will take care of code containing undefined behavior, it might just screw you up as well (and format your hard drive for real!)


I recommend you read What every C programmer should know about Undefined Behavior and A Guide to Undefined Behavior in C and C++, both article series are very informative and might help you out with understanding the state of art.

这篇关于在代码中未调用的函数在运行时被调用的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆