Clang 编译器对未定义行为的不一致性

2023/02/16 LLVM 共 3075 字,约 9 分钟

从一个有趣的例子说起

一个很简单的 CPP 语言样本, 代码如下:

// main.cpp
#include <iostream>

int main(int argc, char* argv[]) {
    while (1);
}

void myfunc() {
    std::cout << "Hello World!";
}

使用如下命令编译代码并运行:

$ clang++ -O1 -g main.cpp -o main.elf
$ ./main.elf
Hello World!

显然, 这个代码时不符合我们对程序的理解的. 由于 main 函数中为一个死循环, 且没有发生任何的 myfunc 函数调用, 因此第 8 行的输出应该是不会被执行的. 但是运行结果却显示改代码被执行了.

这个原因是因为, 编译器在处理到 while(1);语句时, 认为该语句及后续代码为一个 unreachable 语句, 此时生成的 IR 内容如下:

define dso_local noundef i32 @main(i32 noundef %0, i8** nocapture noundef readnone %1) local_unnamed_addr #3 !dbg !920 {
  unreachable, !dbg !926
}

define dso_local void @_Z6myfuncv() local_unnamed_addr #4 !dbg !927 {
  ...
  %1 = call noundef nonnull align 8 dereferenceable(8) %"class.std::basic_ostream"* @_ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l(%"class.std::basic_ostream"* noundef nonnull align 8 dereferenceable(8) @_ZSt4cout, i8* noundef nonnull getelementptr inbounds ([13 x i8], [13 x i8]* @.str, i64 0, i64 0), i64 noundef 12), !dbg !997
  ...
}

显然, while(1); 语句在 IR 层面被优化为了 unreachable 语句. 尝试定位该语句是何时被优化的, 使用如下命令重新编译源代码, 并输出 LLVM Pass 每一次的优化结果:

# -O1 启用优化, 等级1
# -mllvm 将后续参数传递给 LLVM 工具链
# -print-after-all LLVM 工具链参数, 用于输出整个编译流程中每个 LLVM pass 的详细信息
# 2>&1 LLVM 工具链的输出全部采用 llvm::errs() 输出到 stderr 中, 需要重定向到 stdout 中
clang -O1 -g -Wall -mllvm -print-after-all main.cpp -o main.elf > opt_step.txt 2>&1

使用正则表达式检查, 原本的 br 语句在何时被替换为 unreachable, 发现 LoopDeletionPass 将此死循环进行了优化. 分析此 Pass, 发现在 ./llvm/lib/Transforms/Utils/LoopUtils.cpp 文件中的 llvm::deleteDeadLoop 中, 将没有出口的死循环, 通过 Builder.CreateUnreachable(); 替换为了 unreachable 指令.

成因分析

此部分优化与 多线程优化 有关. 具体的, 在 C++ 标准中 ` Multi-threaded executions and data races` 章节的第 24 条有如下说明:

The implementation may assume that any thread will eventually do one of the following:

— terminate,

— make a call to a library I/O function,

— access or modify a volatile object, or

— perform a synchronization operation or an atomic operation.

[ Note: This is intended to allow compiler transformations such as removal of empty loops, even when termination cannot be proven. — end note ]

翻译为中文如下:

该实现可能假定任何线程最终都会执行以下操作之一:

— 终止,

— 调用库 I/O 函数,

— 访问或修改易失性对象,或

— 执行同步操作或原子操作。

[注意:这是为了允许编译器转换,例如删除空循环,即使无法证明终止。

即, 多线程中的代码至少会执行一个有意义的指令, 如果一个线程(包括主线程)不会执行这些操作, 如遇到死循环(infinite loop) 等, 编译器可能会将其优化.

LLVM 13.0 以上版本, llvm/lib/CodeGen/SelectionDAG/SelectionDAGBuilder.cpp 中, 即编译过程中处理 DAG(Directed Acyclic Graph) 的过程中, visitUnreachable 会处理 IR 代码中的 ``unreachable 指令, 并将其忽略.

void SelectionDAGBuilder::visitUnreachable(const UnreachableInst &I) {
  // 判断 -trap-unreachable 选项是否开启, 如果开启则不进行此优化
  if (!DAG.getTarget().Options.TrapUnreachable)
    return;

  // 忽略无返回值调用中的所有不可达指令
  if (DAG.getTarget().Options.NoTrapAfterNoreturn) {
    const BasicBlock &BB = *I.getParent();
    if (&I != &BB.front()) {
      BasicBlock::const_iterator PredI =
        std::prev(BasicBlock::const_iterator(&I));
      if (const CallInst *Call = dyn_cast<CallInst>(&*PredI)) {
        if (Call->doesNotReturn())
          return;
      }
    }
  }
  // 插入 TRAP 指令抛出异常
  DAG.setRoot(DAG.getNode(ISD::TRAP, getCurSDLoc(), MVT::Other, DAG.getRoot()));
}

总结

  1. 为了避免此问题, 可以编译时开启 -trap-unreachable 选项, 编译命令如下:
clang++ -O1 -Wall -mllvm -trap-unreachable main.cpp -o main.elf
  1. 谨慎对待未定义行为!

-mllvm -trap-unreachable https://godbolt.org/z/oMre33sa1

-mllvm -print-after-all https://godbolt.org/z/8EdhdzEK1

相关讨论

Stackoverflow https://stackoverflow.com/questions/3592557/optimizing-away-a-while1-in-c0x

CPP Standard https://isocpp.org/files/papers/N3690.pdf Page 14 : 24

文档信息

搜索

    Table of Contents