【问题标题】:Inconsistent strict aliasing rules不一致的严格别名规则
【发布时间】:2019-03-19 12:36:58
【问题描述】:

我有以下程序,通过将 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");
}

使用 GCC 版本 8.2.1 进行编译时,我收到以下警告消息:

> gcc -std=c99 -Wall -Wextra -Wshadow -fsanitize=address,undefined -O2 main.c
main.c: In function ‘inita’:
main.c:11:3: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
  *(uint64_t *) (ctx->a) = *(uint64_t *) (aptr);
   ^~~~~~~~~~~~~~~~~~~~~

我读到了严格的别名规则,如果打破这个规则可能会出错,这是有道理的。 但是,为什么我在 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)或者这样做是否安全?

【问题讨论】:

  • 我在 gcc 5.4.0 上得到了同样的结果。
  • 尝试在 initb() 中展开循环,您将看到与 inita() 相同的警告。所以当涉及到循环变量时,只是 gcc 没有识别出问题

标签: c strict-aliasing


【解决方案1】:

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

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

您需要明确禁用严格别名,以免编译器生成错误代码。您还没有处理潜在的对齐问题。当你修复这些问题时,你也让你的代码更难移植。

改用memcpy

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

【讨论】:

  • 如果我以与更改 inita 相同的方式更改 initb 函数,将缓冲区转换为正确的大小,那么这仍然是个问题吗?
  • @Noxet 您更改的函数修复了对ctx-&gt;a 的访问,但不是aptr。您将 uint8_t 取消引用为 uint32_t。那仍然无效。
  • 当然。。那编译器只报一半的问题就有点奇怪了。哦,好吧,现在我知道如何解决它了
【解决方案2】:

在丹尼斯·里奇发明的语言中,称为 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;
}

将假定因为p1p2 是不同的类型,并且没有可辨别的关系,它们不会别名,但会认识到因为p2q2 具有相同的类型,他们可以识别相同的对象。它将进一步认识到左值*(float*)q2 显然是基于q2。因此,它将认识到对*(float*)q2 的访问可能是对*p2 的访问。此外,icc 会将volatile 视为“不要假设您了解这里发生的一切”的指示,因此允许通过q3 进行访问可能会以奇怪的方式影响其他对象。

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

【讨论】:

    猜你喜欢
    • 2015-10-15
    • 2017-02-25
    • 2013-03-11
    • 2018-12-14
    • 2015-05-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-01-29
    相关资源
    最近更新 更多