我正在尝试使用 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";
}
Run Code Online (Sandbox Code Playgroud)
我是这样编译的:
g++ -std=c++11 test.cpp -O3 -march=native -S -masm=intel -Wall -Wextra
Run Code Online (Sandbox Code Playgroud)
函数的代码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
Run Code Online (Sandbox Code Playgroud)
让我想知道的是.L8. 也就是说,它已经写入零地址,这保证了分段错误,除非ds有一些非默认选择器。那么为什么要额外的ud2呢?写入零地址不是已经保证会崩溃吗?或者 GCC 不相信ds有默认选择器并试图确保崩溃?
因此,您的代码正在写入地址零(NULL),它本身被定义为“未定义的行为”。由于未定义的行为涵盖了所有内容,最重要的是对于这种情况,“它会执行您可能想象的操作”(换句话说,写入地址零而不是崩溃)。然后编译器决定通过添加一条UD2指令来告诉您这一点。也有可能是为了防止信号处理程序继续执行进一步未定义的行为。
是的,大多数机器在大多数情况下都会因NULL访问而崩溃。但这并不是 100% 保证,正如我上面所说,人们可以在信号处理程序中捕获段错误,然后尝试继续 - 在尝试写入后继续实际上并不是一个好主意NULL,因此编译器会添加UD2以确保您不要继续...它使用了 2 个字节以上的内存,除此之外我看不出它有什么危害[毕竟,它是未定义的发生的事情 - 如果编译器希望这样做,它可以通过电子邮件发送随机图片你的文件系统给英国女王...我认为 UD2 是一个更好的选择...]
有趣的是,LLVM 自己完成了这个工作 - 我没有对NIL指针访问进行特殊检测,但我的 pascal 编译器编译了这个:
program p;
var
ptr : ^integer;
begin
ptr := NIL;
ptr^ := 42;
end.
Run Code Online (Sandbox Code Playgroud)
进入:
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
Run Code Online (Sandbox Code Playgroud)
我仍在尝试找出 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++);
}
}
Run Code Online (Sandbox Code Playgroud)
特别是中间的评论,它说“而不是陷入随机代码”。在您的代码中,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");
...
}
Run Code Online (Sandbox Code Playgroud)
所以,这应该崩溃,但如果没有崩溃,编译器将确保您不会在它之后继续...它使 UB 崩溃更加明显(在这种情况下会阻止硬盘被格式化...)
我试图找出这个功能是什么时候引入的,但这个功能本身起源于 2007 年,但当时它并没有完全用于这个目的,这使得很难弄清楚为什么会这样使用它。