对 char 数组结构成员进行类型双关

type-punning a char array struct member

考虑以下代码:

typedef struct { char byte; } byte_t;
typedef struct { char bytes[10]; } blob_t;

int f(void) {
  blob_t a = {0};
  *(byte_t *)a.bytes = (byte_t){10};
  return a.bytes[0];
}

这会在 return 语句中产生别名问题吗?您确实 a.bytes 取消引用了一个不为 patch 中的赋值指定别名的类型,但另一方面,[0] 部分取消引用了一个别名的类型。

我可以构造一个稍微大一点的例子,其中 gcc -O1 -fstrict-aliasing 确实使函数 return 0,我想知道这是否是 gcc 错误,如果不是,我能做什么避免这个问题(在我的真实示例中,赋值发生在一个单独的函数中,因此两个函数在孤立的情况下看起来真的很无辜)。

这里有一个更长更完整的测试示例:

#include <stdio.h>

typedef struct { char byte; } byte_t;
typedef struct { char bytes[10]; } blob_t;

static char *find(char *buf) {
    for (int i = 0; i < 1; i++) { if (buf[0] == 0) { return buf; }}
    return 0;
}

void patch(char *b) { 
    *(byte_t *) b = (byte_t) {10}; 
}

int main(void) {
    blob_t a = {0};
    char *b = find(a.bytes);
    if (b) {
        patch(b);
    }
    printf("%d\n", a.bytes[0]);
}

使用 gcc -O1 -fstrict-aliasing 生成 0

错误在 *(byte_t *)a.bytes = (byte_t){10};。 C 规范有一条关于字符类型的特殊规则 (6.5§7),但该规则仅适用于使用字符类型访问任何其他类型的情况,而不适用于使用任何类型访问字符的情况。

这里的主要问题是这两个结构是不兼容的类型。因此对齐和填充可能会出现各种问题。

撇开这个问题不谈,标准 6.5/7 只允许这样做(“严格的别名规则”):

An object shall have its stored value accessed only by an lvalue expression that has one of the following types:

  • a type compatible with the effective type of the object,
    ...
  • an aggregate or union type that includes one of the aforementioned types among its members

查看 *(byte_t *)a.bytes,然后 a.bytes 具有 有效类型 char[10]。该数组的每个单独成员依次具有有效类型 char。您使用 byte_t 取消引用它,它不是兼容的结构类型,它的成员中也没有 char[10]。不过它确实有 char

标准并没有明确说明如何处理有效类型为数组的对象。如果你严格阅读上面的部分,那么你的代码确实违反了严格的别名,因为你通过一个没有 char[10] 成员的结构访问 char[10] 。我也有点担心编译器填充任一结构以满足对齐。

一般来说,我只是建议不要做这样可疑的事情。如果您需要类型双关,请使用联合。如果您希望使用原始二进制数据,请使用 uint8_t 而不是可能已签名且不可移植的 char.

根据标准,语法 array[index] 是 shorthand for *((array)+(index))。因此,p->array[index] 等同于 *((p->array) + (index)),它使用 p 的地址来计算 p->array 的地址,然后不考虑 p 的类型, 添加 index (按数组元素类型的大小缩放),然后取消引用结果指针以产生数组元素类型的左值。标准的措辞中没有任何内容暗示通过结果左值的访问是对基础结构类型左值的访问。因此,如果结构成员是字符类型的数组,N1570 6.5p7 的约束将允许该形式的左值访问任何类型的存储。

然而,一些编译器(例如 gcc)的维护者似乎将标准的松散视为一个缺陷。这可以通过代码演示:

struct s1 { char x[10]; };
struct s2 { char x[10]; };
union s1s2 { struct s1 v1; struct s2 v2; } u;

int read_s1_x(struct s1 *p, int i)
{
    return p->x[i];
}
void set_s2_x(struct s2 *p, int i, int value)
{
    p->x[i] = value;
}
__attribute__((noinline))
int test(void *p, int i)
{
    if (read_s1_x(p, 0))
        set_s2_x(p, i, 2);
    return read_s1_x(p, 0);
}
#include <stdio.h>
int main(void)
{
    u.v2.x[0] = 1;
    int result = test(&u, 0);
    printf("Result = %d / %d", result, u.v2.x[0]);
}

代码遵守 N1570 6.5p7 中的约束,因为它对 u 任何部分的所有访问都是使用字符类型的左值执行的。尽管如此,gcc 生成的代码不允许 (*(struct s1))->x[0] 访问的存储也可能被 (*(struct s2))->x[i] 访问,尽管这两种访问都使用字符类型的左值。