【问题标题】:Why is out-of-bounds pointer arithmetic undefined behaviour?为什么越界指针算术未定义行为?
【发布时间】:2012-05-06 19:32:26
【问题描述】:

下面的例子是from Wikipedia

int arr[4] = {0, 1, 2, 3};
int* p = arr + 5;  // undefined behavior

如果我从不取消引用 p,那么为什么 arr + 5 单独是未定义的行为?我希望指针表现得像整数 - 除了在取消引用时指针的值被视为内存地址。

【问题讨论】:

  • 我相当确定“未定义”部分只是标准的说法,它不能告诉您该指针现在指向的位置。像大多数指针“未定义”的东西一样,我确信制作它是可以的,但尊重它是非法的。
  • @EthanSteinberg:只有当他们说生成的 value 未定义时才会如此。如果 behavior 未定义,则执行它是不安全的,即使您从未取消引用它。
  • 指针是不是整数。在底层,表示可能是巧合,但就“C++ 抽象机”而言,它们是完全不同的东西,它们恰好共享一些语法,例如 struct { int a; int x; }struct { char x; }
  • 因为并非所有机器的行为方式都与您的 PC 相同。您期望某种行为取决于它在您的机器上的工作方式。标准委员会有更多的经验,并且理解其他架构以不同的方式实现指针,因此不能保证所有平台上的上述行为(因此它是未定义的)。
  • 我发现这种未定义行为实际上导致计算错误的情况(在普通 x86 上):stackoverflow.com/questions/23683029/…

标签: c++ undefined-behavior


【解决方案1】:

这是因为指针的行为不像整数。这是未定义的行为,因为标准是这样说的。

但是,在大多数平台(如果不是全部)上,如果您不取消引用数组,您将不会崩溃或遇到可疑行为。但是,如果您不取消引用它,那么添加的意义何在?

也就是说,请注意,从技术上讲,在数组末尾加一个的表达式是 100%“正确”的,并且保证不会根据 C++11 规范的 §5.7 ¶5 崩溃。但是,该表达式的结果是 unspecified (只是保证不会溢出);而超过数组边界的任何其他表达式都是明确的 undefined 行为。

注意:这并不意味着从一倍的偏移量读取和写入是安全的。您可能编辑不属于该数组的数据,并且导致状态/内存损坏。你只是不会导致溢出异常。

我的猜测是,它是这样的,因为它不仅取消引用是错误的。还有指针算术、比较指针等。所以说不要这样做比列举它可能危险的情况更容易。

【讨论】:

  • 越界怎么样?
  • @MahmoudAl-Qudsi 标准说没问题,就是这样。
  • @MahmoudAl-Qudsi 只是 :) 有很多东西取决于它。像标准迭代器。
  • 你能澄清你的帖子吗?根据 C++11 规范的§5.7 ¶5,只有 expression 是有效的(即保证不会溢出/没有异常),而不是结果。我知道您没有说 result 已定义,但它很容易被误解为这样。 "如果指针操作数和结果都指向同一个数组对象的元素,或者超过数组对象的最后一个元素,则计算不应产生溢出;否则,行为未定义。"我>
  • 增加指向数组最后一个元素的指针的结果不是未指定的;它被指定为刚刚超过数组最后一个元素的指针;从这样的指针中减去从 1 到数组大小的值将产生一个指向数组元素的有效指针。
【解决方案2】:

原始 x86 可能存在此类语句的问题。在 16 位代码上,指针是 16+16 位。如果向低 16 位添加偏移量,则可能需要处理溢出并更改高 16 位。这是一个缓慢的操作,最好避免。

