使用字符类型上次写入时,使用非字符类型读取对象时的未定义行为

zwo*_*wol 20 c standards strict-aliasing language-lawyer

假设unsigned int没有陷阱表示,请执行下面标记为(A)和(B)的语句中的任何一个或两个引发未定义的行为,为什么或为什么不这样做,以及(特别是如果您认为其中一个定义明确但另一个不定义),您认为标准中存在缺陷吗?我主要对当前版本的C标准(即C2011)感兴趣,但如果在标准的旧版本或C++中有所不同,我也想知道这一点.

(_Alignas在这个程序中用于消除因对齐不充分而导致的任何UB问题.我在解释中讨论的规则虽然没有说明对齐.)

#include <stdlib.h>
#include <string.h>

int main(void)
{
    unsigned int v1, v2;
    unsigned char _Alignas(unsigned int) b1[sizeof(unsigned int)];
    unsigned char *b2 = malloc(sizeof(unsigned int));

    if (!b2) return 1;

    memset(b1, 0x55, sizeof(unsigned int));
    memset(b2, 0x55, sizeof(unsigned int));

    v1 = *(unsigned int *)b1; /* (A) */
    v2 = *(unsigned int *)b2; /* (B) */

    return !(v1 == v2);
}
Run Code Online (Sandbox Code Playgroud)

我对C2011的解释是(A)引发未定义的行为,但(B)定义明确(存储未指定的值v2),因为:

  • memset被定义(§7.24.6.1)写入它的第一个参数作为-如果通过与字符类型,它被允许用于两左值b1b2每特殊情况下,在§6.5p7的底部.

  • 该对象b1具有声明的类型unsigned char[n].因此,它的有效访问类型也是unsigned char[n]6.5p6.语句(A)b1通过一个类型为的左值表达式读取,该表达式unsigned int不是b16.5p7中的有效类型或任何其他异常,因此行为未定义.

  • 指向的对象b2没有声明的类型.存储在其中的值(by memset)是(as-if)通过具有字符类型的左值,因此第二种情况6.5p6不适用.该值未从任何地方复制,因此第三种情况6.5p6也不适用.因此,对象的有效类型是用于访问的左值的类型,即unsigned int,满足6.5p7的规则.

  • 最后,根据6.2.6.1,假设unsigned int没有陷阱表示,memset操作unsigned int在每个b1和中创建了一些未指定值的表示b2.因此,如果(A)和(B)都没有引起未定义的行为,那么实际的值v1v2未指定但它们是相等的.

评论:

"基于类型的别名"规则(即6.5p7)的不对称性,允许具有任何有效类型的对象由具有字符类型的左值访问,但反之不然,是混乱的持续来源.6.5p6的第二种情况似乎是专门添加的,以防止它是未定义的行为来读取由memset(或者,就此而言calloc)初始化的值,但是,因为它只适用于没有声明类型的对象,它本身就是另一个来源混乱.

Phi*_*ler 0

经过表面检查,我同意你的评估(A 是 UB,B 很好),并且可以提供具体的理由说明为什么应该这样(在编辑之前包括_Alignas()):Alignment

堆栈char[]上的 可以从任何地址开始,无论该地址是否有效unsigned int。相反,malloc()需要返回满足相关平台上任何本机类型最严格对齐要求的内存。

该标准显然不想对char[]超出 的对齐要求强加char,因此它必须将对 it 的类型双关访问保留为潜在未定义。

  • 在 C11 中,添加了显式对齐控制,6.5 中的措辞没有改变——通过类型为“unsigned int”的左值访问“unsigned char _Alignas(unsigned int) b1[sizeof(unsigned int)]”仍然未定义,如下所示我读了标准。这“可能”是一种疏忽,或者可能意味着一致性并不是标准措辞原样的唯一原因。 (3认同)
  • 对齐与锯齿无关 (3认同)