严格的混叠规则不一致

时间:2019-03-19 12:36:59

标签: 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 *) (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,我的代码中仍然存在别名问题吗?或者这样做安全吗?

2 个答案:

答案 0 :(得分:2)

  

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

是的,您的两个函数均存在混叠问题,并且不安全。无法保证它会继续工作。编译器在尝试检测时只会感到困惑。

您需要明确禁用严格的别名,以免冒编译器生成错误代码的风险。您还没有处理潜在的对齐问题。修复这些错误后,还使代码难以移植。

改为使用memcpy

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

答案 1 :(得分:1)

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的对象可能会受到以下行为的影响:以地址int*构成foo.x+i,将其解除引用,然后写入结果类型为{{ 1}}。相反,该标准的作者依靠质量实现来做出一些努力来识别明显的情况,在这种情况下,一种类型的指针或左值是从另一种类型的指针或左值派生的。

icc编译器,给出:

int

会假设由于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的访问。此外,icc会将*p2视为“不要假设您了解这里正在发生的一切”指示,因此可以考虑通过volatile进行的访问可能以奇怪的方式影响其他对象。

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