C 不一致的严格别名规则

C 不一致的严格别名规则,c,strict-aliasing,C,Strict Aliasing,我有下面的程序,通过将8位缓冲区转换为32位和64位值,以一种看似快速的方式初始化两个缓冲区 #include <stdio.h> #include <stdint.h> typedef struct { uint32_t a[2]; uint16_t b[4]; } ctx_t; void inita(ctx_t *ctx, uint8_t *aptr) { *(uint64_t *) (ctx->a) = *(uint64_t *)

我有下面的程序,通过将8位缓冲区转换为32位和64位值,以一种看似快速的方式初始化两个缓冲区

#include <stdio.h>
#include <stdint.h>

typedef struct {
    uint32_t a[2];
    uint16_t b[4];
} ctx_t;

void inita(ctx_t *ctx, uint8_t *aptr)
{
    *(uint64_t *) (ctx->a) = *(uint64_t *) (aptr);
}

void initb(ctx_t *ctx, uint8_t *bptr)
{
    for (int i = 0; i < 2; i++) {
        *((uint32_t *) (ctx->b) + i) = *(uint32_t *) (bptr);
    }
}

int main()
{
    uint8_t a[8] = {0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef};
    uint8_t b[8] = {0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef};

    ctx_t ctx;
    inita(&ctx, a);
    initb(&ctx, b);

    printf("a: ");
    for (int i = 0; i < 8; i++) {
        printf("%02x", a[i]);
    }

    printf("\nb: ");
    for (int i = 0; i < 8; i++) {
        printf("%02x", b[i]);
    }
    printf("\n");
}
我读过关于严格的别名规则的书,有一点是有道理的,那就是打破这个规则可能会出问题。 但是,为什么在initb()函数中没有得到相同的警告?在这里,我还将指针和缓冲区转换为声明之外的其他大小

程序运行并产生预期输出:

a: 0123456789abcdef
b: 0123456789abcdef
如果我修复了警告,请执行以下操作:

void inita(ctx_t *ctx, uint8_t *aptr)
{
    *(uint32_t *) (ctx->a) = *(uint32_t *) (aptr);
    *(uint32_t *) (ctx->a + 1) = *(uint32_t *) (aptr + 4);
}
然后,我现在得到与以前相同的结果,但没有警告

我的代码中是否仍然存在别名问题(由于initb),或者这样做是否安全

我的代码中是否仍然存在别名问题(由于initb),或者这样做是否安全

是的,这两个函数都存在别名问题,不,这是不安全的。无法保证它会工作或继续工作。编译器只是在尝试检测它时感到困惑

为了避免编译器生成错误代码的风险,您需要明确禁用严格别名。您还没有考虑到潜在的对齐问题。当您修复这些问题时,也会使代码更难移植

改用
memcpy

void inita(ctx_t *ctx, uint8_t *aptr)
{
    memcpy(ctx->a, aptr, sizeof uint64_t);
}

在Dennis Ritchie发明的称为C的语言中,以及在C编程语言的两个版本中描述的语言(但扩展到包括64位类型),上述代码的一个问题是无法保证自动
char[]
对象将以适合通过64位指针访问的方式对齐。这在x86或x64平台上不会有问题,但在基于其他架构(如ARM Cortex-M0)的平台上会有问题

MSVC或icc等实现所处理的方言支持标准委员会所描述的C的精神,包括“不阻止程序员做需要做的事情”的原则,并努力支持低级编程,将认识到形式
*(t*)的构造pointerOfTypeU
可能会访问U类型的对象。尽管标准没有强制要求提供此类支持,但这可能是因为作者希望实现至少会做出一些努力来识别一种类型的指针由另一种类型的左值构成的情况,并且认为这种认识可以作为一个实施质量问题。请注意,标准中没有要求编译器:

struct foo {int x[10]; };

void test(struct foo *p, int i)
{
  struct foo temp;
  temp = *p;
  temp.x[i] = 1;
  *p = temp;
}
认识到类型为
struct foo
的对象可能会受到以下操作的影响:使用地址
foo.x+i
形成
int*
,取消对其的引用,然后写入类型为
int
的结果左值。相反,该标准的作者依靠高质量的实现来做出一些努力,以识别一种类型的指针或左值是从另一种类型的指针或左值派生出来的明显情况

icc编译器,给出:

int test1(int *p1, float *q1)
{
    *p1 = 1;
    *q1 = 2.0f;
    return *p1;
}
int test2(int *p2, int *q2)
{
    *p2 = 1;
    *(float*)q2 = 2.0f;
    return *p2;
}
int test3(int *p3, float volatile *q3)
{
    *p3 = 1;
    *q3 = 2.0f;
    return *p3;
}
将假定由于
p1
p2
是不同的类型,并且没有可识别的关系,因此它们不会别名,但会识别出因为
p2
q2
具有相同的类型,所以它们可以识别相同的对象。它将进一步认识到左值
*(float*)q2
显然基于
q2
。因此,它将认识到对
*(float*)q2
的访问可能是对
*p2
的访问。此外,icc将把
volatile
视为“不要假设你了解这里发生的一切”的指示,因此允许通过
q3
访问可能以奇怪的方式影响其他对象


一些编译器,如clang和gcc,除非通过
-O0
-fno严格别名
强制以适合低级编程的方式运行,将标准解释为忽略不同类型左值之间的明显关系的借口,除非这样做会严重破坏语言结构,使它们几乎毫无用处。虽然他们碰巧认识到对
someUnion.array[i]
的访问是对
someUnion
的访问,但他们确实认识到
*(someUnion.array+i)
同样,即使
someUnion.array[i]
的定义是
*(someUnion.array+i)
。鉴于该标准将对几乎所有与混合类型存储有关的支持都视为“实现质量”问题,因此真正可以说的是,适用于不同用途的编译器支持不同的结构组合。

我在gcc 5.4.0上也得到了同样的结果。试着在initb()中展开循环,您将看到与inita()相同的警告。因此,如果我改变initb函数的方式与改变inita的方式相同(我将a缓冲区转换为正确的大小),那么gcc没有意识到涉及循环变量时的问题,这仍然是一个问题吗?@Noxet您改变的函数修复了对
ctx->a
的访问,而不是
aptr
。您正在将
uint8\u t
作为
uint32\u t
解除引用。那仍然是无效的。当然。。然后有点奇怪,编译器只抱怨问题的一半。哦,好吧,现在我知道怎么修了
int test1(int *p1, float *q1)
{
    *p1 = 1;
    *q1 = 2.0f;
    return *p1;
}
int test2(int *p2, int *q2)
{
    *p2 = 1;
    *(float*)q2 = 2.0f;
    return *p2;
}
int test3(int *p3, float volatile *q3)
{
    *p3 = 1;
    *q3 = 2.0f;
    return *p3;
}