static void (*FP)();
extern void impl();
void set() { FP = impl; }
void call() {
FP();
}
这段代码的意图很明确,FP
首先被初始化成nullptr
,然后用户程序应该在什么时间调用set
,把FP
的值改成impl
的地址,随后再调用call
,这样就没有问题了…吧
对不起,编译器不是这样看的,首先dereference一个nullptr
是未定义行为,所以编译器可以假定这种情况不会发生,然后gcc9.2和clang9的处理方式还不相同
以下是gcc9.2的汇编代码
set():
mov QWORD PTR FP[rip], OFFSET FLAT:_Z4implv
ret
call():
jmp [QWORD PTR FP[rip]]
在set
里,impl
的地址被赋值给了FP
,保证了FP
不为空指针。call
里调用FP
指向的代码,看起来非常正确,完全符合我们的意图
但这不过是巧合而已
clang9的汇编代码如下
set(): # @set()
ret
call(): # @call()
jmp impl() # TAILCALL
set
直接被优化成了空函数,call
直接调用impl
,也就是说即使你的程序里有bug,set
在没被调用的情况下,依旧不会崩溃,这算好事呢还是坏事呢