【问题标题】:Is it undefined behaviour to memcpy from an uninitialized variable?从未初始化的变量 memcpy 是未定义的行为吗?
【发布时间】:2016-01-28 09:00:10
【问题描述】:

是否使用未初始化的变量作为 src 来处理 C 中的 memcpy 未定义行为?

void foo(int *to)
{
  int from;
  memcpy(to, &from, sizeof(from));
}

【问题讨论】:

  • 行为不是未定义的。但是请注意,编译器可能会完全删除 memcpy,因为函数末尾的 *to 无论如何都是未定义的。
  • 我假设稍后使用*to 是未定义的行为,就像直接使用from 一样,但这个问题是关于memcpy 本身的。我认为*to = from 将是未定义的行为,即使该值并未“真正”使用。
  • @Quentin 这没有任何意义。没有“未定义的值”之类的东西。
  • @Quentin 所以你是说复制字节的后置条件不是副本等于原始,对吧?

标签: c language-lawyer undefined-behavior memcpy


【解决方案1】:

C 委员会建议对defect report 451: instability of uninitialized automatic variables 的回复是:

问题 3 的答案是库函数将展示 用于不确定值时的未定义行为。

如果确实是这样的话,缺陷中的问题已经为 memcpy 和 fwrite 寻求豁免:

[...] 人们希望能够复制未初始化的填充这一事实 使用 memcpy 没有未定义行为的结构中的字节是原因 使用未初始化对象的值不是未定义的 行为。这似乎表明一个结构的 fwrite 与 未初始化的填充字节不应表现出未定义的行为。

提议响应的这一部分似乎是针对对未初始化填充的担忧:

委员会还指出,结构中的填充字节是 可能是一种独特的“摇摆不定”的表现形式。

我们可以看到defect report 338: C99 seems to exclude indeterminate value from being an uninitialized register 表格,这与过去的预期有些不同。它还说:

[...] 我 相信将 unsigned char 类型排除在陷阱之外的意图 表示是允许它用于复制(通过 memcpy) 任意内存,在内存可能包含陷阱的情况下 某些类型的表示。[...]

博文Reading indeterminate contents might as well be undefined 涵盖了在 C 中很好地读取不确定值的演变过程,并对我上面提到的变化进行了一些了解。

值得注意的是,这与 C++ 不同,reading an indeterminate value from a narrow unsigned char is not undefined behaviordefect report 240 注意到了这种差异:

C 委员会正在其 DR338 中处理类似问题。根据这一分析,他们计划通过增加对左值到右值转换版本的描述来采取与上述方法几乎相反的方法。 CWG 认为,如果 unsigned char 在寄存器中分配,访问它可能仍会陷入陷阱,因此需要重新评估提议的解决方案。另见第 129 期。

【讨论】:

  • 我认为这是迄今为止最好的答案,但最终委员会从未真正回答从未初始化的变量中读取 memcpy 本身是否是未定义的行为。
  • @TorKlingberg 不,它没有直接解决这个问题,尽管由于 memcpy 的主要问题是填充,并且有一个项目符号涵盖了该问题,它确实间接解决了它。
  • 恕我直言,C11 对不确定值的描述应被视为对早期版本中指定不佳或错误的错误修复(尽管安腾例外是新增内容)。
  • @M.M 这很有趣,因为您在回答中引用了同一篇文章。
  • @M.M 将未指定的值乘以 2 并将结果取模 2^32 会产生偶数结果。只有未定义的行为解释了作为该计算结果出现的奇数值。您所描述的“j *= 2 使不确定值不确定”并不是在已发布的标准中如何描述“不确定”,尽管它是提出的“不稳定值”的单独概念将如何工作。
【解决方案2】:

这是与复制操作相关的定义行为,除非int 在您的系统中有陷阱表示。定义 int from 时在堆栈上分配内存。这个int 的内容是当时在堆栈中的那个位置上发生的任何事情。因此,最终结果,被复制到 toint 的值未定义(不确定)。

其他答案引用了 C 标准,当未初始化变量的值被“使用”时,会发生未定义的行为。如果您不使用该值,这显然不适用。在复制/分配未初始化的变量时,C11 标准的未定义行为中还提到:

6.3.2.1p2

如果左值指定一个自动存储持续时间的对象, 可以用寄存器存储类声明(从来没有 它的地址被占用),并且该对象未初始化(未声明 使用初始化程序并且之前没有对其进行分配 使用),行为未定义。

这也不会影响您的代码,因为from 的地址是在您调用memcpy 时获取的

C11 标准的另一个相关部分是6.2.6.1

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

一些非常老的处理器可能有一个陷阱表示,用于int 软件可见的奇偶校验位或非二进制补码架构中的“负零”。例如,x86 处理器没有int 的陷阱表示。

【讨论】:

  • 我知道*to 中的值将是未指定或未初始化的,但假设 *to 从未实际使用过,这段代码是否有效?
  • 是的,代码有效。声明 int from 后,您可以合法地复制 contains 中的(随机)值。
  • 请提供该声明的来源。理想的 C 应该如何表现是不够的。特别是,复制相同的未初始化变量两次是否会导致相同的值两次?毕竟,如果它是真正被复制的“随机数据”,它可能会复制堆栈的随机位,memcpy 可能使用的堆栈相同。
  • @MSalters 复制相同的初始化变量两次并不保证复制相同的值两次。当这个变量被复制时,另一个线程可能正在写入它。我的回答非常清楚,不能保证被复制的变量的值。我不明白你的反对意见。我将添加其他答案中未提及的标准引用。
  • “这个 int 的内容是在那一刻发生在堆栈中那个位置的任何东西。” - 这是一个实现细节。通常可能没有堆栈(我使用了一个没有堆栈的实现)。在标准 C 中, int 具有不确定的值;不是碰巧在某个地方的某个特定值。
【解决方案3】:

(之前 -没有注意到from的地址通过)。不,这不会导致未定义的行为,只是 from 具有不确定的价值。只要您打算使用未初始化变量的值,程序将具有明确定义的行为。

(因为分配空间而不是初始化变量不是UB。)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-07-31
    • 2020-02-12
    • 2015-04-19
    • 1970-01-01
    • 2021-11-11
    • 2021-09-07
    相关资源
    最近更新 更多