类型转换:对 char 数组结构体成员实现类型操作

3

考虑以下代码:

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];
}

在返回语句中,这会导致别名问题吗?需要注意的是,a.bytes 解引用了一个与 patch 赋值不同别名的类型,但另一方面,[0] 解引用了一个与赋值相同别名的类型。

我可以构造一个稍微大一些的例子,其中gcc -O1 -fstrict-aliasing确实使函数返回0。我想知道这是否是gcc的bug,如果不是,我该如何避免这个问题(在我实际的例子中,赋值发生在一个单独的函数中,因此两个函数在隔离的情况下看起来都很简单)。

以下是一个更长更完整的测试示例:

#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


1
什么是 byte_t?你是不是想说 uint8_t - MightyPork
1
哈哈,这改变了很多。请尝试发布一个包含完整示例和确切的gcc调用的帖子,我仍然无法重现问题。clang和tcc也认为结果是10。 - MightyPork
1
正确,-Os和-O1会导致问题。我建议您编辑问题并提供完整的示例以及这个重要的细节。 - MightyPork
1
在返回语句中,访问命名对象必然使用其定义类型,因此不能存在任何别名违规。在 *(byte_t *)a.bytes 中存在别名违规。 - Eric Postpischil
2
@ErikCarstensen: a.bytes 是一个指向 char 的指针(由于数组的自动转换)。将该 char 作为 char_t 访问,访问的是一种它不是的类型,并且这不是别名规则中列出的任何允许的类型之一。因此,它违反了别名规则。 - Eric Postpischil
显示剩余11条评论
3个回答

2
主要问题在于这两个结构体不是兼容的类型,因此可能会出现各种对齐和填充方面的问题。
除了这个问题之外,标准6.5/7只允许以下内容(“严格别名规则”):
一个对象的存储值只能由具有以下类型之一的lvalue表达式访问: - 与对象的有效类型兼容的类型 - 包含上述类型之一作为其成员的聚合或联合类型
观察*(byte_t *)a.bytes,那么a.bytes的有效类型是char[10]。该数组的每个单独成员依次具有有效类型char。您使用byte_t对其进行引用,但它既不是兼容的结构体类型,也没有char[10]作为其成员之一。但它确实有char。
标准并没有明确如何处理有效类型为数组的对象。如果您严格阅读上面的部分,则您的代码确实违反了严格别名规则,因为您通过没有char[10]成员的结构体访问了char [10]。我还会有些担心编译器是否会填充任一结构体以满足对齐要求。
通常,我建议不要做这样的可疑操作。如果需要类型转换,请使用union。如果要使用原始二进制数据,请使用uint8_t而不是潜在有符号和不可移植的char。

@supercat 虽然 [] 运算符确实只适用于指针,但有效类型的定义表明它与声明类型相同。因此,在执行左值访问的表达式中进行指针衰减不应该有影响,只有用于声明的类型才是重要的。 - Lundin
然后就可以以一致的方式解释标准,这种方式与大多数现有代码兼容(包括许多在-fstrict-aliasing下失败的代码),但仍然允许大多数有用的优化。此外,可以取消“字符类型”异常并允许更多的优化,同时不破坏非人为构建的代码。不幸的是,clang和gcc的后端对某些标准作者可能认为足够明显而不言自明的事情视而不见。 - supercat
你的意思是如果我将类型更改为 "typedef struct { char bytes[10]; byte_t x; } blob_t;",它就可以工作吗?但即使这样,在使用 gcc -O1 -fstrict-aliasing 时仍然会失败。 - Erik Carstensen
除了可移植性和有符号性之外,'char' 还有其他问题吗?在我的实际代码中,我使用无符号字符,并通过静态断言保护它,以确保 sizeof(char)==1。这是因为我想享受字符类型的别名特权。 - Erik Carstensen
@Lundin:标准中没有任何暗示表明通过形式为structLValue.array[index]的lvalue访问是通过其他元素类型进行访问。结构体对象的地址用于形成指针值structLValue.array,但一旦指针被形成,结构体及其类型就不再涉及。 - supercat
显示剩余7条评论

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

虽然如此,您仍可以通过包括对象的有效类型在其成员中的结构来访问lvalue。 - Lundin
@Lundin(算了,我先去读一下你的答案!)我想知道这是否只在实际使用该成员访问对象时才是真的?标准中的措辞有点含糊,但强加这样的限制似乎是合理的。(而且措辞不仅适用于结构体类型,还适用于数组和联合类型。) - Ian Abbott
@IanAbbott 我在那条评论中的意思是,就指针别名而言,如果用于访问的结构体成员中包含了相同的 char 数组,那么通过 struct lvalue 访问数组(字符或非字符)也是可以的。但对齐仍然是一个问题。 - Lundin
@Lundin 您的回答对我来说非常有道理。 - Ian Abbott
@Lundin:如果允许通过不同结构类型中包含的相同字符数组访问字符数组,那么您对 https://godbolt.org/z/8Gvov5 的看法是什么? - supercat

-1
根据标准,语法array[index]*((array)+(index))的简写。因此,p->array[index]等同于*((p->array) + (index)),它使用p的地址来计算p->array的地址,然后不考虑p的类型,添加index(按数组元素类型的大小进行缩放),然后取消引用结果指针以产生数组元素类型的lvalue。标准措辞中没有任何暗示通过所得到的lvalue访问底层结构类型的lvalue。因此,如果结构成员是字符类型的数组,则N1570 6.5p7的约束将允许该形式的lvalue访问任何类型的存储。
然而,一些编译器的维护者,如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的访问都是使用字符类型的lvalue执行的。然而,由gcc生成的代码将不允许(*(struct s1))->x[0]访问的存储也可能被(*(struct s2))->x[i]访问,尽管这两个访问都使用字符类型的lvalue。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接