Memory alignment problem of structure members

结构体成员内存对齐问题

Before discussing memory alignment, let’s first introduce a related concept—offset.

The distance between the actual address of a storage unit and the segment address where it is located is called the intra-segment offset, also known as the “effective address or offset.”

In simple terms, in a structure, the offset refers to the difference between the address of a member in a structure variable and the address of the structure.

Consider the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Define a structure foo with two members
struct Foo{
char flag;
int i;
};
int main(int argc,char* argv[])
{
Foo foo;
foo.flag='T';
// Attempting to get the address of foo.i by using the position of member foo.flag + 1
int *pi=(int*)(&foo.flag+1);
// Assigning a value to the member stored in the address obtained above
*pi=0x01020304;
// Output the values of foo.flag and foo.i
printf("flag=%c,i=%x\n",foo.flag,foo.i);
return 0;
}

In the code above, a structure is defined that includes a char member flag and an int member i. In the main function, an attempt is made to assign the structure’s integer member a value of 0x01020304 by pointer, but the actual output is 0x01, indicating an assignment error. The layout of its class members in the IR code generated by Clang is:

1
%struct.Foo = type { i8, i32 }

Running result:

The problem with the program lies in the pointer assignment, specifically int *pi=(int*)(&foo.flag+1);. The error is in mistakenly thinking that adding 1 to the address of the structure’s char member flag results in the address of the int member i, leading to the expectation that assigning a value to that address would mean i would receive the intended assignment. Unfortunately, the result of the assignment is not as expected. The root cause of this issue is memory byte alignment.

Memory byte alignment refers to the need for various data types to be stored in memory according to certain rules to ensure CPU access efficiency, rather than being stored strictly in a byte-by-byte manner. The default alignment length for each data type depends on the specific implementation of the compiler, which can differ among various compilers. Generally, the alignment length of basic data types corresponds to the size of the data type itself (sizeof value).

For example, the char type occupies one byte, so the alignment length is one byte; the int type occupies 4 bytes, so the alignment length is four bytes; the double type occupies 8 bytes, so its alignment length is 8 bytes.

For structure data members, the default byte alignment generally follows these several principles.

  1. The starting address of the structure variable must be divisible by the size of its widest data type member.
  2. The offset of each member relative to the starting address of the structure is an integer multiple of the size of that member itself. Padding bytes will be added between members if necessary. (0 is considered an integer multiple of any number.)
  3. The total space occupied by the structure variable must be an integer multiple of the size of the widest data type among its members. If necessary, additional bytes will be padded at the end of the last member to ensure the total size of the structure is an integer multiple of the size of the widest data type.
  4. The size of a union member is determined by the size of its largest member.
  5. Given that structure types need to consider byte alignment, the order of member declarations will affect the size of the structure.

In the initial code of this article, the int member i inside the structure foo occupies 4 bytes, making it the member that occupies the most space. Thus, foo must reside at a memory address that is a multiple of 4. The starting address of char member flag is also the starting address of foo, where flag occupies 1 byte. The starting address of the int data member i must, however, be a multiple of 4, which means it cannot simply be placed at &flag+1 (since flag occupies one byte, and its offset is 1, thus flag+1 is no longer a multiple of 4), but must be placed at &flag+4. Therefore, 3 bytes are wasted after flag. Consequently, foo needs a total of 8 bytes of memory space instead of 5 bytes (the sum of sizeof char type and int type).

As shown in the image, the layout of the members of foo in memory is as follows, where each box represents 1 byte.


In the above code, assigning the address of &flag+1 a value of a 4-byte integer 0x01020304 results in a final value of 1 for i, as the 3 bytes in between do not affect the variable i.

Thus, the correct code to assign a value to member i would be:

1
2
3
4
5
6
7
foo.flag='T';
// Instead of using the address of flag + 1 to assign a value to i, directly use the address of foo.i.
int *pi=&foo.i;
// Assign a value to the member stored in the address obtained above
*pi=0x01020304;
// Output the values of foo.flag and foo.i
printf("flag=%c,i=%x\n",foo.flag,foo.i);

The details of byte alignment depend on the specific implementation of the compiler, and can differ across platforms. Some compilers allow changing the default memory alignment conditions in code via the preprocessor directive #pragma pack(n) or type attribute __attribute__((packed)).

Let’s analyze another piece of code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct student{
int year;
double math;
// Union
union hold{
char GPA;
int degit;
char letter;
}hold;
};

int main(void){
sizeof(student);
return 0;
}
// The output result is 24, the compiling platform is windows7x64,GCC5.2.0

The student class generates the following IR code in Clang:

1
2
%struct.student = type { i32, double, %"union.student::hold" }
%"union.student::hold" = type { i32 }
Member Size Offset
(int 4byte)year 4 0
(double 8byte)math 8 8
(union (int)4byte)hold 4 16
Padding 4 bytes 24

The offset of year is 0, the offset of math is equal to sizeof(year)+4byte, which is 8byte, and the offset of hold is the offset of math (8) plus sizeof(math) (8), resulting in 16. The size of the union is determined by the size of its largest member (which is int degit; (4byte)). The size of sizeof(student) is offset of the last member plus its size (16+4=20), but the result is not an integer multiple of the size of all its members, so an additional 4 bytes will be padded after hold to satisfy that requirement. Thus, the sizeof(student) is 24bytes.

Suddenly, I feel that a picture is worth a thousand words…..


The #pragma pack(ALIGN_NUM) preprocessor directive can be used to specify alignment:

1
2
3
4
5
6
// sizeof(A) == 16
struct A{
int ival;
bool b;
double dval;
};

Using custom alignment (1 for no alignment):

1
2
3
4
5
6
7
8
// sizeof(A) == 13
#pragma pack(32)
struct A{
int ival;
bool b;
double dval;
};
#pragma pack()

Since C++11, the alignof keyword has been introduced to obtain the alignment size of a type.

As shown in the following structure:

1
2
3
4
5
6
7
8
9
10
11
12
struct A
{
char c;
int ival;
double dval;
short sival;
};
int main()
{
printf("%d\n", alignof(A));
}
// output: 8
The article is finished. If you have any questions, please comment and communicate.

Scan the QR code on WeChat and follow me.

Title:Memory alignment problem of structure members
Author:LIPENGZHA
Publish Date:2016/06/02 10:12
Update Date:2025/01/14 09:23
Word Count:4.5k Words
Link:https://en.imzlp.com/posts/61962/
License: CC BY-NC-SA 4.0
Reprinting of the full article is prohibited.
Your donation will encourage me to keep creating!