重置后尝试访问指针

时间:2015-08-18 22:27:23

标签: c++ pointers

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

#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)不同?

4 个答案:

答案 0 :(得分:1)

未定义的行为并不意味着代码必须触发异常终止。这意味着任何事情都可能发生。异常终止只是一种可能的结果。不确定行为的不同实例之间的行为不一致是另一种情况。另一种可能的(虽然在实践中很少见)似乎“正常工作”(然而一个人定义“正常工作”)直到下一个满月,然后神秘地表现不同。

从提高普通程序员技能和提高软件质量的角度来看,编程人员在编写具有未定义行为的代码时可能会被认为是可取的。

答案 1 :(得分:1)

  

为什么ref和direct_p指向零? (不是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读取值是个坏主意,但不是致命的(如上所述)。

答案 2 :(得分:1)

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

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

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

由于该内存位置很快就会生效,因此当程序通过ref和{{direct_p访问它时,它很可能仍然存在(尚未从地址空间或其他此类特定于实现的内容中取消映射)。 1}}。 C ++并不要求内存完全无法访问。因此,在这种情况下,您只需“成功”读取程序执行期间该位置处发生的任何内容。

至于为什么值恰好是0,那么有几种可能性。一个是您可以在调试模式下运行,该模式有目的地将释放的内存清零。另一种可能性是,当您通过refdirect_p访问该内存时,其他内容已经将其重新用于其他目的,最终将其保留为该值。您的std::cout << *p << "\n";行可能会这样做。

答案 3 :(得分:1)

正如其他人所说未定义的行为意味着任何事情都可能发生。代码是不可预测的。但是,让我试着用一个例子来解释问题'b'。

SIGSEGV归因于硬件使用MMU(内存管理单元)报告的硬件故障。您的内存保护级别以及因此抛出的SIGSEGV级别在很大程度上取决于您的硬件使用的MMU( source )。如果您的未分配指针恰好指向一个正确的地址,您将能够读取它们的内存,如果它指向某个不好的地方,那么您的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;
}

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