【问题标题】:Is using a structure without all members assigned undefined?是否使用未分配所有成员的结构?
【发布时间】:2018-05-06 02:15:17
【问题描述】:

在块范围内考虑这段代码:

struct foo { unsigned char a; unsigned char b; } x, y;
x.a = 0;
y = x;

C [N1570] 6.3.2.1 2 说“如果左值指定了一个可以使用 register 存储类声明的具有自动存储持续时间的对象(从未占用其地址),并且对象未初始化(未使用初始化程序声明,并且在使用之前未对其执行任何赋值),行为未定义。”

虽然x 的成员已被赋值,但尚未执行对x 的赋值,其地址也未被占用。因此,看来 6.3.2.1 2 告诉我们xy = x 中的行为是未定义的。

但是,如果我们为x 的每个成员都分配了一个值,那么将x 视为出于6.3.2.1 2 的目的未初始化似乎是不合理的。

(1) 严格来说,标准中是否有任何内容导致 6.3.2.1 2 不适用于(使未定义)上述代码?

(2) 假设我们正在修改标准或确定对 6.3.2.1 2 的合理修改,是否有理由更喜欢以下其中之一? (a) 6.3.2.1 2 不适用于结构。 (b) 如果结构的至少一个成员已被赋值,则该结构不会为 6.3.2.1 2 的目的而未初始化。 (c) 如果结构的所有命名1 成员已为 6.3.2.1 2 分配了一个值,该结构不会未初始化。

脚注

1 结构可能有未命名的成员,因此并不总是可以为结构的每个成员赋值。 (根据 6.7.9 9,即使结构已初始化,未命名的成员也具有不确定的值。)

【问题讨论】:

  • 在 C++ 中,它是 UB,因为可能会捕获 int,并且默认的复制构造函数执行成员复制。见stackoverflow.com/questions/9163555/…。但是 C 和 C++ 在这类事情上的分歧相当大。我不认为 C 副本以相同的方式工作。不幸的是,高于我的工资等级,但 FWIW 我倾向于“是的,这是 UB 方面”。
  • @Bathsheba 我倾向于另一边,因为如果不确定值是陷阱表示,则读取未初始化的值只是 UB,而 int 值很少(如果有的话?)有这些。另一方面,there's this 所以我不再那么确定了。真的高于我的工资等级...... :)
  • @EricPostpischil:因此,为了避免学究,修改问题以使用unsigned char 类型是否明智?
  • 我刚刚编辑使用unsigned char 明确表示我们对陷阱表示不感兴趣,只是对 6.3.2.1 2 的含义。
  • @Bathsheba:是的。起初我不愿意编辑这个问题,以防对起草答案的人有点不公平。但显然我需要采取先发制人的措施。

标签: c struct language-lawyer undefined-behavior


【解决方案1】:

我的观点是它是未定义的行为,仅仅是因为它没有被标准明确定义。来自 4 Conformance §2(强调我的):

...未定义的行为是否则 在本国际标准中用“未定义行为”或 省略任何明确的行为定义

在 N1570 草案中多次阅读后,我找不到任何明确的使用部分初始化的结构的行为定义。一方面 6.3.2.1 §2 说:

...如果 左值指定一个自动存储持续时间的对象,该对象可能已经 使用寄存器存储类声明(从未占用其地址),以及该对象 未初始化(未使用初始化程序声明且未对其赋值 在使用之前执行),行为未定义

所以这里x 是自动的,从未被初始化(只有它的一个成员),并且不可否认它的地址从未被占用,所以我们可以认为它是明确的UB

另一方面,6.2.6.1 §6 说:

...结构或联合对象的值永远不是陷阱表示,即使结构或联合对象的成员的值可能是陷阱表示。

因为 6.2.6.1 §5 刚刚定义了一个陷阱表示:

某些对象表示不需要表示对象类型的值。如果存储 一个对象的值具有这样的表示,并由一个左值表达式读取 没有字符类型,行为未定义。如果产生这样的表示 通过一个左值表达式修改对象的全部或任何部分的副作用,这意味着 a 成员的值为 0,b 成员的值为未定义。 没有字符类型,行为是未定义的。50)这样的表示被称为 陷阱表示。

我们可以认为获取结构的值总是合法的,因为它不能是陷阱表示

此外,我不清楚设置结构成员的值是否实际上使结构处于 unitialized 状态。

出于所有这些原因,我认为该标准没有明确定义行为应该是什么,仅仅因为这个原因,它是未定义的行为。


话虽如此,我很确定任何常见的编译器都会接受它,并将给 y x 的当前表示,这意味着 a 成员的值为 0 和与当前表示相同的不确定值一个用于x.b,用于b 成员。

