【问题标题】:Is dereferencing a NULL pointer to array valid in C?在 C 中取消引用指向数组的 NULL 指针是否有效?
【发布时间】:2020-05-04 12:38:17
【问题描述】:

这种行为是否已定义?

volatile long (*volatile ptr)[1] = (void*)NULL;
volatile long v = (long) *ptr;

printf("%ld\n", v);

它之所以有效,是因为通过取消引用指向数组的指针,我们接收到一个数组本身,然后该数组衰减为指向它的第一个元素的指针。

更新的演示:https://ideone.com/DqFF6T

此外,GCC 甚至将下一个代码视为常量表达式:

volatile long (*ptr2)[1] = (void*)NULL;
enum { this_is_constant_in_gcc = ((void*)ptr2 == (void*)*ptr2) };
printf("%d\n", this_is_constant_in_gcc);

基本上,在编译时取消引用 ptr2;

【问题讨论】:

  • 你的问题很不清楚...取消引用NULL是无效的,基本上总是会导致分段错误。
  • 是的,确实发生了取消引用。 ptr 是一个指针。
  • 因为“工作”是未定义行为的绝佳且完全有效的示例。未定义的行为 not 是否意味着“它不起作用”。这意味着它可能会起作用。它可能会失败。 未定义会发生什么。
  • 最后关于为什么应该定义行为的论点实际上使这个问题变得有点有趣。
  • 因为指向数组的指针和指向其他东西的指针是不同的东西。 - 从技术上讲,它们不是。指针指向某种类型的对象的地址,不管这个对象是否是数组的一部分。它可能有一些实现定义的原因来选择另一个,但每个标准没有区别。 当解引用指向数组的指针时,我们会收到一个数组本身。 不,当你解引用指向数组的指针时,实际上你访问的是该数组的第一个元素对象,根据我上面所说的。

标签: c arrays pointers null dereference


【解决方案1】:

这个:

long (*ptr)[1] = NULL;

正在声明一个指向“1 个数组long”的指针(更准确地说,类型是long int (*)[1]),初始值为NULL。一切正常,任何指针都可以是NULL

那么,这个:

long v = (long) *ptr;

正在取消引用NULL 指针,这是未定义的行为。所有的赌注都没有了,如果你的程序没有崩溃,下面的语句可以打印任何值或做任何其他事情。

让我再澄清一次:未定义的行为意味着任何事情都可能发生。没有解释为什么在调用未定义的行为后会发生任何奇怪的事情,也不需要解释。编译器可以很好地发出 16 位实模式 x86 程序集,产生删除整个主文件夹的二进制文件,发出 Apollo 11 Guidance Computer 程序集代码,或其他任何东西。这不是错误。完全符合标准。


您的代码似乎可以工作的唯一原因是 GCC 出于巧合决定执行以下操作 (Godbolt link):

mov     QWORD PTR [rbp-8], 0    ; put NULL on the stack
mov     rax, QWORD PTR [rbp-8]
mov     QWORD PTR [rbp-16], rax ; move NULL to the variable v

导致NULL-dereference 从未真正发生过。这很可能是取消引用 ptr ¯\_(ツ)_/¯

中未定义行为的结果

有趣的是,我之前在评论中说过:

取消引用NULL 无效,基本上总是会导致分段错误。

当然,因为“基本上总是”是错误的未定义行为。我认为这是我第一次看到空指针取消引用不会导致 SIGSEGV。

【讨论】:

  • long (*ptr)[1] = NULL; - 声明了一个指向“数组1 long”的指针,初始值为NULL。一切都很好,任何指针都可以是NULL - ptr 的值是否为NULL 或者是NULL 存储在long 对象中,而ptr 只是指向NULL long (*ptr)[1] = NULL; 语句的值?这是我现在不太清楚的地方。
  • @RobertS-ReinstateMonica ptrlong (*)[1] 类型的变量。是的,这是一个非常令人困惑的定义。
  • @RobertS-ReinstateMonica long (*ptr)[1] = NULL;是一种判断地址 0 处存在数组的方法。
  • 不,@NekoNeko。 long (*ptr)[1] = NULL; 是一种表示ptr 不指向任何对象的方式。这实际上是空指针常量的主要用途之一,例如NULL
  • @NekoNeko 我真的不知道怎么说了:取消引用NULL 是未定义的行为!任何事情都有可能发生。 没有解释为什么会发生这种情况。编译器可以很好地生成 16 位实模式 x86 程序集,生成删除整个主文件夹的二进制文件,生成 Apollo 11 指导计算机源代码,或其他任何东西。这不是错误。它完全符合标准。
【解决方案2】:

这种行为是否已定义?

不是。

long (*ptr)[1] = NULL;
long v = (long) *ptr;

printf("%ld\n", v);

它之所以有效,是因为通过取消引用指向数组的指针,我们收到了一个 数组本身,然后该数组衰减到指向它的指针是第一个 元素。

不,您将类型与值混淆了。确实,第二行上的表达式 *ptr 具有 type long[1],但无论数据类型如何,并且无论将应用于何种自动转换,评估该表达式都会产生未定义的行为结果(如果已定义)。

规范的相关部分是paragraph 6.5.2.3/4:

一元 * 运算符表示间接。如果操作数指向一个 函数,结果是一个函数指示符;如果它指向一个 对象,结果是一个指定对象的左值。如果操作数 类型为“类型指针”,结果类型为“类型”。如果 无效的值已分配给指针,的行为 一元 * 运算符未定义。

A footnote 继续澄清这一点

[...] 一元 * 运算符取消引用指针的无效值包括空指针 [...]

从经验意义上说,它可能对您“有效”,但从语言的角度来看,任何输出或没有输出都是一致的结果。

更新:

有趣的是,显式获取*ptr 的地址与假设数组衰减将克服取消引用的不确定性的答案可能会有所不同。该标准规定,作为一种特殊情况,如果一元 & 运算符的操作数是一元 * 运算符的结果,则不会计算这些运算符。如果满足所有相关约束,则结果就好像它们都被完全省略了,只是它永远不是左值。

因此,这没关系:

long (*ptr)[1] = NULL;
long v = (long) &*ptr;

printf("%ld\n", v);

在许多实现中,它会可靠地打印 0,但请注意 C 并未指定它必须为 0。

这里的关键区别在于,在这种情况下,* 操作不会被评估(根据规范)。原始代码中的* 操作是is 评估的,尽管如果指针值有效,结果数组将立即转换回指针(不同类型,但相同的位置)。这确实表明了实现可以使用原始代码采用的明显捷径,如果他们愿意,他们可以采用它,而不考虑ptr 的值是否有效,因为如果它有效然后他们可以为所欲为。

【讨论】:

  • 如果指向一个对象,结果是一个指定对象的左值这里的对象是一个数组,成功返回。
  • 不,@NekoNeko。暂时不考虑标准明确指出空指针属于“无效值”类别的事实,您仍然会混淆类型和值。如果指针有效,则指针将指向一个对象,但并非所有指针值都有效,尤其是空指针不指向任何对象根据定义
  • 奇怪的是程序好像打印0;我认为 Linux 系统几乎可以保证在取消引用空指针时出现段错误,但这个程序也在我的 Linux 笔记本电脑上打印 0。
  • @exnihilo 它会在任何地方打印 0,我想。因为没有发生“真正的”取消引用。
  • @NekoNeko 不,它不会到处打印 0。这是未定义的行为。 ANY 兼容的编译器可以很好地在取消引用 NULL 后打印 Hello World!
【解决方案3】:

仅回答您提供的问题:

  1. 在 C 中取消引用指向数组的 NULL 指针是否有效?

没有。

  1. 此行为是否已定义?

它被归类为“未定义的行为”,所以没有定义。


不管这种情况,数组的这个技巧,可能会在某些实现上起作用,而且它完全不需要这样做(我暗示你是出于好奇而问的),它不是 有效的 根据 C 标准,以 任何 方式取消引用 NULL 指针并将导致“未定义行为”。


当您在程序中实现此类语句时,任何事情都可能发生。

看看这个问题的答案,这解释了原因:

What EXACTLY is meant by "de-referencing a NULL pointer"?

来自Adam Rosenfield´s answer

空指针是一个不指向任何有效数据的指针(但它不是唯一的此类指针)。 C 标准说取消引用空指针是未定义的行为。这意味着任何事情都可能发生:程序可能会崩溃,它可能会继续静默运行,或者它可能会擦除您的硬盘驱动器(尽管这不太可能)。

【讨论】:

    【解决方案4】:

    这种行为是否已定义?

    行为未定义,因为您将 * 运算符应用于比较等于空指针常量的指针。

    下面的 stackoverflow 线程试图解释什么是未定义的行为:Undefined, unspecified and implementation-defined behavior

    【讨论】:

    • 嗯,NULL可以用(void*)1代替,也可以。
    • 对于未定义行为的程序的行为进行推理是没有意义的。我相信,这个程序会产生龙。
    猜你喜欢
    • 1970-01-01
    • 2012-03-25
    • 1970-01-01
    • 1970-01-01
    • 2011-10-20
    • 2017-01-29
    • 2014-02-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多