0

以下使用 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:这是一个半假设的情况。导致我想到这一点的错误在我发布之前已修复,但我之前遇到过它并且正在考虑将来如何使用它。

为了论证而可以假设的事情:

  • 如果用 seg-v 的东西调用 Set,这是一个错误
  • Set 可能会被我不负责修复的代码调用。(例如我提交了一个错误)
  • 我试图修复的代码可能会调用 Set 。(例如,我在调试工作中添加了完整性检查。)
  • 以不提供有关调用 Set 的位置的信息的方式调用我。(即允许 Get 到 seg-v 不是调试任何东西的有效方法。)
  • 代码不需要是可移植的或捕获 100% 的错误指针。它只需要经常在我当前的系统上运行,就足以让我找到哪里出了问题。
4

8 回答 8

12

没有可移植的方法来测试除 NULL 之外的任何无效指针。在你对它做任何事情之前,评估&ip[3]会给出未定义的行为;唯一的解决方案是在对指针进行任何算术之前测试 NULL 。

如果你不需要可移植性,也不需要保证捕获所有错误,那么在大多数主流平台上你可以检查地址是否在内存的第一页内;通常将 NULL 定义为地址零,并保留第一页以捕获大多数空指针取消引用。在 POSIX 平台上,这看起来像

static size_t page_size = sysconf(_SC_PAGESIZE);
assert(reinterpret_cast<intptr_t>(i) >= page_size);

但这不是一个完整的解决方案。唯一真正的解决方案是首先解决任何滥用空指针的问题。

于 2010-09-15T17:38:18.857 回答
2

您根本不应该对空指针进行指针运算(包括数组索引)。

你应该使用0, 而不是NULL在 C++ 中。NULL 是 c 的一个特性,在 c++ 中仍然受支持但不是惯用的。


关于 BCS 的许多评论和编辑。这将问题从表面上相当幼稚的问题转变为更深层次的问题。但是……在调用你的代码之前保护自己免受人们做愚蠢事情的影响并不容易——用一种像 c++ 这样宽松的语言。

于 2010-09-15T17:40:28.807 回答
1

我真的不建议尝试解决其他人代码中的错误。如果您在开发代码时没有运行通过调试器编写的所有内容,那么再多的检查也无法帮助您发现所有问题。让他们修复他们的代码。

如果您不使用调试器,请获得一个体面的崩溃处理程序,该处理程序转储每个线程的调用堆栈以及有关程序状态的尽可能多的附加信息。试着找出可能出了什么问题。

通过静态分析工具定期运行您的代码也可以提供帮助。

请记住,可能不是有人忘记初始化指针,而是其他人通过从完全不相关的某个地方写入错误的内存来覆盖该指针。也有一些工具可以帮助追踪此类事情。

关于 NULL Vs 0 辩论,#define NULL 0由于以下几个原因更好:

1)当你处理指针时,你可以更容易地看到。

2) 使用 NULL 提供的安全性不亚于使用 0。那么为什么不让你的代码更具可读性呢?

3) 当 C++11 最终发布#define NULL nullptr时,比所有这些零更容易改变。(你可以走另一条路,#define nullptr 0今天我想,但如果你正在开发跨平台代码,这可能会在未来引起问题。)

为了记录,C++ 标准明确指出空指针常量是一个右值整数类型,其计算结果为零。所以请不要再胡说八道空指针不必等于零。

于 2010-09-15T18:41:13.900 回答
1

有什么方法可以施加一些影响来纠正错误的代码?不可能有好的结果。从法律上讲,仅仅创建一个无效的指针是未定义的行为。

如果Set总是从 传递一个小的偏移量ip,并且ip总是被初始化为 NULL,那么你可能会对你正在做的事情感到满意。大多数现代系统确实将空指针常量作为所有位为零,并且大多数都会做自然的事情。当然,绝对不能保证它可以在具有任何给定编译器和任何给定编译器选项的任何给定系统上工作,并且更改其中任何一个都可能导致它失败。

由于任何错误指针的使用都可能导致程序失败,因此您应该考虑当代码触发内存违规时会发生什么。

另外,我不知道你的ASSERT宏做了什么,但是assert,在大多数实现中,只在调试模式下被激活。如果你想把这块垃圾推到生产环境中,或者在优化模式下运行,你可能想确保它仍然会更温和地失败。

于 2010-09-15T18:26:28.760 回答
1

如果您不介意非常糟糕的 hack,您可以使用volatile(nb volatileis evil) 强制进行内存访问。根据 GCC 文档,可变访问必须跨序列点排序,因此您可以执行以下操作:

int test = *(volatile int *)i;
*(volatile int *)i = test;

我不认为这=是一个序列点,但以下可能也有效:

*(volatile int *)i = *(volatile int *)i;
于 2010-09-15T18:31:14.760 回答
1

尝试解决未定义的行为将始终非常依赖于您的平台、编译器、版本等。如果可能的话。

常见的 *nixes 从不精确映射地址空间的第一页以捕获空指针访问,因此您可能会检查指针值是否在 0 和 4096 之间(或系统使用的任何页面大小)。

但是不要这样做,你不能防范所有可能出错的事情,而是专注于让代码正确。如果某人向您传递了一个无效的指针,那么很可能存在指针验证检查无法修复的严重错误。

于 2010-09-15T17:47:09.647 回答
0

在许多情况下,您不能以可移植方式执行此操作的一个原因是 NULL 不能保证为 0。仅指定空指针将比较等于 0。您可以编写 0(或预处理器宏“NULL” ) 在您的代码中,但编译器知道这个 0 在指针上下文中,因此它生成适当的代码以将其与空指针进行比较,无论空指针的实际实现是什么。有关这方面的更多信息,请参见此处此处。将 NULL 指针重新解释为整数类型可能会导致它具有真值而不是假。

于 2010-09-15T17:55:59.673 回答
0

您必须考虑您的特定操作系统和硬件架构。如果您只对检测“接近 null”的指针感兴趣,那么您可以使用 ASSERT(i > pageSize),假设第一页在您的操作系统中始终受到写保护。

但是......显而易见的问题是:为什么要打扰?操作系统将在这种情况下检测空值,并且正如您指出的那样,SEGV 与 ASSERT 一样好,不是吗?

于 2010-09-15T18:51:43.330 回答