【讨论】:

  • 我接受了这个答案并获得了赏金,因为我认为这是正确的答案——从技术上讲,标准未定义该行为,即使这可能不是本意。
  • 我不确定我是否遵循您的逻辑。如果结构/联合值永远不会是陷阱表示的保证将非常有用,如果它意味着将此类对象从一个可读存储区域复制到一个不相交的可写存储区域将在最坏的情况下使目标部分保留不确定值,并且否则很少有用。至少,这应该意味着质量实现应该以这种方式表现,除非它们记录了这样做的令人信服的理由。至于专业化或低质量的实现可能会做什么,谁知道呢?
【解决方案2】:

首先,让我们注意 6.3.2.1/2 中引用的部分,即所谓的“Itanium 子句”是该代码可能有问题的唯一子句。换句话说,如果这个子句不存在,那么代码就可以了。结构可能没有陷阱表示,因此即使 x 完全未初始化,y = x; 也可以。 DR 451 的决议阐明了不确定的值可以通过赋值传播,而不会导致 UB。


回到这里的 Itanium 子句。正如您所指出的,标准没有明确说明x.a = 0; 是否否定前提条件“x 未初始化”。

IMO,这意味着我们应该求助于 Itanium 条款的基本原理来确定意图。一般来说,标准文件的措辞是为了实现一个意图;一般来说,我不同意对标准的微小细节持教条主义:从措辞中剔除那些并非创造措辞的人所意图的含义。

This Q/A 很好地解释了基本原理。潜在的问题是x 可能存储在设置了 NaT 位的寄存器中,然后y = x 将由于读取设置了该位的寄存器而导致硬件异常。


所以问题是:在 IA64 上,x.a = 0; 是否清除了 NaT 位?我不知道,我想我们需要熟悉该平台的人在这里给出结论性的答案。

天真地,我想如果x 在寄存器中,那么通常x.a = 0; 将需要读取旧值,并应用掩码来清除a 的位,从而在以下情况下触发异常xNaT。但是x.a = 0;不能触发UB,所以逻辑一定是不正确的。也许 IA64 编译器从不将结构存储在寄存器中,或者它们在声明 1 时清除 NaT 位,或者可能有一条硬件指令在以前的 NaT 寄存器上实现 x.a = 0;,我不知道。

【讨论】:

  • @DrorK。如果该文件实际上自己回答了这些问题,那就太好了。但它没有
  • @DrorK.:Dennis Ritchie 是标准的规范部分吗?
  • 当然。我认为这个问题和讨论是学术性的——它是一种思考标准的练习。我希望我们都同意编译器编写者应该如何解释这一点——未定义的行为不适用于问题中的代码。但我们最终可能会向标准委员会报告缺陷。
  • @EricPostpischil:至于这里的歧义是否是“缺陷”,这取决于标准是否旨在全面描述质量编译器必须做的所有事情,或者是否它旨在省略以下情况: (1) 存在一种明显的合理行为,以及 (2) 没有理由期望寻求编写高质量编译器的人会在某些诊断场景之外做任何其他事情无论如何都不需要受标准的约束。如果代码复制了部分写入的结构,并忽略了副本中未写入原始结构的字段...
  • @EricPostpischil: ...允许复制操作“提高实现定义的信号”以帮助符合各种编码标准可能有一些好处,但除非实现记录了这种行为,我认为没有理由将定义行为的成本效益比视为压倒性的支持。在减少代码大小和执行时间方面的显着优势,通常是零成本,而且成本从来不会很高。
【解决方案3】:

复制部分编写的结构属于质量实现将以一致的方式处理的操作类别没有很好的理由不这样做,专门的实现可能会以不同的方式处理,因为它们有充分的理由这样做,质量差但符合标准的实现可能会被用作行为荒谬的借口。

请注意,复制自动持续时间或 malloc 创建的字符数组的未初始化值将属于类似的操作类别,除了会捕获此类操作的实现(例如,帮助程序员识别和追踪潜在的信息泄漏) 将不允许将自己描述为“符合”。

专门用于诊断意外信息泄漏的实现可能会明智地限制复制部分编写的结构的努力。在使用某种类型的统一化值可能导致奇怪行为的实现中,复制具有该类型的统一化成员的结构然后尝试使用该副本的该成员可能会明智地这样做。

