我正在尝试使用GCC,试图说服它假定代码的某些部分不可访问,以便捕获机会进行优化。我的一项实验给了我一些奇怪的代码。来源:

#include <iostream>

#define UNREACHABLE {char* null=0; *null=0; return {};}

double test(double x)
{
    if(x==-1) return -1;
    else if(x==1) return 1;
    UNREACHABLE;
}

int main()
{
    std::cout << "Enter a number. Only +/- 1 is supported, otherwise I dunno what'll happen: ";
    double x;
    std::cin >> x;
    std::cout << "Here's what I got: " << test(x) << "\n";
}

这是我的编译方式:
g++ -std=c++11 test.cpp -O3 -march=native -S -masm=intel -Wall -Wextra
test函数的代码如下所示:
_Z4testd:
.LFB1397:
        .cfi_startproc
        fld     QWORD PTR [esp+4]
        fld1
        fchs
        fld     st(0)
        fxch    st(2)
        fucomi  st, st(2)
        fstp    st(2)
        jp      .L10
        je      .L11
        fstp    st(0)
        jmp     .L7
.L10:
        fstp    st(0)
        .p2align 4,,10
        .p2align 3
.L7:
        fld1
        fld     st(0)
        fxch    st(2)
        fucomip st, st(2)
        fstp    st(1)
        jp      .L12
        je      .L6
        fstp    st(0)
        jmp     .L8
.L12:
        fstp    st(0)
        .p2align 4,,10
        .p2align 3
.L8:
        mov     BYTE PTR ds:0, 0
        ud2         // This is redundant, isn't it?..
        .p2align 4,,10
        .p2align 3
.L11:
        fstp    st(1)
.L6:
        rep; ret

让我感到奇怪的是.L8上的代码。即,它已经写入了零地址,这保证了分段错误,除非ds具有一些非默认选择器。那么,为什么要附加ud2?写入零地址是否已确保崩溃?还是GCC不相信ds具有默认选择器并试图确保崩溃?

最佳答案

因此,您的代码正在写入地址零(NULL),该地址本身被定义为“未定义的行为”。由于未定义的行为涵盖了所有内容,并且最重要的是,在这种情况下,“它做到了您可能想像的那样”(换句话说,写地址为零而不是崩溃)。然后,编译器决定通过添加UD2指令来告诉您。也有可能防止信号处理程序从其他不确定的行为继续进行。

是的,在大多数情况下,大多数机器将因NULL访问而崩溃。但这不是100%保证的,而且正如我在上面说的那样,可以在信号处理程序中捕获segfault,然后尝试继续-在尝试写入NULL之后实际上继续操作不是一个好主意,因此编译器将UD2添加到确保您不会继续...它使用的内存多了2个字节,除此之外,我看不到它有什么害处[毕竟,不确定发生了什么-如果编译器希望这样做,则可以通过电子邮件随机发送图片从您的文件系统到英国女王...我认为UD2是更好的选择...]

有趣的是,LLVM本身就是这样做的-我没有对NIL指针访问进行特殊检测,但是我的pascal编译器对此进行了编译:

program p;

var
   ptr : ^integer;

begin
   ptr := NIL;
   ptr^ := 42;
end.

变成:
0000000000400880 <__PascalMain>:
  400880:   55                      push   %rbp
  400881:   48 89 e5                mov    %rsp,%rbp
  400884:   48 c7 05 19 18 20 00    movq   $0x0,0x201819(%rip)        # 6020a8 <ptr>
  40088b:   00 00 00 00
  40088f:   0f 0b                   ud2

我仍在尝试找出LLVM中的这种情况,并尝试了解UD2指令本身的目的。

我认为答案就在这里,在llvm / lib / Transforms / Utils / Local.cpp中
void llvm::changeToUnreachable(Instruction *I, bool UseLLVMTrap) {
  BasicBlock *BB = I->getParent();
  // Loop over all of the successors, removing BB's entry from any PHI
  // nodes.
  for (succ_iterator SI = succ_begin(BB), SE = succ_end(BB); SI != SE; ++SI)
    (*SI)->removePredecessor(BB);

  // Insert a call to llvm.trap right before this.  This turns the undefined
  // behavior into a hard fail instead of falling through into random code.
  if (UseLLVMTrap) {
    Function *TrapFn =
      Intrinsic::getDeclaration(BB->getParent()->getParent(), Intrinsic::trap);
    CallInst *CallTrap = CallInst::Create(TrapFn, "", I);
    CallTrap->setDebugLoc(I->getDebugLoc());
  }
  new UnreachableInst(I->getContext(), I);

  // All instructions after this are dead.
  BasicBlock::iterator BBI = I->getIterator(), BBE = BB->end();
  while (BBI != BBE) {
    if (!BBI->use_empty())
      BBI->replaceAllUsesWith(UndefValue::get(BBI->getType()));
    BB->getInstList().erase(BBI++);
  }
}

特别是中间的注释,它说“而不是陷入随机代码中”。在您的代码中,没有跟随NULL访问的代码,但可以想象一下:
void func()
{
    if (answer == 42)
    {
     #if DEBUG
         // Intentionally crash to avoid formatting hard disk for now

         char *ptr = NULL;
         ptr = 0;
    #endif
         // Format hard disk.
         ... some code to format hard disk ...
    }
    printf("We haven't found the answer yet\n");
    ...
}

因此,这应该崩溃,但是如果没有崩溃,编译器将确保您在此之后不再继续...这使UB崩溃更加明显(并且在这种情况下,防止格式化硬盘...)

我试图找出何时引入此功能,但该功能本身起源于2007年,但当时并未完全用于此目的,这使得很难弄清为什么使用此功能。

10-08 08:08