试图在复位后访问指针

Trying to access pointer after resetting

本文关键字:访问 指针 复位      更新时间:2023-10-16

调试一个应用程序并做了一些实验,我发现了一个非常奇怪的行为,可以用下面的代码复制:

#include <iostream>
#include <memory>
int main()
{
    std::unique_ptr<int> p(new int);
    *p = 10;
    int& ref = *p;    
    int* direct_p = &(*p);
    p.reset();
    std::cout << *p << "n";        // a) SIGSEGV
    std::cout << ref << "n";       // b) 0
    std::cout << *direct_p << "n"; // c) 0
    return 0;
}

在我看来,所有三个变体都必须导致未定义行为。考虑到这一点,我有这些问题:

  1. 为什么refdirect_p仍然指向零?(不是10)(我的意思是,int的破坏机制对我来说似乎很奇怪,编译器在未使用的内存上重写有什么意义?)
  2. 为什么b)和c)不启动SIGSEGV?
  3. 为什么a)的行为不同于b)和c)?

p.reset();相当于p.reset(nullptr);。所以unique_ptr的内部指针被设置为空。因此,执行*p的结果与尝试解引用null的原始指针的结果相同。

另一方面,refdirect_p仍然指向先前被该int占用的内存。试图使用它们读取内存进入未定义行为领域,所以原则上我们不能得出任何结论…

但在实践中,有一些事情我们可以做出有根据的假设和猜测。

由于该内存位置不久前是有效的,当您的程序通过refdirect_p访问它时,它很可能仍然存在(没有从地址空间中取消映射,或其他类似的特定于实现的东西)。c++并不要求内存完全不可访问。因此,在这种情况下,在程序执行过程中,你只是"成功"地读取了在该内存位置发生的任何内容。

至于为什么值恰好是0,有几种可能。一种情况是,您可能正在调试模式下运行,该模式有意将已释放的内存归零。另一种可能性是,当您通过refdirect_p访问该内存时,其他一些东西已经出于不同的目的重用了它,最终将其保留为该值。您的std::cout << *p << "n";行可能已经这样做了。

未定义的行为并不意味着代码必须触发异常终止。这意味着任何事情都有可能发生。异常终止只是一种可能的结果。未定义行为的不同实例之间的行为不一致是另一个问题。另一种可能(尽管在实践中很少)是看起来"正常工作"(无论人们如何定义"正常工作")直到下一个满月,然后神秘地表现不同。

从提高平均程序员技能和提高软件质量的角度来看,当程序员编写带有未定义行为的代码时,电击他们可能是可取的。

正如其他人所说未定义行为字面意思是任何事情都可能发生。密码是不可预测的。但让我试着用一个例子来阐明问题b。

SIGSEGV是由带有MMU (Memory management unit)的硬件上报的硬件故障。您的内存保护级别以及因此引发的SIGSEGV级别在很大程度上取决于您的硬件所使用的MMU (source)。如果你的未分配的指针碰巧指向一个ok的地址,你将能够读取内存,如果它指向某个不好的地方,那么你的MMU将会崩溃,并与你的程序引发一个SIGSEGV。

以MPC5200为例。这个处理器相当老旧,有一个有点简陋的MMU。要让它崩溃导致段错误是相当困难的。

例如,以下命令不一定会在MPC5200上导致SIGSEGV:

int *p = NULL;
*p;
*p = 1;
printf("%d", *p); // This actually prints 1 which is insane

唯一能让它抛出段错误的方法是用下面的代码:

int *p = NULL;
while (true) {
  *(--p) = 1;
}

总结一下,未定义行为确实意味着未定义。

为什么ref和direct_p指向0 ?(不是10)(我是说int的破坏机制在我看来很奇怪,有什么意义呢让编译器重写未使用的内存?)

改变内存的不是编译器,而是c++/C库。在您的特殊情况下,libc做了一些有趣的事情,因为它在释放值时重新分配堆数据:

Hardware watchpoint 3: *direct_p
_int_free (have_lock=0, p=0x614c10, av=0x7ffff7535b20 <main_arena>) at malloc.c:3925
3925        while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2)) != old2);

为什么b)和c)不触发SIGSEGV?

如果试图访问已分配地址空间之外的内存,内核将触发

SIGSEGV。通常情况下,libc在释放内存后不会真正删除页面——这样做的代价太大了。你正在写一个未被libc映射的地址,但是内核不知道。您可以使用内存屏障库(例如ElectricFence,非常适合调试)来实现这一点。

为什么a)的行为不同于b)和c)?

你让p的值指向某个内存,比如100。然后有效地为该内存位置创建别名,因此direct_pref将指向100。注意,它们不是变量引用,而是内存引用。因此,您对p所做的更改对它们没有影响。然后释放p,它的值变成0(即它现在指向内存地址0)。尝试从内存地址0读取值保证SIGSEGV。从内存地址100读取值是个坏主意,但不是致命的(如上所述)。