【问题标题】:Does this avoid UB这是否避免了UB
【发布时间】:2015-04-22 08:07:43
【问题描述】:

这个问题更像是一个学术问题,因为没有正当理由编写自己的 offsetof 宏了。尽管如此,我还是经常看到这个本土实现的弹出窗口:

#define offsetof(s, m) ((size_t) &(((s *)0)->m))

从技术上讲,这就是取消引用 NULL 指针 (AFAIKT):

C11(ISO/IEC 9899:201x) §6.3.2.3 指针第 3 节

值为0 的整型常量表达式,或转换为void * 类型的表达式称为空指针常量

所以上面的实现是,按照我是怎么看标准的,和写的一样:

#define offsetof(s, m) ((size_t) &(((s *)NULL)->m))

这确实让我想知道,通过改变一个微小的细节,offsetof 的以下定义将是完全合法的,并且可靠:

#define offsetof(s, m) (((size_t)&(((s *) 1)->m)) - 1)

看来,不是用0,而是用1作为指针,最后减1,结果应该是一样的。我不再使用 NULL 指针。据我所知the results are the same

所以基本上:有什么理由为什么在这个offsetof 定义中使用1 而不是0 可能不起作用?在某些情况下它仍然会导致 UB,如果是:何时以及如何?基本上,我在这里要问的是:我在这里遗漏了什么吗?

【问题讨论】:

  • 它不仅是自产的,它在每个标准库中,而且在任何地方都以相同的方式实现。至于您使用1 而不是0 的变体,请考虑并非所有平台都可以同样好地处理未对齐访问(有些根本不是)。无论如何,这并不重要,因为编译器无论如何都可以在编译时对其进行评估。
  • @JoachimPileborg:其中,我不完全确定,例如 gcc 依赖于其内部的__builtin_offsetof。我还没有看到任何无法处理第一个 offsetof 实现的编译器,但这一点:((s *)NULL)->m 从学术角度来看是不正确的,而 ((s *) 1)->m 不是(我认为)跨度>

标签: c language-lawyer undefined-behavior


【解决方案1】:

这两个定义都是未定义的行为:在第一个定义中,空指针被取消引用,而在第二个定义中,您正在取消引用无效指针(指针未指向有效对象)。用 C 语言编写 offsetof 宏的可移植版本是不可能的。

缺陷报告#44 说:

“特别是,这就是 offsetof 宏存在的原因:否则就没有可移植的方法来计算这样的平移时间常数。”

