【问题标题】:Is storing an invalid pointer automatically undefined behavior?存储无效指针是否自动未定义的行为?
【发布时间】:2011-04-19 19:49:28
【问题描述】:

显然,取消引用无效指针会导致未定义的行为。但是在指针变量中简单地存储一个无效的内存地址呢?

考虑以下代码:

const char* str = "abcdef";
const char* begin = str;
if (begin - 1 < str) { /* ... do something ... */ }

表达式begin - 1 的计算结果为无效的内存地址。请注意,我们实际上并没有取消引用这个地址——我们只是在指针运算中使用它来测试它是否有效。尽管如此,我们仍然需要将无效的内存地址加载到寄存器中。

那么,这是未定义的行为吗?我从没想过是这样,因为很多指针算术似乎都依赖于这种东西,而指针实际上只不过是一个整数。但是最近我听说即使将无效指针加载到寄存器中的行为也是未定义的行为,因为如果您这样做,某些架构会自动抛出总线错误或其他东西。任何人都可以指出 C 或 C++ 标准的相关部分来解决这个问题吗?

【问题讨论】:

  • 根据 C/C++ staqndard,这确实是未定义的行为。但是,坦率地说,我从未见过真实世界的 CPU/架构在上面是未定义的行为,即不允许任意指针运算的机器。我见过很多架构,包括嵌入式微控制器。所以,在我(谦虚)看来,代码是可以的,只要你把自己限制在现代非深奥的架构中。
  • 你能否扩展这个问题 - 如果你有循环,你在哪里向后遍历数组?在这个遍历中,你肯定需要在第一个元素之前检查元素,而不是取消引用它。我有类似的问题,但它是最后一个元素之后的元素。

标签: c++ c pointers


【解决方案1】:

几年前就已经给出了正确答案,但我觉得有趣的是C99 rationale [sec. 6.5.6,最后 3 段]解释了为什么标准支持将 1 添加到指向数组最后一个元素的指针 (p+1):

普遍实践的一个重要认可是要求指针始终可以递增到刚好超过数组的末尾,而不必担心溢出或回绕

以及为什么不支持p-1

另一方面,在 p-1 的情况下,必须在 p 遍历的对象数组之前分配整个对象,因此从数组底部运行的递减循环可能会失败。例如,此限制允许分段架构将对象放置在可寻址内存范围的开头。

因此,如果指针 p 指向该注释认可的可寻址内存范围开头的对象,则 p-1 将产生下溢。

请注意,整数溢出是未定义行为的标准示例 [sec. 3.4.3],因为它取决于翻译环境和操作环境。我相信很容易看出这种对环境的依赖延伸到了指针下溢。

这就是标准明确将其设为未定义行为的原因 [在 6.5.6/8 中],正如此处其他答案所指出的那样。引用那句话:

如果指针操作数和结果都指向同一个数组对象的元素,或者超过数组对象的最后一个元素,则计算不应产生溢出;否则,行为未定义。

另见[秒。 C99 基本原理的 6.3.2.3,最后 4 段],它更详细地描述了无效指针是如何生成的,以及可能产生的影响。

