以下使用seg-V崩溃:
// my code
int* ipt;
int bool set = false;
void Set(int* i) {
ASSERT(i);
ipt = i;
set = true;
}
int Get() {
return set ? *ipt : 0;
}
// code that I don't control.
struct S { int I, int J; }
int main() {
S* ip = NULL;
// code that, as a bug, forgets to set ip...
Set(&ip->J);
// gobs of code
return Get();
}
这是因为虽然i
不是NULL
,但它仍然无效。如果调用代码从NULL
指针获取数组索引操作的地址,则会发生同样的问题。
对此的一个解决方案是修剪低阶位:
void Set(int* i) {
ASSERT((reinterpret_cast<size_t>(i))>>10);
ipt = i;
set = true;
}
但我应该/可以摆脱多少位?
编辑,我并不担心未定义的行为,因为无论如何我都会流产(但比seg-v更干净)。
FWIW:这是一个半假设的情况。在我发布之前,导致我想到这个问题的错误已得到解决,但我之前遇到过它,并且正在考虑将来如何使用它。
为了争论可以假设的事情:
答案 0 :(得分:12)
除了NULL之外,没有可移植的方法来测试任何无效指针。在对它做任何事情之前,评估&ip[3]
会给出未定义的行为;唯一的解决方案是在对指针进行任何算术运算之前测试NULL 。
如果您不需要可移植性,并且不需要保证捕获所有错误,那么在大多数主流平台上,您可以检查地址是否在内存的第一页内;通常将NULL定义为地址零,并保留第一页以捕获大多数空指针解引用。在POSIX平台上,这看起来像
static size_t page_size = sysconf(_SC_PAGESIZE);
assert(reinterpret_cast<intptr_t>(i) >= page_size);
但这不是一个完整的解决方案。唯一真正的解决方案是首先解决滥用空指针的问题。
答案 1 :(得分:2)
你根本不应该在空指针之外进行指针运算(包括数组索引)。
你应该在c ++中使用0
,而不是NULL
。 NULL是c的一个特性,在c ++中仍然受支持但不是惯用的。
关于BCS的许多评论和编辑。这将问题从表面上相当天真的问题转变为更深层次的问题。但是......用一种像c ++那样宽松的语言来保护自己免受人们在做你的代码之前做蠢事的事情并不容易。
答案 2 :(得分:1)
尝试解决未定义的行为将始终非常依赖于您的平台,编译器,版本等。如果可能的话。
Common * nixes永远不会精确地映射地址空间的第一页以捕获空指针访问,因此您可能会检查指针值是否在0到4096之间(或者系统使用的任何页面大小)。
但是不要这样做,你无法防范可能出错的一切,而是专注于正确的代码。如果somone传递了一个无效的指针,那么无论如何指针验证检查都无法解决,可能会出现严重错误。
答案 3 :(得分:1)
有没有办法可以发挥一些影响力来纠正错误的代码?没有可能的方法可以很好地结果。从法律上讲,只是创建一个无效的指针是未定义的行为。
如果Set
始终从ip
传递一个小偏移量,并且ip
将始终初始化为NULL,那么你可能会对你正在做的事情做好。大多数现代系统确实将空指针保持为常量,因为所有位都为零,并且大多数都是自然的。当然,绝对不能保证它可以在任何给定的系统上使用任何给定的编译器和任何给定的编译器选项,并且更改其中任何一个可能会导致它失败。
由于使用坏指针会导致程序失败,因此您应该考虑代码触发内存冲突时会发生什么。
此外,我不知道您的ASSERT
宏的作用,但在大多数实现中,assert
仅在调试模式下激活。如果你想将这块垃圾投入生产,或者以优化模式运行,你可能需要确保它仍然会更温和地失败。
答案 4 :(得分:1)
如果你不介意一个非常糟糕的黑客攻击,你可以使用volatile
强制进行内存访问(n.b。volatile
是邪恶的)。根据GCC文档,必须跨序列点排序易失性访问,因此您可以执行以下操作:
int test = *(volatile int *)i;
*(volatile int *)i = test;
我认为=
不是序列点,但以下可能也有效:
*(volatile int *)i = *(volatile int *)i;
答案 5 :(得分:1)
我真的不建议尝试解决其他人的代码中的错误。如果您在开发代码时没有运行通过调试器编写的所有内容,那么任何数量的检查都无法帮助您解决所有问题。让他们修复他们的代码。
如果您没有使用调试器,请获取一个适当的崩溃处理程序,它可以转储每个线程的callstack以及尽可能多的有关程序状态的附加信息。试着找出可能出错的地方。
通过静态分析工具定期运行代码也可以提供帮助。
请记住,可能不会有人忘记初始化指针,可能是其他人通过从完全不相关的地方写入错误的内存来覆盖该指针。有些工具可以帮助追踪这些事情。
关于NULL Vs 0辩论,#define NULL 0
更好,原因有两个:
1)您可以更轻松地查看何时处理指针。
2)使用NULL提供的安全性不低于使用0.为什么不使代码更具可读性?
3)当C ++ 11最终发布时,#define NULL nullptr
比所有这些零更容易改变。 (今天我可以采用另一种方式和#define nullptr 0
,但如果您正在开发跨平台代码,那么将来可能会出现问题。)
对于记录,C ++标准明确声明空指针常量是一个计算结果为零的右值整数类型。所以,请不要再有关于空指针不必等于零的废话。
答案 6 :(得分:0)
其中一个原因,其中一个,你不能以便携方式做到这一点是NULL不能保证为0.只指定空指针将比较等于0.你可以写一个0(或预处理器宏)代码中的“NULL”),但编译器知道这个0在指针上下文中,因此它生成适当的代码以将其与空指针进行比较,无论空指针的实际实现是什么。有关详细信息,请参阅here和here。将NULL指针重新解释为整数类型可能会导致它具有true值而不是false。
答案 7 :(得分:0)
您必须考虑您的特定操作系统和硬件架构。如果您只对检测“接近null”的指针感兴趣,那么您可以使用ASSERT(i&gt; pageSize),假设您的操作系统中第一页始终处于写保护状态。
但是......显而易见的问题是:为什么要这么麻烦?操作系统将检测此情况下的空值和SEGV,正如您所指出的那样,这与ASSERT一样好,不是吗?