我正在尝试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
有默认选择器,并试图制造一个肯定会崩溃的错误?
2条答案
按热度按时间m2xkgtsf1#
因此,您的代码正在写入地址零(NULL),其本身被定义为“未定义行为”。由于未定义行为涵盖了任何内容,而且对于这种情况来说最重要的是,“它做了你可能想象它会做的事情”(换句话说,写入地址为零而不是崩溃)。然后编译器决定通过添加
UD2
指令来告诉您。也有可能是为了防止信号处理程序继续进行进一步的未定义行为。是的,在大多数情况下,大多数机器都会因为
NULL
访问而崩溃。但这不是100%的保证,正如我上面所说的,可以在信号处理程序中捕获segfault,然后尝试继续-在尝试写入NULL
之后实际继续并不是一个好主意,所以编译器添加了UD2
以确保您不会继续。它使用了2个字节的内存,除此之外,我看不出它会有什么危害[毕竟,它是未定义的-如果编译器希望这样做,它可以从你的文件系统发送随机图片到英国女王。我认为UD 2是一个更好的选择。有趣的是,LLVM本身就可以做到这一点-我没有特殊的
NIL
指针访问检测,但我的pascal编译器编译了这个:进入:
我仍在试图弄清楚LLVM中发生这种情况的位置,并试图理解UD 2指令本身的目的。
我认为答案就在这里,在llvm/lib/Transforms/Utils/Local.cpp中
特别是中间的评论,它说“而不是落入随机代码”。在你的代码中,NULL访问之后没有代码,但是想象一下:
所以,这应该崩溃,但如果它不编译器将确保你不继续后,它.它使UB崩溃更明显一点(并且在这种情况下防止硬盘被格式化.)
我试图找出它是什么时候被引入的,但是这个函数本身起源于2007年,但是当时它并没有被用于这个目的,这使得很难弄清楚为什么它被这样使用。
envsm3lx2#
在Linux上,可以用
mmap
分配一个零地址,如this question中所讨论的,并使用生成的指针进行阅读和写入。因此,即使使用ds
中的默认选择器,写入[ds:0]
也不一定会崩溃。