试图在复位后访问指针
Trying to access pointer after resetting
调试一个应用程序并做了一些实验,我发现了一个非常奇怪的行为,可以用下面的代码复制:
#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;
}
在我看来,所有三个变体都必须导致未定义行为。考虑到这一点,我有这些问题:
- 为什么
ref
和direct_p
仍然指向零?(不是10)(我的意思是,int
的破坏机制对我来说似乎很奇怪,编译器在未使用的内存上重写有什么意义?) - 为什么b)和c)不启动SIGSEGV?
- 为什么a)的行为不同于b)和c)?
p.reset();
相当于p.reset(nullptr);
。所以unique_ptr的内部指针被设置为空。因此,执行*p
的结果与尝试解引用null的原始指针的结果相同。
另一方面,ref
和direct_p
仍然指向先前被该int占用的内存。试图使用它们读取内存进入未定义行为领域,所以原则上我们不能得出任何结论…
但在实践中,有一些事情我们可以做出有根据的假设和猜测。
由于该内存位置不久前是有效的,当您的程序通过ref
和direct_p
访问它时,它很可能仍然存在(没有从地址空间中取消映射,或其他类似的特定于实现的东西)。c++并不要求内存完全不可访问。因此,在这种情况下,在程序执行过程中,你只是"成功"地读取了在该内存位置发生的任何内容。
至于为什么值恰好是0,有几种可能。一种情况是,您可能正在调试模式下运行,该模式有意将已释放的内存归零。另一种可能性是,当您通过ref
和direct_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_p
和ref
将指向100。注意,它们不是变量引用,而是内存引用。因此,您对p
所做的更改对它们没有影响。然后释放p
,它的值变成0
(即它现在指向内存地址0
)。尝试从内存地址0
读取值保证SIGSEGV
。从内存地址100
读取值是个坏主意,但不是致命的(如上所述)。
- C++ - 循环访问指针数组会导致错误
- 如何访问指针结构的成员变量?
- 从子线程访问指针
- 如何在C++中使用类对象访问指针数据成员
- 如何使用二维语法访问指针
- 我可以保证以负偏移访问指针吗?
- 是否访问指针元组和互斥锁线程安全
- 如何从嵌套类中访问指针
- C++ 如何访问指针的结构变量
- 从地图擦除后访问指针
- 会员在共享_ptr上访问指针
- 删除后访问指针
- 为什么在递增后使用 [] 运算符访问指针数组会返回错误地址的当前内存位置
- 如何在python脚本中使用Pybind11修改/访问C++指针
- 无法从另一个函数访问指针数组
- 无法访问指针C 的值
- 访问指针类成员 (C++)
- 访问指针数组中的子类方法
- 访问指针到结构中的向量
- 访问指针类型的静态属性