【问题标题】:Why dereferencing a null pointer is undefined behaviour?为什么取消引用空指针是未定义的行为?
【发布时间】:2011-10-11 05:14:04
【问题描述】:

根据 ISO C++,取消引用空指针是未定义的行为。我的好奇心是,为什么?为什么标准决定声明它未定义的行为?这个决定背后的理由是什么?编译器依赖?似乎不是,因为据我所知,根据 C99 标准,它是明确定义的。机器依赖?有什么想法吗?

【问题讨论】:

  • 信不信由你,地址 0 在 x86 上是可用的,所以有时,您实际上可能需要取消引用“空”指针。
  • 但如果不是未定义,那么行为应该是什么?
  • @drb: nasal demons 例如...
  • @Rob:这不是真的。 6.5.3.2/4 说“如果已为指针分配了无效值,则一元 * 运算符的行为是未定义的。”,脚注包括“一元 * 运算符取消引用指针的无效值包括一个空指针”。
  • 空指针不一定指向地址0。

标签: c++ pointers undefined-behavior


【解决方案1】:

为取消引用 NULL 指针定义一致的行为将要求编译器在大多数 CPU 架构上每次取消引用之前检查 NULL 指针。对于专为速度而设计的语言来说,这是不可接受的负担。

它也只修复了一个较大问题的一小部分 - 有很多方法可以使无效指针超出 NULL 指针。

【讨论】:

  • 您假设 NULL 必须 很特别,据我了解,OP 的问题是,为什么要特别吗?
  • @Mehrdad:如何假设NULL 很特别?就取消引用而言,它与未初始化的指针或不再指向现有对象的指针相比并没有什么特别之处。
  • @James:好吧,因为编译器没有理由必须检查NULL指针(或其他无效指针,就此而言)......它取消引用它就像任何其他指针,如果它想要的话,如果它不是特别的。只有当它是特殊的时候,编译器才需要检查。
  • @Mehrdad,这就是我第二段的重点 - NULL 指针 不是 特殊的,不应该如此。
  • @Mehrdad "为什么要特别?" 因为它一个特别的值。没有其他有效的指针值不是对象的地址,也不是某个数组的末尾。 OTOH,取消引用空指针不是特殊情况。
【解决方案2】:

主要原因是在他们编写最初的 C 标准时,已经有许多实现允许它,但给出了相互矛盾的结果。

在 PDP-11 上,碰巧地址 0 总是包含值 0,因此取消引用空指针也会得到值 0。很多使用这些机器的人认为,由于它们是原始机器 C写在/用于编程,这应该被认为是所有机器上 C 的规范行为(即使它最初是非常偶然地发生的)。

在其他一些机器上(想到Interdata,虽然我的记忆很容易出错)地址0被正常使用,所以它可以包含其他值。还有一些硬件,地址 0 实际上是一些内存映射硬件,所以读/写它做了一些特殊的事情——根本不等同于读/写普通内存。

营地不会就应该发生的事情达成一致,因此他们将其设为未定义的行为。

编辑:我想我应该补充一点,在编写 C++ 标准时,它的未定义行为已经在 C 中得到了很好的确立,并且(显然)没有人认为有充分的理由在这一点上产生冲突,所以他们保持不变。

【讨论】:

  • 还值得注意的是,在 C89 发布之前,它并没有对 any 行为提出任何要求,但许多 C 实现确实定义了许多事物的行为。如果某些 C 编译器为某些操作定义了行为,而有些则没有,那么未定义行为只会保留现状。直到最近,该标准未能定义事物才被解释为表明没有合理的代码——即使是在 C 标准之前定义行为的代码目标平台——应该使用任何东西不在标准中。
【解决方案3】:

给出定义行为的唯一方法是为每个指针取消引用和每个指针算术运算添加运行时检查。在某些情况下,这种开销是不可接受的,并且会使 C++ 不适合它经常用于的高性能应用程序。

C++ 允许您创建自己的智能指针类型(或使用库提供的类型),在安全性比性能更重要的情况下,可以包括此类检查。

根据 C99 标准的第 6.5.3.2/4 条,在 C 中取消引用空指针也是未定义的。

【讨论】:

  • 这不是真的。定义的行为可以简单地是“只要未访问该值,您就可以取消引用空指针。如果访问了结果左值的值,则行为未定义”。这不需要任何检查。
  • @Johannes:是的,你是对的;我将“取消引用”解释为“访问取消引用的值”,这并不完全准确。
  • @Johannes Schaub - litb:我在此处发布了您的另一个答案的摘录作为答案,如果您想将其添加为您自己的答案,请随意这样做。如果是这样,我会删除一个标记的社区 wiki。
  • @Als 我不做欺骗帖子。但我赞成你的。谢谢你的传播。玩得开心:)
  • @Johannes Schaub - litb:好的:) 不管怎样,我在发布时标记了那个社区维基!
