我正在尝试使用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年,但当时并未完全用于此目的,这使得很难弄清为什么使用此功能。