数组外的指针比较的基本原理是UB

时间:2015-07-01 01:19:40

标签: c pointers language-lawyer undefined-behavior

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

  

C99 6.5.8 / 5关系运算符

     

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

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

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

this question的删除答案提到这条UB允许跳过段寄存器的比较并仅比较偏移。保存特别有价值吗?

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

4 个答案:

答案 0 :(得分:5)

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

  

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

2

  

然后我们可能需要一种新的类型来保证总订单时间   从指针转换(例如,在分段体系结构中,转换   需要获取段寄存器的地址并添加   存储在指针中的偏移量)。

3

  

指针虽然历史上并非完全有序,但几乎是如此   对于今天存在的所有系统,除了象牙塔   委员会的想法,所以重点是没有实际意义。

4

  

但是,即使是分段的体系结构,尽管它不太可能,但确实会出现   回来,订购问题仍然需要解决,如std :: less   需要完全订购指针。我只想要运营商&lt;是一个   该属性的替代拼写。

     

为什么其他人都假装受苦(我的意思是假装,   因为在委员会的一个小队伍之外,人们已经   假设指针相对于运算符&lt;)to完全有序   满足一些目前不存在的理论需求   架构?

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

更新

Annotated C++ Reference Manual ARM )也支持这一点,它说这是由于在分段体系结构上支持这一点的负担:

  

在分段体系结构上,表达式可能无法评估为false   [...]这解释了为什么加法,减法和比较   指针仅定义为指向数组和一个元素的指针   超越结束。 [...]具有非分段地址的计算机的用户   然而,空间发展的成语指的是超越的元素   数组的末尾不能移植到分段体系结构   除非作出特别努力[...]允许[...]费用昂贵   并没有什么用处。

答案 1 :(得分:3)

8086是一个具有16位寄存器和20位地址空间的处理器。为了应对寄存器中缺少的位,存在一组段寄存器。在内存访问时,解除引用的地址计算如下:

address = 16 * segment + register

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

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

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

当然,这种快速比较仅在指针派生自相同的基地址时起作用,这是C标准仅在两个指针指向同一对象时定义指针比较的原因之一。

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

答案 2 :(得分:1)

我认为它未定义,所以C可以在架构上运行,实际上,#34;智能指针&#34;在硬件中实现,具有各种检查以确保指针永远不会意外地指向它们被定义为引用的存储区域之外。我从来没有亲自使用过这样的机器,但是考虑它们的方法是计算一个无效的指针就像禁止0一样被禁止;您可能会遇到终止程序的运行时异常。此外,禁止的是计算指针,你甚至不需要取消引用它来获得异常。

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

是的,这些受保护指针架构之一的编译器理论上可以实现&#34;禁止&#34;通过转换为无符号或等效的比较,但(a)这样做的效率可能会大大降低;(b)这将是一种肆无忌惮的规避建筑的预期保护,至少部分保护架构的C程序员可能想要启用(而不是禁用)。

答案 3 :(得分:1)

从历史上看,该行为调用未定义行为意味着任何使用此类行为的程序都可能只能在那些为该行为定义满足其要求的行为定义的实现上正确。指定一个被调用的动作未定义的行为并不意味着使用这种动作的程序应该被认为是非法的#34;而是意图允许C用于运行不需要的程序这些行动,在无法有效支持它们的平台上。

通常,期望编译器要么输出指令序列,这些指令序列在标准要求的情况下最有效地执行指示的动作,并且做任何在其他情况下发生的指令序列,或者输出一系列指令,这些指令在这种情况下的行为被认为是某种更有用的方式&#34;比自然序列。如果某个操作可能会触发硬件陷阱,或者在某些情况下触发操作系统陷阱可能会被认为比执行&#34;自然&#34;更可取。指令序列,并且陷阱可能导致C编译器控制之外的行为,标准没有要求。因此,这种情况标记为&#34;未定义的行为&#34;。

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

直到最近,特定操作会调用标准未定义的行为这一事实通常只会给尝试在操作会产生不良后果的平台上编写代码的人带来困难。此外,在一个行动只会产生不良后果的平台上,如果编译器不愿意这样做,那么程序员通常会接受这样一种明智的行为。

如果接受以下观念:

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

  2. 存在比较无关指针会有问题的平台

  3. 然后,标准将完全意义上的无关指针比较视为未定义行为。如果他们预料到即使是针对所有指针定义不相交全球排名的平台的编译器也可能会使无关指针比较否定时间和因果关系的规律(例如:

    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;
    }
    

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