【讨论】:

    【解决方案2】:

    一些体系结构有专门的寄存器来保存指针。将未映射地址的值放入这样的寄存器是允许崩溃的。允许整数上溢/下溢崩溃。由于 C 旨在在各种平台上工作,因此指针提供了一种安全编程不安全电路的机制。

    如果您知道自己不会在具有如此挑剔特征的外来硬件上运行,则无需担心语言未定义的内容。它由平台明确定义。

    当然,这个例子的风格很糟糕,没有充分的理由这样做。

    【讨论】:

    • 它在平台上定义良好的事实并不能使其在针对该平台的所有实现上都得到良好定义。即使底层平台愿意,也不能依赖那些对“优化”而不是支持低级编程更感兴趣的编译器来可靠地处理此类代码。
    • @supercat 这是一个很好的观点,你在技术上是正确的。然而,在实践中,当一个编译器变得如此激进以至于ptr = arr - 1; 变成一个无操作(或崩溃,或者……)时,它的用户可能会非常沮丧,以至于他们会去寻找其他编译器。虽然标准允许这样做,但这种行为是如此微妙的病态,而且这种计算是如此普遍,以至于它很少是一个可行的解决方案。
    • 像 gcc 和 clang 这样的编译器似乎很受欢迎,尽管它们的行为在理智的时候会被认为是令人发指的。标准的作者将短无符号类型提升为有符号的原因之一是,根据基本原理,即使x*y 大于INT_MAX。然而,GCC 有时会以破坏的方式“优化”该功能。
    • @supercat 是的,这是另一个常年抱怨的来源。尽管如此,还是更容易捕捉到这种错误。越界计算有时很难避免,也很难在代码中看到。 C++ 正在引入 std::launder 来选择性地祝福这些值,但实际上指定该函数与您预期的一样奇怪。
    • @Evg m68k 有地址寄存器,我不能 100% 确定,但未映射的地址加载注释可能指的是 IA64。
    【解决方案3】:

    是的,这是未定义的行为。请参阅 the accepted answerthis closely related question。将无效指针分配给变量、比较无效指针、强制转换无效指针都会触发未定义的行为。

    【讨论】:

      【解决方案4】:

      $5.7/6 - “除非两个指针都指向 到同一数组对象的元素, 或一个过去的最后一个元素 数组对象,行为是 未定义.75)"

      总结,它是未定义的即使你没有取消引用指针

      【讨论】:

      • 该文本涉及从指针中减去指针; OP 正在从一个指针中减去一个整数。
      • @James McNellis:我猜这就是指针算法。最终是关于结果指针值
      • 我不确定您的推理,从不同的数组中减去两个指针实际上可能会遇到问题,因为指针指向不同的内存区域(想想 16 位架构中的远/近内存)。这里并没有涉及到指针本身的干预,实际上使用 64 位指针的高位来存储附加标志是很常见的。
      【解决方案5】:

      由于不同的原因,您的代码是未定义的行为:

      表达式begin - 1 确实产生一个无效的指针。这是未定义的行为。不允许您在正在处理的数组范围之外执行指针算术。因此,减法本身是无效的,而不是存储结果指针的行为。

      【讨论】:

      • C99 基本原理(在我的回答中链接到)特别提到超出数组边界的指针算术会产生无效指针。
      • 如果将表达式修改为(ptrdiff_t)begin - 1,还会产生未定义的行为吗?由于 ptrdiff_t 必须是有符号整数类型,我认为这没问题。
      • 只能为指向同一数据对象的两个指针计算 ptrdiff_t。 “在数组范围内”的唯一例外是指针 one 超出数组的end
      • @fizzer:我这里没有 C++ 标准(几天前格式化了我的计算机,仍然需要从我的备份中获取它),但它声明这是未定义的。我不知道 C 是否有不同的做法,但我想这只是处理 实际 发生的事情的基本原理(实际上,你只是得到一个无效的指针),但标准更多严格并说“这是一个无意义的操作,它是未定义的”。
      • @Channel72: 是的,只要满足以下条件即可:(1) sizeof(ptrdiff_t) &gt;= sizeof(void*)(不一定保证),(2) 将begin 转换为已签名的结果整数类型ptrdiff_t 不会导致该类型可表示的最小值(如果是,则减法将导致未定义的行为),并且(3)实现一致地定义了指针到整数的转换,以便你可以将这个表达式的结果与(ptrdiff_t)str的结果进行比较,得到一个有意义的结果(也不能保证)。
      【解决方案6】:

      我这里有 C 草案标准,它通过遗漏使其未定义。它在 6.5.6/8 处定义了ptr + I 的情况

      • 如果指针操作数指向数组对象的一个​​元素,并且数组足够大,则结果指向一个与原始元素偏移的元素,使得结果和原始数组元素的下标之差等于整数表达式。
      • 此外,如果表达式 P 指向数组对象的最后一个元素,则表达式 (P)+1 指向数组对象的最后一个元素,如果表达式 Q 指向数组对象的最后一个元素,则数组对象,表达式 (Q)-1 指向数组对象的最后一个元素。

      您的情况不符合其中任何一项。您的数组也不够大,无法让-1 调整指针以指向不同的数组元素,任何结果或原始指针也不会指向过去。

      【讨论】:

      • 这是未定义或未指定的行为。我希望代码能够运行和工作并且不会产生不良后果,尽管它进入 if 分支的天气是不可知的(通过标准)。
      • @Martin York:C++ 标准将其定义为未定义的行为,即使它没有被取消引用。我希望我已经在我的帖子中找到了相关的报价
      • 这种行为可能会导致硬件出现硬件故障,从而验证指针寄存器的内容。因此,它是未定义的行为。特定实现可以指定如果程序按照标准执行各种会引起未定义行为的事情时会发生什么,并且是允许的。如果一个实现符合它自己的规范,那么行为将是明确定义的。但是,如果代码在符合 C 标准但不符合该特定实现规范的不同实现上运行,则程序可能会以任意方式失败。
      • @supercat 是正确的:在某些 CPU 上,将无效指针加载到寄存器中本身会导致程序崩溃,因此保证它会起作用会禁用很多优化。
      • @Lorehead:“优化”一词的现代用法是指编译器应该积极识别会调用 UB 的情况,并得出结论认为变​​量不能保存会导致这种情况出现的值。例如,给定代码if (p != 0) doSomething(p); debug_log(*p);,“现代”优化编译器可以得出结论,无条件调用doSomething 是安全的,因为如果“p”为空,即使在目标平台上读取空指针只会产生一个无意义的值。
      【解决方案7】:

      任何无效指针的使用都会产生未定义的行为。我这里没有 C 标准,但请参阅基本原理中的“无效指针”:http://www.open-std.org/jtc1/sc22/wg14/www/C99RationaleV5.10.pdf

      【讨论】:

      • 如果是这样的话,你不能在做指针运算时把所有的指针都转换成ptrdiff_t吗?换句话说,如果我将上面的代码示例更改为if ((ptrdiff_t)begin - 1),那将不再是未定义的行为吗?
      • 不是未定义的行为,但结果是实现定义的。也就是说,您的实现将记录一些合理的行为,但它不是可移植的,并且可能没有用处。
      • comp.lang.c 常见问题解答解决了这个问题:c-faq.com/ptrs/int2ptr.html。就像我说的,我手头没有标准。
      • 请注意,ptrdiff_t 将保存 指针之间的差异,而不是指针本身。这不是一回事。
      猜你喜欢
      • 2018-11-05
      • 2011-12-04
      • 2017-01-27
      • 2014-06-10
      • 1970-01-01
      • 1970-01-01
      • 2017-02-17
      • 2022-07-19
      • 1970-01-01
      相关资源
      最近更新 更多