何时复制填充字节 - 结构赋值,按值传递,其他?

时间:2012-06-08 18:43:31

标签: c struct padding memory-alignment

在调试问题时,出现了以下问题。 (请忽略次要代码错误;代码仅供参考。)

定义了以下结构:

typedef struct box_t {
  uint32_t x;
  uint16_t y;
} box_t;

这个结构的实例正在通过函数的值传递(显然简化):

void fun_a(box_t b)
{
    ... use b ...
}

void fun_b(box_t bb)
{
    // pass bb by value
    int err = funa(bb);
}

void fun_c(void)
{
    box_t real_b;
    box_t some_b[10];
    ...
    ... use real_b and some_b[]  ...
    ...
    funb(real_b);
    funb(some_b[3]);
    ...
    box_t copy_b = some_b[5];
    ...
}

在某些情况下,会比较两个box_t实例:

 memcmp(bm, bn, sizeof(box_t));

在几个嵌套调用中,box_t arg的字节使用以下内容转储:

char *p = (char*) &a_box_t_arg;
for (i=0; i < sizeof(box_t); i++) {
    printf(" %02X", *p & 0xFF);
    p++;
}
printf("\n");

sizeof(box_t)是8;有2个填充字节(发现在uint16_t之后)。转储显示结构的字段相等,但填充字节不是;这导致memcmp失败(不足为奇)。

有趣的部分是发现'腐败'垫值的来源。在向后跟踪之后,发现一些box_t实例被声明为局部变量并且被初始化为:

box_t b;
b.x = 1;
b.y = 2;

上面没有(似乎)初始化pad字节,它似乎包含'garbage'(无论是在为b分配的堆栈空间中)。在大多数情况下,初始化是使用memset(b, 0, sizeof(box_t))完成的。

问题是,通过(1)结构赋值或(2)传递值初始化box_t的实例是否总是相当于sizeof(box_t)的memcpy。是不是只复制了“真实字段”的6个字节(并且填充字节不是)。

从调试开始,似乎总是完成memcpy sizeof(box_t)等效项。有没有(例如,在标准中)实际指定这个?随着调试的推进,了解有关pad字节处理的内容将会有所帮助。

谢谢! (在Ubuntu LTS 10.4 64位上使用GCC 4.4.3)

奖励积分:

void f(void)
{
    box_t ba;
    box_t bb;
    box_t bc;

3个实例分配16个字节,而sizeof()显示8.为什么需要额外空间?

3 个答案:

答案 0 :(得分:4)

未指定填充字节的值(C99 / C116.2.6.1§6):

  

当值存储在结构或联合类型的对象中时,包括在成员对象中,对应于任何填充字节的对象表示的字节采用未指定的值。

另见脚注42/51(C99:TC3,C1x草案):

  

因此,例如,结构分配不需要复制任何填充位。

编译器可以根据需要自由复制或不复制填充。在x86 [1]上,我的猜测是将复制2个尾随填充字节,但不会复制4个字节(即使在32位硬件上也可能发生,因为结构可能需要8字节对齐,例如允许原子读取double值。)

[1] 未进行实际测量。


扩展答案:

标准不会使任何保证填充字节的位置。但是,如果使用静态存储持续时间初始化对象,则最终会出现归零填充的可能性很高。但是如果你使用那个对象通过赋值来初始化另一个对象,那么所有的赌注都会再次关闭(我希望尾随填充字节 - 再次,没有完成测量 - 特别适合从复制中删除)。

使用memset()memcpy() - 即使在分配给各个成员时,因为这也可以使填充无效 - 这是一种在合理的实现上保证填充字节值的方法。但是,原则上编译器可以随时更改“背后”的填充值(可能与寄存器中的缓存成员相关 - 再次疯狂猜测),您可以通过使用{ {1}}存储。

我能想到的合理可移植的解决方法是通过引入适当大小的虚拟成员来明确指定内存布局,同时使用特定于编译器的方式验证不会引入额外的填充({{1对于gcc而言,volatile

答案 1 :(得分:3)

C11将允许您定义匿名结构和联合成员:

typedef union box_t {
  unsigned char allBytes[theSizeOfIt];
  struct {
    uint32_t x;
    uint16_t y;
  };
} box_t;

该联合的行为与以前几乎相同,您可以访问.x等,但默认的初始化和分配会发生变化。如果您始终确保正确初始化变量,请执行以下操作:

box_t real_b = { 0 };

或者像这样

box_t real_a = { .allBytes = {0}, .x = 1, .y = 2 };

所有填充字节应正确初始化为0。如果您的整数类型具有填充位,这将无济于事,但至少您选择的uintXX_t类型根据定义将不具有它们。

gcc和粉丝已经将其作为扩展实现,即使它们尚未完全是C11。

修改:在P99中,有一个宏以一致的方式执行此操作:

#define P99_DEFINE_UNION(NAME, ...)                     \
 union NAME {                                           \
   uint8_t p00_allbytes[sizeof(union { __VA_ARGS__ })]; \
   __VA_ARGS__                                          \
 }

这就是数组的大小是通过声明一个“未标记”的联合来确定的。

答案 2 :(得分:2)

正如克里斯托夫所说,对填充没有任何保证。最好的办法是不要使用memcmp来比较两个结构。它工作在错误的抽象级别。 memcmp在表示中按字节顺序工作,而您需要比较成员的值。

最好使用一个单独的比较函数,它接受两个结构并分别比较每个成员。像这样:

int box_isequal (box_t bm, box_t bn)
{
    return (bm.x == bn.x) && (bm.y == bn.y);
}

对于你的奖励,这三个对象是单独的对象,它们不是同一个数组的一部分,并且不允许它们之间的指针算术。作为函数局部变量,它们通常在堆栈上分配,并且因为它们是分开的,所以编译器可以以最佳方式对齐它们,例如,表现。