Sunday, June 28, 2009

A data alignment issue -- example

=== the examples assume a 32bit compilation ===

Understanding what data alignment is and realizing the need for data alignment is a different topic by itself; I'm not going to write about it as there are lots of them around.

Issue: The first member of a struct need not me located at the starting (offset 0) of the struct instance (yes, assuming there are no virtual functions).

Unfortunately, in most cases, this happens to be true; however the point here is that it needn't be. I've personally seen this behavior recently which led me to write this (albeit on a 64bit compiler).

Consider the struct definition,
//
typedef struct _A {
int b;
} A;
//
the sizeof(A) will be 4. This is trivial. Now consider this struct,
//
typedef struct _A2 {
char a;
int b;
} A2;
//
A2 has one char in addition. Some people might expect the sizeof(A2) to be 5 -- but in reality the sizeof(A2) would be 8 due to the data alignment requirement. So where is the extra 3 bytes (called padding) gone? let's examine the offsets of the individual data members to figure out the gap.

Assuming a2 is an instance of A2,

offset of A2::a => (char*) &a2.a - (char*) &a2; // offset of a2.a from the starting of a2 => 0
offset of A2::b => (char*) &a2.b - (char*) &a2; // offset of a2.b from the starting of a2 => 4

Clearly a2.a starts from the zeroth byte, and a2.b starts at the fourth byte. The layout of the struct is as follows {a2.a|*|*|*|a2.b|a2.b|a2.b|a2.b} where * represents the padding bytes and each | represents a byte boundary.

It is important to note that, C/C++ standards do not allow the compiler to change the ordering of the struct's members in its memory representation (please let me know if someone feels this is wrong). However, if you think a while, you would realize that without any change to the ordering of the members, the padding can be moved around while still fulfilling the data alignment requirement.

For eg., the memory layout of A2 could also have been {*|a2.a|*|*|a2.b|a2.b|a2.b|a2.b} where * represents the padding bytes and each | represents a byte boundary. This is perfectly valid and easily invalidates the assumption about the address of first member of the struct -- because the offset of a2.a is now 1 instead of 0.

ok, but why would someone rely on this assumption??! Pbly not directly; it does not make sense to use a2, where a2.a is to be used. However, in nested structures, this might go unnoticed. Consider this scenario,
//
typedef struct _A3 {
char a;
void *ptr; // assume that by design, ptr points to A4 or A5
} A3;

typedef struct _A4 {
char c;
} A4;

typedef struct _A5 {
char c;
int n;
} A5;

void print_members(A3 *pa3)
{
// assume by design:in most cases pa3->ptr points to a A4 instance.
// and given that both A4 and A5 have the common first member
// it might be tempting to write a code like following.
A4* pa4 = (A4*) pa3->ptr;
printf("%c ", (char) pa3->a);
printf("%c ", (char) pa4->c); // here the code is trying to print A4::c or A5::c
if(IS_A5(pa3->ptr))
printf("%d ", (int) ((A5*)pa3->ptr)->n);
}
//
The code at line 23 may or may not work as intended, based on the result of data alignment for the struct A5. This is a perfect disguise of this untrue assumption. So, Beware!!

No comments:

Post a Comment