【解决方案4】:

This 来自@Johannes Schaub 的回答 - litb,提出了一个有趣的理由,这似乎很有说服力。


仅取消引用空指针的正式问题是不可能确定生成的左值表达式的身份:从取消引用指针产生的每个此类表达式在计算该表达式时必须明确引用对象或函数。如果取消引用空指针,则没有此左值标识的对象或函数。这是标准用来禁止空引用的参数。

另一个增加混乱的问题是typeid 运算符的语义使这种痛苦的一部分得到了很好的定义。它说,如果给定一个由取消引用空指针而产生的左值,则结果将引发bad_typeid 异常。虽然,这是一个有限的领域,对于上述寻找身份的问题存在一个例外(没有双关语)。存在对未定义行为产生类似异常的其他情况(尽管不那么微妙,并且在受影响的部分有参考)。

委员会讨论了通过定义一种没有对象或函数身份的左值来全局解决这个问题:所谓的空左值。然而,这个概念仍然存在问题,他们决定not to adopt it


注意:
将此标记为社区 wiki,因为答案和功劳应归于原始发布者。我只是在这里粘贴原始答案的相关部分。

【讨论】:

  • 恕我直言,许多问题可以通过更好地定义“C对象”和地址的含义来解决,认识到一个N字节的C对象有N+1个关联地址,每个地址的前N个标识一个字节,最后N个每个跟随一个字节。这个定义可以推广到零字节对象,它们有一个既不标识也不跟随任何存储字节的地址,并且可能匹配也可能不匹配任何其他零字节对象的地址。
【解决方案5】:

真正的问题是,你期望什么行为?

根据定义,空指针是表示对象不存在的奇异值。解引用指针的结果是获得指向对象的引用。

那么如何从指向 void 的指针中获得良好的引用?

你没有。因此未定义的行为

【讨论】:

  • 抛出异常?发出信号?致电abort()?有很多明智的事情可以被定义;问题是,为什么不定义它?
  • @Mike Seymour:似乎我们没有类似地解释这个问题 :) 检查(事先)取消引用将是昂贵的。另一方面,在 Unix 上,操作系统 无论如何都会执行检查,因此理论上可以连接信号处理程序并执行您引用的操作之一......但我认为这不是处处可行。特别是在没有操作系统的嵌入式平台上。指定一种行为会削弱这些平台。
  • @MikeSeymour 在没有throw 的情况下抛出异常几乎不是明智之举。 (是的,你可以得出关于 Java 的结论。)
【解决方案6】:

我怀疑这是因为如果行为是明确定义的,编译器必须在指针被取消引用的任何地方插入代码。如果定义了实现,那么一种可能的行为仍然可能是硬崩溃。如果未指定,则某些系统的编译器会产生额外的过度负担,或者它们可能会生成导致硬崩溃的代码。

因此,为了避免对编译器造成任何可能的额外负担,他们未定义行为。