标准没有特别说明部分编写的结构是否算作已编写,因为寻求产生高质量实现的人不应该关心。专门用于检测潜在信息泄漏的质量实现应该在任何试图复制未初始化数据的尝试时发出警告,而不考虑标准何时允许或不允许此类行为(前提是它们将自己描述为不合格)。旨在支持各种程序的高质量通用实现应该允许在程序不查看整个结构复制上下文之外的未初始化部分的情况下复制部分初始化的结构(这种处理很有用并且通常成本在非人为的情况下什么都没有)。该标准可以被解释为授予低质量但符合标准的实现权利,将复制部分编写的结构作为无意义行为的借口,但这种实现几乎可以使用任何东西作为这样的借口。复制结构时,高质量的实现不会做任何不寻常的事情,除非他们记录了这样做的充分理由。

【讨论】:

    【解决方案4】:

    C 标准规定结构类型不能有陷阱表示,尽管结构的成员可以。该保证有用的主要情况是涉及部分书面结构的情况。此外,禁止在编写所有成员之前复制结构,即使是副本的接收者永远不会使用的结构,这将要求程序员编写不必要的低效代码并且没有任何用处。以“优化”的名义提出这样的要求是完全愚蠢的,而且我知道没有证据表明该标准的作者打算这样做。

    不幸的是,该标准的作者使用相同的术语来描述两种情况:

    1. 有些实现在所有情况下定义了某个动作 X 的行为,而有些只为某些定义了它;标准的其他部分定义了一些特定情况下的操作。作者想说的是,实现不需要像在所有情况下定义行为的那些一样,无需撤销标准中其他地方做出的保证

    2. 虽然标准的其他部分会在某些情况下定义动作 X 的行为,但在所有此类情况下保证行为可能代价高昂,并且实现不需要保证它们即使在其他部分的情况下标准将定义它们

    在编写标准之前,一些实现会将所有自动变量初始化为零。因此,这些实现将保证读取未初始化值的行为,即使是带有陷阱表示的类型。该标准的作者希望明确表示他们不想要求所有实现都这样做。此外,一些对象可以定义所有位模式在存储在内存中时的行为,而不是在存储在寄存器中时。然而,这种处理通常仅限于标量类型,而不是结构。

    从实际的角度来看,将复制结构的行为定义为复制所有字段的状态(已定义或不确定)与允许编译器在复制部分写入的结构时以任意方式行为相比,成本不会更高。不幸的是,一些编译器作者错误地认为“聪明”和“愚蠢”是反义词,因此表现得好像标准的作者希望邀请编译器假设程序永远不会收到任何会导致结构被复制的输入部分写了。

    【讨论】:

    • 陷阱表示与此问题无关。 6.3.2.1 2 不涉及陷阱表示;即使对象类型没有陷阱表示,它也会在给定条件下使行为未定义。
    • @EricPostpischil:在 C89 中,未指定值和不确定值之间的区别在于,持有前者的对象将保证持有其类型的值,而后者可以持有值或陷阱表示。如果您认为标准的作者无意禁止具有陷阱表示的结构,以保证如果没有尝试访问副本的不确定成员(除了制作更多副本的目的),可以安全复制部分编写的结构结构),也许你可以......
    • ...请告诉我您认为该禁令的预期目的是什么?我知道现代编译器编写者想要处理一种语言,该语言要求程序浪费时间编写具有已知位值的东西,即使在位值的每个可能组合本来可以满足程序要求的情况下,但我不认为那是作者C89 的想法(如果这是标准的更高版本的作者所想到的,我会认为 C89 更优越)。
    • 作为 M.M.注意,6.3.2.1 2 中的这句话是为了支持 Itanium/IA-64 检测寄存器中未初始化数据的能力而创建的。它与陷阱表示完全分开。它甚至适用于无符号整数,包括没有陷阱表示的 char。 Itanium 寄存器有一个额外的位,指示寄存器中是否有有效数据。如果在没有初始化或赋值的情况下使用此类寄存器中的对象,则会发生硬件异常。添加 6.3.2.1 2 是为了说明 C 实现可以执行此操作,即使类型中没有陷阱表示。
    • @EricPostpischil:早在 Itanium 之前,机器在寄存器和内存中就有不同的表示形式,并且在没有所有位模式对该类型有效的情况下将寄存器用于一种类型并不罕见。如果一个双成员结构被分配了一对寄存器,并且在将结构复制到另一对寄存器之前写入了一个成员,则未写入的成员可能具有对其类型无效的位模式,这可能会导致麻烦如果代码实际尝试使用该值。也许安腾已经被破坏得足以支持这种语义......
    猜你喜欢
    • 2021-08-25
    • 1970-01-01
    • 2021-12-09
    • 2017-02-23
    • 1970-01-01
    • 2012-09-22
    • 2020-05-02
    • 2016-07-20
    • 2011-08-29
    相关资源
    最近更新 更多