&(;(struct name *)NULL - > b)是否会在C11中导致未定义的行为?

时间:2014-11-13 10:27:27

标签: c language-lawyer c11 offsetof

代码示例:

struct name
{
    int a, b;
};

int main()
{
    &(((struct name *)NULL)->b);
}

这是否会导致未定义的行为?我们可以讨论它是否"取消引用null"但是C11没有定义术语" dereference"。

6.5.3.2/4明确指出在空指针上使用*会导致未定义的行为;但是->并没有说同样的内容,也没有将a -> b定义为(*a).b;它为每个运营商提供了单独的定义。

6.5.2.3/4中->的语义表示:

  

后缀表达式后跟 - >运算符和标识符指定成员   结构或联合对象。该值是对象的指定成员的值   第一个表达式指向哪个,并且是左值。

但是,NULL并未指向某个对象,因此第二句似乎未明确。

也可能是6.5.3.2/1:

  

约束:

     

一元&运算符的操作数应该是函数指示符,是a的结果   []或一元*运算符,或左值,指定一个不是位字段的对象,是   未使用寄存器存储类说明符声明。

但是我觉得粗体文本是有缺陷的,应该按照6.3.2.1/1( lvalue 的定义)读取可能指定对象的左值 - C99弄乱了左值的定义,因此C11不得不重写它,也许这部分错过了。

6.3.2.1/1确实说:

  

lvalue是一个潜在的表达式(对象类型不是void)   指定一个对象;如果一个左值在评估时没有指定一个对象,那么   行为未定义

但是&运算符 会评估其操作数。 (它不会访问存储的值,但这是不同的。)

这一长串推理似乎表明代码会导致UB,但它相当脆弱,我不清楚标准的作者是什么意图。如果事实上他们有意做的事情,而不是让我们讨论:)

6 个答案:

答案 0 :(得分:22)

从律师的角度来看,表达式&(((struct name *)NULL)->b);应该导致UB,因为你找不到没有UB的路径。恕我直言,根本原因在于,您将->运算符应用于未指向对象的表达式。

从编译器的角度来看,假设编译器程序员没有过于复杂,很明显表达式会返回与offsetof(name, b)相同的值,而且我很确定如果它被编译没有错误任何现有的编译器都会给出结果。

正如所写,我们不能责怪编译器会注意到在内部部分中对表达式使用运算符->而不是指向对象(因为它为null)并发出警告或错误

我的结论是,除非有一个特殊的段落说,只要它只是取其地址是合法的,取消引用空指针,这个表达式不合法C.

答案 1 :(得分:16)

是的,->的使用在英语术语undefined的直接意义上有未定义的行为。

仅当第一个表达式指向一个对象而未定义(= undefined)时,才会定义该行为。一般来说,你不应该在术语undefined中搜索更多,这意味着:标准没有为你的代码提供意义。 (有时它明确指出它没有定义的情况,但这不会改变该术语的一般含义。)

这是为了帮助编译器构建者处理事情而引入的松弛。 他们可能会定义一种行为,即使对于您要呈现的代码也是如此。特别是,对于编译器实现,对offsetof宏使用此类代码或类似代码是完全正确的。使此代码成为约束违规将阻止编译器实现的路径。

答案 2 :(得分:10)

让我们从间接运算符*开始:

  

6.5.3.2 p4:   一元*运算符表示间接。如果操作数指向函数,则结果为   功能指示符;如果它指向一个对象,结果是一个左值指定   宾语。 如果操作数的类型为“指向类型的指针”,则结果的类型为“type”。如果   已为指针分配了无效值,一元*运算符的行为为   未定义。 102)

* E,其中E是空指针,是未定义的行为。

有一个脚注说明:

  

102)因此,&*E相当于E(即使E是空指针),&(E1 [E2])相当于((E1)+(E2) ))。它是   如果E是函数指示符或左值是一元的有效操作数,则总是如此。   运算符,*& E是函数指示符或等于E的左值。如果* P是左值,则T是名称   对象指针类型,*(T)P是左值,其类型与T指向的类型兼容。

这意味着定义了E为NULL的& * E,但问题是对于&(* E).m是否也是如此,其中E是空指针,其类型是结构有会员m?

