gcc 写入[ds:0x0]后添加ud2的意义是什么?

v1l68za4  于 2023-10-19  发布在  其他
关注(0)|答案(2)|浏览(105)

我正在尝试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有默认选择器,并试图制造一个肯定会崩溃的错误?

m2xkgtsf

m2xkgtsf1#

因此,您的代码正在写入地址零(NULL),其本身被定义为“未定义行为”。由于未定义行为涵盖了任何内容,而且对于这种情况来说最重要的是,“它做了你可能想象它会做的事情”(换句话说,写入地址为零而不是崩溃)。然后编译器决定通过添加UD2指令来告诉您。也有可能是为了防止信号处理程序继续进行进一步的未定义行为。
是的,在大多数情况下,大多数机器都会因为NULL访问而崩溃。但这不是100%的保证,正如我上面所说的,可以在信号处理程序中捕获segfault,然后尝试继续-在尝试写入NULL之后实际继续并不是一个好主意,所以编译器添加了UD2以确保您不会继续。它使用了2个字节的内存,除此之外,我看不出它会有什么危害[毕竟,它是未定义的-如果编译器希望这样做,它可以从你的文件系统发送随机图片到英国女王。我认为UD 2是一个更好的选择。
有趣的是,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中发生这种情况的位置,并试图理解UD 2指令本身的目的。
我认为答案就在这里,在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年,但是当时它并没有被用于这个目的,这使得很难弄清楚为什么它被这样使用。

envsm3lx

envsm3lx2#

在Linux上,可以用mmap分配一个零地址,如this question中所讨论的,并使用生成的指针进行阅读和写入。因此,即使使用ds中的默认选择器,写入[ds:0]也不一定会崩溃。

相关问题