Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When are pad bytes copied - struct assignment, pass by value, other?

While debugging a problem, the following issue came up. (Please ignore minor code errors; the code is just for illustration.)

The following struct is defined:

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

Instances of this struct are being passed by value from function to function (obviously simplified):

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

In some cases, two instances of box_t are compared like this:

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

Within several nested calls, the bytes of the box_t arg were dumped using something like this:

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

The sizeof(box_t) is 8; there are 2 pad bytes (discovered as being after the uint16_t). The dump showed that the fields of the struct were equal, but the pad bytes were not; this caused the memcmp to fail (not surprisingly).

The interesting part has been to discover where the 'corrupted' pad values came from. After tracking backwards it was discovered that some of the box_t instances were declared as local variables and were initialized like this:

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

The above does not (appear to) initialize the pad bytes, which appear to contain 'garbage' (whatever was in the stack space allocated for b). In most cases the initialization was done using memset(b, 0, sizeof(box_t)).

The question is whether initializing an instance of box_t by either (1) struct assignment or (2) passing by value will always do the equivalent of a memcpy of sizeof(box_t). Is it ever the case that only the 6 bytes of the 'real fields' are copied (and the pad bytes are not).

From the debugging it appears that the memcpy sizeof(box_t) equivalent is always done. Is there anything (e.g., in the standard) that actually specifies this? It would be helpful to know what can be counted on regarding the handling of the pad bytes as debugging goes forward.

Thanks! (Using GCC 4.4.3 on Ubuntu LTS 10.4 64-bit)

For bonus points:

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

The 3 instances are allocated 16 bytes apart while sizeof() shows 8. Why the extra space?

like image 545
Art Swri Avatar asked Jun 08 '12 18:06

Art Swri


1 Answers

The value of padding bytes is unspecified (C99/C11 6.2.6.1 §6):

When a value is stored in an object of structure or union type, including in a member object, the bytes of the object representation that correspond to any padding bytes take unspecified values.

See also footnote 42/51 (C99:TC3, C1x draft):

Thus, for example, structure assignment need not copy any padding bits.

The compiler is free to copy or not copy padding as it sees fit. On x86[1], my guess would be that 2 trailing padding bytes will be copied, but 4 bytes won't (which can occur even on 32-bit hardware as structures may require 8-byte alignment, eg to allow atomic reads of double values).

[1] No actual measurements were performed.


To expand on the answer:

The standard doesn't make any guarantees where padding bytes are concerned. However, if you initialize an object with static storage duration, the chance is high that you'll end up with zeroed padding. But if you use that object to initialize another one via assignment, all bets are off again (and I'd expect trailing padding bytes - again, no measurements done - to be particularly good candidates to be omitted from copying).

Using memset() and memcpy() - even when assigning to individual members, as this can invalidate padding as well - is a way to guarantee the values of padding bytes on reasonable implementations. However, in principle the compiler is free to change padding values 'behind your back' any time (which might be related to caching members in registers - wildly guessing again), which you probably can avoid by using volatile storage.

The only reasonably portable workaround I can think of is to specify the memory layout explicitly by introducing dummy members of appropriate size while verifying with compiler-specific means that no additional padding is introduced (__attribute__ ((packed)), -Wpadded for gcc).

like image 155
Christoph Avatar answered Oct 03 '22 21:10

Christoph