【讨论】:

    【解决方案7】:

    有时您需要一个无效指针(另请参阅 Windows 上的 MmBadPointer)来表示“无”。

    如果一切都是有效的,那就不可能了。所以他们使NULL 无效,并且不允许你取消引用它。

    【讨论】:

      【解决方案8】:

      这是一个简单的测试和示例:

      1. 分配一个指针:

        int * 指针;

      ?创建时指针中的值是多少?
      ?指针指向什么?
      ?当我取消引用当前状态下的这个点时会发生什么?

      1. 标记链表的结尾。 在链表中,一个节点指向另一个节点,除了最后一个。
        最后一个节点的指针值是多少?
        当您取消引用最后一个节点的“下一个”字段时会发生什么?

      需要是一个值,表明一个指针没有指向任何东西或它处于无效状态。这就是 NULL 指针概念发挥作用的地方。链表可以使用 NULL 指针来指示链表的结束。

      【讨论】:

        【解决方案9】:

        在其他地方已经提出论点,如果没有大量开销,就不可能为空指针引用定义明确的行为,我认为这是正确的。这是因为 AFAIU 这里的“定义明确”也意味着“便携”。如果您不特别处理 nullptr 引用,您最终会生成简单地尝试读取地址 0 的指令,但会在不同的处理器上产生不同的行为,因此无法很好地定义。

        所以,我想这就是为什么取消引用 nullptr(可能还有其他无效指针)被标记为未定义的原因。

        我确实想知道为什么这是未定义的,而不是未指定的或实现定义的,它们与未定义的行为不同,但需要更多的一致性。

        特别是,当程序触发未定义的行为时,编译器几乎可以做任何事情(例如,可能丢弃整个程序?)并且仍然被认为是正确的,这有点问题。在实践中,您会期望编译器只会将空指针解引用编译为读取地址零,但随着现代优化器变得更好,但对未定义的行为也更加敏感,我认为,他们有时会做更多的事情彻底破坏程序。例如。考虑以下几点:

        matthijs@grubby:~$ cat test.c
        unsigned foo () {
                unsigned *foo = 0;
                return *foo;
        }
        
        matthijs@grubby:~$ arm-none-eabi-gcc  -c test.c -Os && objdump -d test.o 
        
        test.o:     file format elf32-littlearm
        
        
        Disassembly of section .text:
        
        00000000 <foo>:
           0:   e3a03000        mov     r3, #0
           4:   e5933000        ldr     r3, [r3]
           8:   e7f000f0        udf     #0
        

        这个程序只是取消引用并访问一个空指针,这会导致生成一个“未定义的指令”(在运行时停止程序)。

        当这是一个意外的空指针取消引用时,这可能没问题,但在这种情况下,我实际上是在编写一个 需要 读取地址 0(包含重置向量)的引导加载程序,所以我很很惊讶会发生这种情况。

        因此,与其说是答案,不如说是对此事的一些额外看法。

        【讨论】:

          【解决方案10】:

          根据原始 C 标准 NULL 可以是任何值 - 不一定是零

          语言定义指出,对于每种指针类型,都有一个特殊的值——“空指针”——它与所有其他指针值区分开来,并且“保证与指向任何对象或函数的指针进行比较不相等”。 '也就是说,空指针绝对不指向任何地方。它不是任何对象或函数的地址

          每种指针类型都有一个空指针,不同类型的空指针内部值可能不同。

          (来自http://c-faq.com/null/null1.html

          【讨论】:

            【解决方案11】:

            尽管从语言的角度来看,在 C/C++ 中取消引用 NULL 指针确实会导致未定义的行为,但这种操作在编译器中针对在相应地址具有内存的目标进行了很好的定义。在这种情况下,这种操作的结果只是简单地读取地址 0 处的内存。

            此外,只要您不绑定引用的值,许多编译器将允许您取消引用 NULL 指针。这样做是为了提供对不符合标准但广泛使用的代码的兼容性,例如

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

            甚至还有一个discussion 使这种行为成为标准的一部分。

            【讨论】:

            • 没有理由期望上面的强制转换能够正常工作,即使空指针的处理方式与其他指针没有区别,因为系统不需要在指针和整数之间使用任何特定的映射。如果有一个永远不会被修改的全局char* x;,一个更有趣的概念是((char*)&amp;(((struct_type*)x)-&gt;member) - x)。在定义表达式的所有情况下,它将产生该成员的(常量)偏移量,并且如果编译器无法判断 x 是否拥有指向 struct_type 的指针,这是最有效的方法...跨度>
            • ...评估该表达式将使其直接产生该常量,而不涉及 x 在运行时。
            【解决方案12】:

            因为您无法创建空引用。 C++ 不允许这样做。因此,您不能取消引用空指针。

            主要是它是未定义的,因为没有逻辑的方法来处理它。

            【讨论】:

            • 您当然可以在 C++ 中创建和(尝试)取消引用空指针。
            • 重点是不能创建空引用,那么在尝试使用后门解决方案时应该如何定义呢?只需取消定义即可。
            • @RockyTriton 取消引用指针会产生 lvalue,而不是引用。在 C++ 中没有具有引用类型的表达式。
            • @MikeSeymour 问题是关于*nullpointer 的结果。那将是一个“空左值”,这当然是 Rocky 的意思。 “空左值”将是“空地址”处的左值。 “空地址”是矛盾的。
            【解决方案13】:

            您实际上可以取消引用空指针。有人在这里做了:http://www.codeproject.com/KB/system/soviet_kernel_hack.aspx

            【讨论】:

              猜你喜欢
              • 2011-10-31
              • 1970-01-01
              • 2019-07-22
              • 1970-01-01
              • 1970-01-01
              • 2017-02-17
              • 2011-02-24
              • 2018-07-11
              • 2014-03-03
              相关资源
              最近更新 更多