5

在调试问题时,出现了以下问题。(请忽略小代码错误;代码仅用于说明。)

定义了以下结构:

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;

上面没有(似乎)初始化填充字节,填充字节似乎包含“垃圾”(无论在为 b 分配的堆栈空间中是什么)。在大多数情况下,初始化是使用memset(b, 0, sizeof(box_t)).

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

从调试看来,memcpy sizeof(box_t) 等效项总是完成。是否有任何东西(例如,在标准中)实际指定了这一点?随着调试的进行,了解在处理填充字节方面可以依靠什么会很有帮助。

谢谢!(在 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。为什么会有额外的空间?

4

3 回答 3

5

填充字节的值未指定(C99/C11 6.2.6.1 §6):

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

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

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

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

[1]未进行实际测量。


扩展答案:

该标准不保证涉及填充字节。但是,如果您使用静态存储持续时间初始化对象,那么您最终会得到零填充的可能性很高。但是,如果您使用该对象通过赋值来初始化另一个对象,那么所有的赌注都会再次关闭(我希望尾随的填充字节 - 再次,没有进行测量 - 是特别好的候选者,可以从复制中省略)。

使用memset()and memcpy()- 即使分配给单个成员,因为这也会使填充无效 - 是一种在合理实现上保证填充字节值的方法。但是,原则上,编译器可以随时“在你背后”随意更改填充值(这可能与寄存器中的缓存成员有关——再次疯狂猜测),你可以通过使用volatile存储来避免这种情况。

我能想到的唯一合理可移植的解决方法是通过引入适当大小的虚拟成员来明确指定内存布局,同时使用特定于编译器的验证意味着没有引入额外的填充(__attribute__ ((packed))对于-Wpaddedgcc)。

于 2012-06-08T18:58:14.393 回答
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__                                          \
 }

也就是说,数组的大小是通过仅针对其大小声明一个“未标记”联合来确定的。

于 2012-06-08T19:31:38.753 回答
2

正如克里斯托夫所说,没有关于填充的保证。你最好的选择是不要memcmp用来比较两个结构。它在错误的抽象级别上工作。memcmp在表示中按字节工作,而您需要比较成员的值。

最好使用单独的比较函数,该函数采用两个结构并分别比较每个成员。像这样的东西:

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

为了您的奖励,这三个对象是独立的对象,它们不是同一个数组的一部分,并且它们之间的指针算术是不允许的。作为函数局部变量,它们通常分配在堆栈上,并且由于它们是独立的,编译器可以以任何最佳方式对齐它们,例如为了性能。

于 2012-06-08T21:25:11.717 回答