打包的相同结构是否保证具有相同的内存布局?

Zak*_*akk 2 c struct casting packing language-lawyer

假设我有两个结构:objectwidget

struct object {
    int field;
    void *pointer;
};
Run Code Online (Sandbox Code Playgroud)
struct widget {
    int field;
    void *pointer;
};
Run Code Online (Sandbox Code Playgroud)

还有一个函数:

void consume(struct object *obj)
{
    printf("(%i, %p)\n", obj->field, obj->pointer);
}
Run Code Online (Sandbox Code Playgroud)

我知道如果我尝试这样做:

struct widget wgt = {3, NULL};
consume(&wgt);
Run Code Online (Sandbox Code Playgroud)

我会违反严格的别名规则,从而产生未定义的行为。

据我了解,未定义的行为是由于编译器可能以不同的方式对齐结构字段的事实造成的:也就是说,填充字段以与地址边界对齐(但永远不会更改字段顺序,因为保证遵守该顺序)标准)。

但是如果这两个结构被打包怎么办?它们具有相同的内存布局吗?或者,换句话说,上面的代码是否consume()仍然具有未定义的行为(尽管存在持久的编译器警告)?

注意:我用于struct __attribute__((__packed__)) object { ... };打包(GCC)。

Nat*_*dge 7

它们很可能具有相同的布局;这将成为编译器 ABI 的一部分。

相关架构和/或操作系统可能具有标准 ABI,该 ABI 可能包含也可能不包含packed.但是编译器将有自己的 ABI 以可预测的方式布置它们,尽管算法可能不会在编译器源代码之外的任何地方精确地写下来。

然而,这并不意味着您的代码是安全的。严格的别名规则适用于指向不同类型的指针,无论它们是否具有相同的布局。

这是一个可以使用以下命令进行编译的示例gcc -O2

#include <stdio.h>

__attribute__((packed))
struct object {
    int field;
    void *pointer;
};

__attribute__((packed))
struct widget {
    int field;
    void *pointer;
};

struct widget *some_widget;

__attribute__((noipa)) // prevent inlining which hides the bug
void consume(struct object *obj) 
{
    some_widget->field = 42;
    int val = obj->field;
    printf("%i\n", val);
}

int main(void) {
    struct widget wgt = {3, NULL};
    some_widget = &wgt;
    consume((struct object *)&wgt);
}
Run Code Online (Sandbox Code Playgroud)

尝试一下神箭

您可能希望打印此代码42,因为some_widgetobj都指向wgt,因此应该读取由 编写的val = obj->field相同内容。但实际上它打印了。编译器可以假设并且不使用别名,因为它们具有不同的类型;因此写入和读取被认为是独立的并且可以重新排序。intsome_widget->field = 423objsome_widget

在标准级别上,您正在通过类型为 的左值访问wgt有效类型为的对象。这些类型不兼容,因为它们具有不同的标签 ( vs ),因此行为未定义。struct widget*some_widgetstruct objectwidgetobject


Eri*_*hil 5

\n

\xe2\x80\x9c据我了解,未定义的行为是由于编译器可能以不同方式对齐结构字段\xe2\x80\xa6

\n
\n

不,它(仅)没有。即使两个结构具有相同的成员定义,它们也是不同的类型。考虑两种类型:

\n
struct ComplexNumber  { double real, imag; };\nstruct GeometricPoint { double x, y;       };\n
Run Code Online (Sandbox Code Playgroud)\n

这可能会传递给某个例程:

\n
double foo(ComplexNumber *c, GeometricPoint *p)\n\xe2\x80\xa6\n
Run Code Online (Sandbox Code Playgroud)\n

在函数内部,代码可能会分配一些值*p并使用 的值*c,反之亦然。由于这些是不同且不兼容的类型,因此编译器可以假设它们不是同一内存的别名。这意味着,在优化时,它可以假设为 赋值*p不会改变 的值*c,编译器可能已经在先前使用时将其保存在寄存器中。因此,在分配给*p发生变化的情况下不需要重新加载寄存器*c

\n

因此,别名规则授予编译器对此行为和类似行为的许可,并且意味着,如果违反该规则,则不会定义该行为,即使结构具有相同的布局也是如此。

\n
\n

注意:我用于struct __attribute__((__packed__)) object { ... };打包(GCC)。

\n
\n

打包结构是 GCC 的扩展。由于其扩展规范,您可以预期相同定义的打包结构将具有相同的内存布局。然而,C 标准的别名规则仍然适用。 GCC 有一个开关可以关闭别名规则的要求-fno-strict-aliasing

\n

如果您知道两个对象具有相同的布局,并且希望将其中一个用作另一个而不违反别名规则,则可以通过以下方式执行此操作:

\n
    \n
  • 将一个字节复制到另一个字节中,就像memcpy(p, c, sizeof *p);.
  • \n
  • 定义包含两种类型的联合,用一种类型对其进行初始化,然后访问另一种类型的成员。 (这是由 C 标准定义的,但不是由 C++ 标准定义的。)
  • \n
\n