C标准没有定义该行为。

如果定义了,则会出现新问题,其中一个列在下面。 C Standard是正确的,以保持未定义,并提供一个宏内部处理问题的宏偏移。

  

6.3.2.3指针

     
      
  1. 一个整数常量表达式,其值为0,或者这样的表达式强制转换为类型   void *,称为空指针常量。 66)如果空指针常量转换为a   指针类型,结果指针,称为空指针,保证比较不等   指向任何对象或函数的指针。
  2.   

这意味着值为0的整型常量表达式将转换为空指针常量。

但是空指针常量的值未定义为0.该值是实现定义的。

  

7.19通用定义

     
      
  1. 宏是   空值   它扩展为实现定义的空指针常量
  2.   

这意味着C允许一个实现,其中空指针将具有一个值,其中所有位都已设置,并且对该值使用成员访问将导致溢出,这是未定义的行为

另一个问题是你如何评价&(* E).m?括号是否适用并首先进行*评估。保持不确定可以解决这个问题。

答案 3 :(得分:4)

首先,让我们确定我们需要一个指向对象的指针:

  

6.5.2.3结构和工会成员

     

4后缀表达式后跟->运算符,标识符表示成员   结构或联合对象。该值是对象的指定成员的值   其中第一个表达式指向,并且是左值.96)如果第一个表达式是指针   如果是限定类型,则结果具有指定类型的限定版本   构件。

不幸的是,没有空指针指向对象。

  

6.3.2.3指针

     

3一个整数常量表达式,其值为0,或者这样的表达式强制转换为类型   void *,称为空指针常量 .66)如果将空指针常量转换为   指针类型,结果指针,称为空指针保证比较不等   指向任何对象或函数的指针

结果:未定义的行为。

作为旁注,还有其他一些可以咀嚼的东西:

  

6.3.2.3指针

     

4将空指针转换为另一种指针类型会产生该类型的空指针。   任何两个空指针都应该相等   5整数可以转换为任何指针类型。除非事先指明,否则   结果是实现定义的,可能没有正确对齐,可能不指向   引用类型的实体,可能是陷阱表示.67)
  6任何指针类型都可以转换为整数类型。除非事先指明,否则   结果是实现定义的。如果结果无法以整数类型表示,   行为未定义。结果不必在任何整数的值范围内   类型。

     

67)将指向整数或整数的指针转换为指针的映射函数旨在与执行环境的寻址结构保持一致。

所以,即使UB这次恰好是良性,它仍可能导致一些完全出乎意料的数字。

答案 4 :(得分:0)

没有。让我们区分开来:

&(((struct name *)NULL)->b);

与:

相同
struct name * ptr = NULL;
&(ptr->b);

第一行显然是有效且定义明确。

在第二行中,我们计算相对于地址0x0的字段的地址,这也是完全合法的。例如,Amiga在地址0x4中有指向内核的指针。所以你可以使用这样的方法来调用内核函数。

事实上,在C macro offsetofwikipedia)上使用相同的方法:

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

因此,这里的混乱围绕着NULL指针是可怕的事实。但是从编译器和标准的角度来看,表达式在C语言中是合法的(C ++是一种不同的野兽,因为你可以重载&运算符)。

答案 5 :(得分:0)

C标准中的任何内容都不会对系统对表达式的作用施加任何要求。在编写标准时,它会在运行时导致以下事件序列完全合理:

  1. 代码将空指针加载到寻址单元
  2. 代码要求寻址单元添加字段b的偏移量。
  3. 尝试将整数添加到空指针时,寻址单元会触发陷阱( >运行时陷阱,即使许多系统没有捕获它)
  4. 系统在通过一个从未设置的陷阱向量进行调度后开始执行基本上随机的代码,因为设置它的代码会浪费内存,因为不应该发生寻址陷阱。
  5. 当时未定义行为的含义本质。

    请注意,自C早期出现的大多数编译器都将位于常量地址的对象成员的地址视为编译时常量,但我不认为这种行为是当时强制要求,也没有添加任何标准,这将要求在运行时计算不能的情况下定义涉及空指针的编译时地址计算。