(DR#44 适用于 C89,但 C99 和 C11 中的语言没有任何改变,可以实现可移植。)

【讨论】:

  • 好的,所以不管在本地开发的offsetof 宏中使用什么假定地址,您都属于->m 的灰色区域(访问无效对象上的成员),但是由于您只使用该表达式来获取内存地址,大多数编译器都可以处理该表达式吗?对吗?
  • @EliasVanOotegem 如果编译器在 C 中提供了 offsetof 的实现(不是作为具有某种魔法的内置函数),它决定通过标准定义什么是 UB 是可以接受的。跨度>
  • @EliasVanOotegem 我不这么认为。现代编译器执行各种形式的静态分析并将 UB 标记为不可访问,因此编译器可以假定永远不会调用给定的代码。这意味着它可以优化分支甚至假设函数永远不会被调用。
  • @MaciejPiechotka:这就是我说 “大多数编译器” 而不是 “所有编译器” 的原因,意思是我不假设他们能够为了处理我的问题中的表达,我只是想回顾一下 oauh 给出的答案
  • @EliasVanOotegem oauh 没有说大多数编译器会处理它。许多编译器(包括 gcc 和 clang)都在使用数据流分析。许多编译器会将 UB 节点视为不可访问。即使他们在您的测试用例中工作,也可能会发生这只是一个意外,如果星星正好对齐,它就会失败。
【解决方案2】:

我相信行为是实现定义的。在 n1256 的 6.3.2.3 中:

5 整数可以转换为任何指针类型。除非前面指定,否则结果是实现定义的,可能未正确对齐,可能不指向引用类型的实体,并且可能是陷阱表示。

【讨论】:

    【解决方案3】:

    一个问题是你创建的指针没有指向一个对象。

    6.2.4 对象的存储时长

    1. 对象的生命周期是程序执行的一部分,在此期间存储是 保证为它保留。一个对象存在,有一个常量地址,33) 并保留 其在其整个生命周期中的最后存储价值。 34) 如果一个对象在其外部被引用 生命周期,行为未定义。当指针的值变得不确定时 它指向(或刚刚过去)的对象达到其生命周期的终点。

    J.2 未定义的行为
    - 使用指向生命周期已结束的对象的指针的值 (6.2.4)。

    3.19.2 不确定值:未指定的值或陷阱表示

    当你将 1 转换为指针,并且创建的指针不指向对象时,指针的值变得不确定。然后使用指针。这两者都会导致未定义的行为。

    整数到指针的转换也是有问题的:

    6.3.2.3 指针

    1. 整数可以转换为任何指针类型。除先前规定外, 结果是实现定义的,可能没有正确对齐,可能不指向 引用类型的实体,并且可能是陷阱表示。 67)

    【讨论】:

      【解决方案4】:

      使用取消引用 NULL 指针的 offsetof 的实现会调用未定义的行为。在这个实现中,假设假设结构开始于地址0。您可能会认为它是1,是的,它也会调用 UB,因为您取消引用空指针,但因为取消引用了未初始化的指针。

      【讨论】:

        【解决方案5】:

        任何版本的 C 标准都不会禁止编译器使用任何试图在不定义存储位置来保存指定对象的情况下实现效果的宏执行任何它想要的操作。尽管如此,像这样的形式:

        #define offsetof(s, m) ((char*)&((((s)*)0)->m)-(char*)0)
        

        对于 C99 之前的编译器来说可能是相当安全的。请注意,它通过从另一个中减去一个char* 来生成一个整数。当指针访问同一有效对象的部分时,它被指定为工作并产生一个常量值,并且实际上将在任何没有注意到空指针不是有效对象的编译器上工作。相比之下,将指针转换为整数或反之亦然的效果在不同的平台上会有所不同,并且在许多平台上(int)(((char*)&foo)+1) - (int)(char*)&foo 可能不会产生 1。

        另请注意,“未定义行为”的含义最近发生了变化。过去未定义的行为意味着规范没有说明编译器必须做什么,但大多数编译器通常会选择(有时是任意的)数学上正确或在底层平台上有意义的行为。例如,在 32 位处理器上,int32_t foo=2147483647; foo+=(unsigned char)x; if (foo > 100) ... 编译器可能会确定对于 x 的任何可能值,分配给 foo 的数学正确值将在 2147483647 到 2147483903 的范围内,因此大于 100在任何情况下。或者它可能使用二进制补码算法执行操作,并对可能环绕的值执行比较。然而,较新的编译器可能会做一些更有趣的事情。

        一个新的编译器可能会查看类似于 foo 示例的表达式,并推断如果 x 为零,则 foo 必须保持为 2147483647,如果 x 为非零,则允许编译器随心所欲,因此它可能会推断出在执行语句时x 的 LSB 必须等于 0,因此如果代码前面有对 (unsigned char)x==0 的测试,则该表达式将始终为真。给定像 offsetof 宏这样的代码,无论任何变量的值如何,它都会生成未定义的行为,编译器不仅有权删除使用它的任何代码,还可以删除任何无法通过任何定义的方式导致程序的前面代码执行终止。

        请注意,将非零整数文字仅转换为指针未定义行为如果不存在任何地址已被获取并转换为整数的对象,以便产生相同的值。因此,编译器将无法识别基于指针差异的offsetof 宏的变体,该变体将一些非零值转换为表现出未定义行为的指针,除非它可以确定所讨论的数字不对应于任何指针。另一方面,尝试将非零整数转换为指针会在某些系统上执行验证检查以确保指针有效;如果不是这样,那么这样的系统可能会陷入陷阱。

        【讨论】:

          【解决方案6】:

          你实际上并没有取消引用指针,你所做的更像是指针加法,所以使用零应该没问题。

          【讨论】:

          • 将整数添加到空指针会调用未定义行为,编译器应在运行时比通常更积极地捕获该行为;虽然编译器在编译时允许这种类型的偏移计算是有帮助的,但标准中没有任何理由证明它是正确的。
          猜你喜欢
          • 2020-08-15
          • 1970-01-01
          • 1970-01-01
          • 2016-10-13
          • 2010-11-02
          • 1970-01-01
          • 2016-06-20
          • 2016-02-10
          • 1970-01-01
          相关资源
          最近更新 更多