在这些系统上,array_base+offset 保证不会溢出,如果偏移量在范围内(array+5 会溢出。

溢出的结果是你得到了一个指针,它没有指向数组后面,而是指向数组之前。这甚至可能不是 RAM,而是内存映射硬件。如果您构造指向随机硬件组件的指针,即它是真实系统上的未定义行为,C++ 标准不会尝试限制会发生什么。

【讨论】:

    【解决方案3】:

    如果arr恰好位于机器内存空间的末尾,那么arr+5可能在该内存空间之外,因此指针类型可能无法表示该值,即它可能会溢出,溢出是未定义。

    【讨论】:

      【解决方案4】:

      “未定义的行为”并不意味着它必须在该行代码上崩溃,但它确实意味着您无法对结果做出任何保证。例如:

      int arr[4] = {0, 1, 2, 3};
      int* p = arr + 5; // I guess this is allowed to crash, but that would be a rather 
                        // unusual implementation choice on most machines.
      
      *p; //may cause a crash, or it may read data out of some other data structure
      assert(arr < p); // this statement may not be true
                       // (arr may be so close to the end of the address space that 
                       //  adding 5 overflowed the address space and wrapped around)
      assert(p - arr == 5); //this statement may not be true
                            //the compiler may have assigned p some other value
      

      我相信您还可以在这里举出许多其他示例。

      【讨论】:

      • arr+5 不是一个过去的末端,而是两个过去的末端,因此根据 §5.7 ¶5 它是 UB,它可能在具有陷阱表示的机器上崩溃指针。
      • 那是对已被删除的评论的回复,请忽略“不是过去的最后”部分。其余部分仍然适用,它可能崩溃,但我同意这将是不寻常的。
      【解决方案5】:

      一些系统,非常罕见的系统,我不能说出其中的一个,当你像这样增加边界时会导致陷阱。此外,它允许存在提供边界保护的实现......虽然我想不出一个。

      基本上,您不应该这样做,因此没有理由指定您这样做时会发生什么。指定会发生什么会给实施提供者带来不必要的负担。

      【讨论】:

      • 可以 做到这一点的系统实际上很常见,Intel x86(和兼容的)就是一个典型的例子。它通常不使用这种方式,但 x86 的基于段的内存保护按描述工作——它甚至可以抛出异常,甚至尝试形成无效地址。然而,大多数典型的操作系统将所有段设置为以 0 为基数和 4Gig 的限制,从而使所有可能的偏移都有效。值得一提的是,此功能实际上已在 OS/2 1.x 中使用。
      • @JerryCoffin:我希望英特尔在 80386 上使用 32 位段寄存器,上半部分选择段描述符,下半部分用作缩放乘数,其行为将由该段控制描述符。这样的架构可以使用 32 位对象引用而没有 4 GB 的寻址限制(不同对象的数量将限制在 40 亿以下,但它们的总大小可能会更大)。
      【解决方案6】:

      您看到的这个结果是由于 x86 的基于段的内存保护。我发现这种保护是合理的,因为当您增加指针地址并存储时,这意味着在您的代码中的未来时间点,您将取消引用指针并使用该值。因此,编译器希望避免这种情况,您最终会更改其他人的内存位置或删除代码中其他人拥有的内存。为了避免这种情况的编译器已经设置了限制。

      【讨论】:

        【解决方案7】:

        除了硬件问题之外,另一个因素是尝试捕获各种编程错误的实现的出现。尽管如果配置为捕获程序已知不使用的结构,许多这样的实现可能是最有用的,即使它们是由 C 标准定义的,但标准的作者不想定义结构的行为,这些行为会 - -在许多编程领域--出现错误。

        在许多情况下,捕获使用指针算法来计算非预期对象地址的操作要比以某种方式记录指针不能用于访问它们标识的存储这一事实要容易得多,但可以修改为他们可以访问其他存储。除了较大(二维)数组中的数组之外,允许实现保留“刚刚过去”每个对象末尾的空间。给定类似doSomethingWithItem(someArray+i); 的东西,实现可以捕获任何试图传递任何不指向数组元素或刚刚超过最后一个元素的空间的地址的尝试。如果分配someArray 为额外未使用的元素保留空间,并且doSomethingWithItem() 仅访问它接收到的指针的项目,则实现可以相对便宜地确保上述代码的任何非陷阱执行都可以--at最糟糕的——访问其他未使用的存储空间。

        计算“刚刚过去”地址的能力使得边界检查比其他情况更加困难(最常见的错误情况是传递 doSomethingWithItem() 一个刚刚超过数组末尾的指针,但行为将是除非doSomethingWithItem 会尝试取消引用该指针,否则已定义——调用者可能无法证明这一点)。因为标准允许编译器在大多数情况下保留刚刚超过数组的空间,但是,这种允许将允许实现限制未捕获的错误造成的损害——如果允许更通用的指针算术,这可能是不切实际的。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-01-08
          • 2011-10-11
          • 1970-01-01
          相关资源
          最近更新 更多