【问题标题】:Rationale for pointer comparisons outside an array to be UB数组外部指针比较为 UB 的基本原理
【发布时间】:2015-07-01 01:19:39
【问题描述】:

因此,标准(参考N1570)对比较指针有以下说明:

C99 6.5.8/5 关系运算符

当比较两个指针时,结果取决于相对的 指向的对象的地址空间中的位置。 ... [剪断聚合内比较的明显定义] ... 在所有其他情况下, 行为未定义。

这个 UB 实例的基本原理是什么,而不是指定(例如)转换为 intptr_t 并进行比较?

是否有一些机器架构难以构建合理的指针总排序?是否存在某种类型的优化或分析会阻碍无限制的指针比较?

this question 的已删除答案提到,这段 UB 允许跳过段寄存器的比较,只比较偏移量。保存起来特别有价值吗?

(相同的已删除答案,以及此处的一个,请注意,在 C++ 中,std::less 等需要实现指针的总顺序,无论普通比较运算符是否执行。)

【问题讨论】:

  • 委员会没有在其标准的官方理由中回答这个具体问题,所以任何答案都是推测性的,除非可能来自委员会成员。尽管如此,规范相关方面的官方理由考虑了聚合对象表示附近字节的分段架构和可寻址性,所以我倾向于猜测@cheersandhth 在他的回答中提到 C 的那部分是正确的。跨度>
  • 分段架构让它变得很糟糕——正如在英特尔 80386 和相关产品上发现的那样。在过去 80286、80186、8086、8088 的糟糕日子里,这尤其痛苦。同一个地址可以有多个位表示。即使使用现代 Pentiums 和 x86_64s 等,理论上您也可以拥有分段地址;不过,没有人疯狂到可以使用它们。好吧,几乎没有人——我敢肯定还有人仍然在运行带有分段地址的软件。
  • 这不是一个基于意见的问题,据我所知,C 和 C++ 标准中的所有未定义行为都有其合理性。它们是否易于从公共资源中辨别是另一个问题,但这并不会使它们成为坏问题。
  • @JonathanLeffler 这在微控制器中仍然很常见,因为在 70 年代、80 年代和 90 年代发明的旧 8/16 位微控制器架构仍在生产中使用。您将拥有“存储”内存和各种奇怪的指针类型来访问它。许多嵌入式程序员无缘无故地狂热地坚持这种陈旧的废话。
  • @Lundin:标准中没有任何内容要求整数表示的比较产生与指针比较相同的结果,即使在后者定义明确的情况下也是如此。嵌入式程序员通常需要能够调整现有设计。如果一个产品已经畅销十年并且人们继续购买它,但某个部件变得不可用,那么能够将现有代码用于新部件可能比从头开始重写所有内容更可取。对于需要与其他设备交互的产品(这可能取决于某些...

标签: c pointers language-lawyer undefined-behavior


【解决方案1】:

ub 邮件列表讨论中的各种 cmets Justification for < not being a total order on pointers? 强烈暗示分段架构是原因。包括关注cmets,1

另外,我认为核心语言应该简单地认识到当今所有机器都具有平坦内存模型的事实。

2:

那么我们可能需要一个新类型来保证总订单 从指针转换而来(例如,在分段架构中,转换 将需要获取段寄存器的地址并添加 存储在指针中的偏移量)。

3:

指针虽然在历史上不是完全有序的,但实际上是这样的 对于今天存在的所有系统,除了象牙塔 委员会的想法,所以这一点没有实际意义。

4:

但是,即使是分段架构,虽然不太可能,但确实会出现 回来,排序问题仍然需要解决,因为 std::less 需要对指针进行完全排序。我只希望 operator

为什么其他人都应该假装受苦(我的意思是假装, 因为在委员会的一小部分人之外,人们已经 假设指针相对于 operator

ub mailing列表中的cmets趋势相反,FUZxxl指出支持DOS是不支持完全有序指针的一个原因。

更新

Annotated C++ Reference Manual(ARM) 也支持这一点,它表示这是由于在分段架构上支持这一点造成的负担:

表达式在分段架构上的计算结果可能不会为 false [...] 这解释了为什么加法、减法和比较 指针仅针对指向数组和一个元素的指针定义 结束之后。 [...] 具有非分段地址的机器用户 然而,空间发展成语指的是超越的元素 数组的末端 [...] 不能移植到分段架构 除非采取特别的努力 [...] 允许 [...] 将是昂贵的 并且几乎没有什么有用的用途。

【讨论】:

  • 我认为支持 DOS 是不强制要求完全有序指针的一个重要原因。这些人从哪里知道他们的机器是人们编写 C 代码的唯一机器?
  • 有些算法需要总排序,但更多的算法只要求两个任意指针之间的比较除了产生零或一之外没有副作用。你能想到标准不应该要求实现保证或预定义一个宏来表明它们不能做到这一点的任何理智的理由吗?
【解决方案2】:

8086 是具有 16 位寄存器和 20 位地址空间的处理器。为了解决其寄存器中缺少位的问题,存在一组段寄存器。在内存访问时,取消引用的地址是这样计算的:

address = 16 * segment + register

请注意,除其他外,地址通常有多种表示方式。比较两个任意地址很繁琐,因为编译器必须先对两个地址进行规范化,然后再比较规范化的地址。

许多编译器指定(在可能的内存模型中)在进行指针运算时,段部分保持不变。这有几个后果:

  • 对象的大小最多为 64 kB
  • 对象中的所有地址都具有相同的段部分
  • 比较对象中的地址可以通过比较寄存器部分来完成;这可以在一条指令中完成

这种快速比较当然只在指针来自同一个基地址时才有效,这也是为什么 C 标准只为两个指针都指向同一个对象时才定义指针比较的原因之一。

如果您想要对所有指针进行有序比较,请考虑先将指针转换为 uintptr_t 值。

【讨论】:

    【解决方案3】:

    我相信它是未定义的,因此 C 可以在实际上“智能指针”在硬件中实现的架构上运行,并进行各种检查以确保指针永远不会意外指向它们被定义为引用的内存区域之外到。我从未亲自使用过这样的机器,但考虑它们的方式是计算无效指针与除以 0 一样被禁止;您可能会遇到终止程序的运行时异常。此外,禁止计算指针,您甚至不必取消引用它即可获得异常。

    是的,我相信该定义最终还允许更有效地比较旧 8086 代码中的偏移寄存器,但这不是唯一的原因。

    是的,这些受保护指针架构之一的编译器理论上可以通过转换为无符号或等价物来实现“禁止”比较,但是 (a) 这样做的效率可能会大大降低,并且 (b) 这会是对架构预期保护的肆意故意规避,至少一些架构的 C 程序员可能希望启用(而不是禁用)保护。

    【讨论】:

      【解决方案4】:

      从历史上看,说动作调用了未定义的行为意味着任何使用此类动作的程序都可以期望正确地仅在那些为该动作定义了满足其要求的行为的实现上。指定调用未定义行为的动作并不意味着使用此类动作的程序应被视为“非法”,而是旨在允许 C 用于在无法有效运行的平台上运行不需要此类动作的程序支持他们。

      一般来说,期望编译器要么输出在标准要求的情况下最有效地执行指定操作的指令序列,并执行该指令序列在其他情况下碰巧做的任何事情,或者输出一系列指令,在这种情况下,其行为被认为以某种方式比自然序列更“有用”。如果某个动作可能触发硬件陷阱,或者在某些情况下触发操作系统陷阱可能被认为比执行“自然”指令序列更可取,并且陷阱可能导致 C 编译器无法控制的行为,该标准没有规定任何要求。因此,此类情况被标记为“未定义行为”。

      正如其他人所指出的,在某些平台上,对于不相关的指针 p1 和 p2,p1 &lt; p2 可以保证产生 0 或 1,但是在这种情况下比较 p1 和 p2 的最有效方法是有效的标准定义的可能不支持p1 &lt; p2 || p2 &gt; p2 || p1 != p2 的通常期望。如果为这样的平台编写的程序知道它永远不会故意比较不相关的指针(暗示任何此类比较都将代表程序错误),那么让压力测试或故障排除构建生成捕获任何此类比较的代码可能会有所帮助。标准允许此类实现的唯一方法是进行此类比较未定义的行为。

      直到最近,特定操作会调用标准未定义的行为这一事实通常只会给尝试在该操作会产生不良后果的平台上编写代码的人们带来困难。此外,在平台上,如果编译器不遗余力地执行操作,则操作只会产生不良后果,程序员普遍接受的做法是明智地依赖此类操作。

      如果有人接受以下观念:

      1. 该标准的作者预计,不相关指针之间的比较将在这些平台上有效地工作,并且只有在那些平台上,比较相关指针的最自然方法也适用于不相关的指针,并且

      2. 存在比较不相关指针会出现问题的平台

      那么标准将不相关的指针比较视为未定义行为是完全有意义的。如果他们预料到,即使是为所有指针定义了不相交的全局排名的平台编译器也可能会使不相关的指针比较否定时间和因果律(例如,给定:

      int needle_in_haystack(char const *hs_base, int hs_size, char *needle)
      { return needle >= hs_base && needle < hs_base+hs_size; }
      

      编译器可能会推断程序将永远不会收到任何输入,这会导致 needle_in_haystack 被赋予不相关的指针,并且任何仅在程序收到此类输入时才相关的代码可能会被删除)我认为他们会以不同的方式指定事物。编译器作者可能会争辩说,编写needle_in_haystack 的正确方法是:

      int needle_in_haystack(char const *hs_base, int hs_size, char *needle)
      {
        for (int i=0; i<size; i++)
          if (hs_base+i == needle) return 1;
        return 0;
      }
      

      因为他们的编译器会识别循环正在做什么,并且还会识别出它运行在不相关的指针比较工作的平台上,因此生成的机器代码与旧编译器为早先陈述的公式生成的机器代码相同。至于是否最好要求编译器提供一种方法来指定类似于以前版本的代码应该在支持它的平台上明智地在支持它的平台上或拒绝在不支持它的平台上编译,或者更好地要求程序员打算使用以前的语义应该写后者,希望优化器把它变成有用的东西,我把它留给读者判断。

      【讨论】:

        猜你喜欢
        • 2018-05-16
        • 2023-02-01
        • 2020-11-20
        • 1970-01-01
        • 1970-01-01
        • 2019-09-13
        • 2016-11-18
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多