我知道一旦发生未定义行为,就不可能再考虑代码了。我完全相信这一点。我甚至认为我不应该过于深入理解未定义行为:一个健全的 C++ 程序不应该涉及未定义行为,就这样。
但是为了让我的同事和经理们确信它的真正危险性,我试图找到一个具体的例子,使用我们产品中存在的错误(他们认为这不是危险的,最多只会因为访问冲突而崩溃)。
我的主要关注点是在指向多态类的悬空指针上调用虚成员函数。
当一个指针被删除时,Windows 操作系统会在堆块的头部写入几个字节,并且通常也会覆盖堆块本身的前几个字节。这是它跟踪堆块,将它们管理为链表的方式...操作系统相关的内容。
尽管这在 C++ 标准中没有定义,但据我所知,多态是使用虚表来实现的。 在 Windows 下,指向虚表的指针位于堆块的前几个字节中,给定一个仅继承一个基类的类。(对于多重继承可能更为复杂,但我不会考虑这个。我们只考虑一个继承了 A 的类以及几个继承了 A 的 B、C、D 类。)
现在假设我有一个指向 A 的指针,它被实例化为 D 对象。并且该 D 对象已经在代码的其他位置被删除:因此堆块现在是一个自由堆块,其首字节已经被覆盖,因此虚表指针几乎随机地指向内存中的某个位置,比如说地址 0x01234567
。
当我们在代码中的某个地方调用:
void test(A * pA)
{
# here we do not know that pA is dangling pointer
# that memory address has been deleted by another thread, in another part of the code
pA->SomeVirtualFunction();
}
我说得对吗:
- 运行时将会把内存地址
0x01234567
的内容解释为虚表。 - 如果错误地把这个内存地址解释成虚表,但没有到达禁止访问的内存区域,就不会发生任何访问违规。
- 被误解的虚表将为虚函数提供一个随机地址,比如说
0x09876543
。 - 随机地址
0x09876543
处的内存将被解释为有效的二进制代码,并且真正地被执行。 - 这可能导致任何想象得到的损坏。
我不想夸大其词,只是想说明情况。所以,我的说法正确、可能和有可能发生吗?