例子举的特别好
很多文章大概都有像这样的结论:
1. 数据项只能存储在地址是数据项大小的整数倍的内存位置上;
2. 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
3. 对齐在N上,也就是说该数据的"存放起始地址%N=0
很明显,如果对数据存放地址的把握错误了的话,那么由此推断出来的地址对齐规则也就全都是错的了,而事实上也是如此。我研究这个课题 80%的时间都是花在这个上面,而真正的对齐规则一个下午应该就可以解决了。
当然,像上面的结论在一般情况下基本上是正确的,也就是说:
char变量的地址 %1 =0;
short变量地址 %2 =0;
int变量的地址 %4 =0;
double变量的地址 %8 =0
这里假设:
sizeof(char) = 1;
sizeof(short) = 2;
sizeof(int)=4;
sizeof(double)=8
这是 win32 平台上的实际值,此篇都以此假设为基础。
当这些变量是处于内存的数据区(或只读数据区)或者是从堆上分配出来的话,应该都是正确的,因为编译器和堆管理可能会帮你把这件事情做得很好,而程序员在代码里面基本上控制不了这些区域的变量的起始地址,实则我的多次实际测试也都符合上面的结论,即变量存放起始地址%N=0,结构体也符合首地址能够被其最宽基本类型成员的大小所整除。
而唯一我们比较好灵活控制的就是栈上数据,也就是局部变量。在 32 位的系统上栈的单位大小是 32 bit,即 4 字节,每一个栈的地址肯定也是 %4 = 0 的,如果一个栈存放了 char / short / int 的话那么他们肯定也满足 % N = 0。我们唯一可以找到破绽的就是使用一个8字节的数据,也就是 double,通过对栈上数据的巧妙安排让 double 变量的地址处于一个可以让 4 整除而不可以让 8 整除的地址上,那么我们的目的就达到了,"存放起始地址 % N = 0 "的结论即可推翻。当然,熟悉栈布局的话这些是可以轻易做到的。具体可以按如下步骤实验。
#include "stdafx.h"
int _tmain(int argc, _TCHAR* argv[])
{
double a = 0;
double b = 0;
double c = 0;
printf("&a=0x%X, &b=0x%X, &c=0x%X", &a, &b, &c);
getchar();
return 0;
}
以下是 VS2013 上的输出
可以看到,变量地址的最后一位是 4,不能被 8 整除(double 为 8 字节),因此已经可以推翻结论1了。
得到的这些地址可能有些随机性,这些地址依赖于具体的环境和编译参数等,但是,无论如何,我们实际看到的东西已经可以推翻上面结论 1 对地址假设的错误的结论了。我们再来看结构体的情况,把他们彻底推翻!为了让内部类型的变量一定可以按照其自身的对齐参数对齐,我们指定对齐参数设为16。
#include "stdafx.h"
#pragma pack(16)
struct A{
char a;
double b;
};
int _tmain(int argc, _TCHAR* argv[]){
struct A a;
struct A b;
struct A c;
printf("&a=0x%X, &b=0x%X, &c=0x%X\n", &a, &b, &c);
printf("&a.b=0x%X, &b.b=0x%X, &c.b=0x%X", &a.b, &b.b, &c.b);
getchar();
return 1;
}
由上面可以看到,结构体 A 的最大长度成员的类型是 double,就是成员变量 b 。 可以看到,结构体变量的起始地址不能被8整除,结构体中的 double 成员的地址也不能被 8 整除,当然这个的原因还是结构体变量的起始地址不能被8整除导致 double 的成员也不能被 8 整除。
我们实际看到的东西已经可以推翻上面结论 2 对地址假设的错误的结论了
尽管在不同的编译环境和系统上会有不同的值,但是从上面的两个实验我们确实得到了不满足那些结论的值,也就是说,无论如何,那些对变量起始地址的假设和结论一定错了~!
我本来以为我对栈布局还比较熟悉,通过多次试验是不是可以完全预测,但是后来证明这些一切都是徒劳,在不同的编译环境下,特别是编译器的优化选项,栈的存放简直就是五花八门,根本预测不到,我们确实不能对变量的地址做过多的假设。
2. 地址的字节对齐规则地址对齐的规则也不是很复杂,只要把对起始地址有假设的那些结论稍微改一下基本上就差不多了。以下我先给出我自己总结的一个版本,然后再慢慢论证与解析。
编译器都有一个指定的对齐参数用于 structure, union, and class 成员,在 win32 平台上的编译器都是默认为 8,这个指定的对齐参数可以在代码里面使用 pack(n) 指令指定,n合法的值是1,2,4,8,16。
每个内部类型自身也都有一个自己的对齐参数,一般来说这个对齐参数就是 sizeof(具体type) 的值,在 win32 平台上就是采用sizeof作为具体类型的自身对齐参数的,也就是讲,char 的自身对齐参数是 1 , short 是 2 , int 是 4, float 也是 4, double 是 8 等。
地址对齐是相对于结构的成员来说的,单个内部类型的变量这种就没什么对齐不对齐的说法了。
结构的成员按照结构中声明的顺序依次排放,对齐的意思是成员相对于结构变量的起始地址的相对对齐,关键是在于相对于结构变量的起始地址的偏移。
有效对齐参数,内部类型的有效对齐是指它的自身对齐参数和指定对齐参数中较小的那个对齐参数;结构类型的有效对齐参数是指它的成员中,有效对齐参数最大的那个值。数组的有效对齐就是它的成员类型的有效对齐。
有了这些就可以得出对齐规则了:
1. (成员的起始地址相对于结构的起始地址的偏移) % (成员的有效对齐) == 0
2. (结构的总大小) % (结构的有效对齐) == 0
3. 如果无法满足对齐规则的话就填充字节直到满足对齐规则
从上面可以看到,如果指定的对齐参数大于了变量的自身对齐参数的话,指定的对齐参数将不起作用,这就是之前为什么要 #pragma pack(16) 的原因了,使得指定对齐参数没用,各个变量按照自己的类型的自身对齐参数对齐。
结构的总大小也要求符合对齐规则,主要是考虑到了结构体数组的情况,数组的各个成员是紧密排列的,不会有空隙,如果结构总大小满足对齐要求的话那么整个数组就自然满足对齐要求了,如果总大小不满足对齐要求的话,数组各个成员又要紧密排列,那么这个对齐就又没意义了,CPU 读取这些数组成员还是要花多余的开销。
说了这么多,还是举例子讲话来得实在。
# pragma pack(16)
struct A{
char a;
double b;
};
第一个成员的地址就是结构的起始地址,所以它的地址相对于结构的起始地址的偏移是 0,而 a 是 char 类型,它的自身对齐是 1 小于指定的对齐参数 16,所以 a 的有效对齐是1,a 的起始地址偏移也满足 0 % 1 = 0;第二个成员是 double 类型,其自身对齐参数是 8,也小于指定的对齐参数,所以它的有效对齐是 8,这样我们指定的 # pragma pack(16) 就相当于一点用都没有了。而 double 类型的成员 b 要想满足对齐规则就必须在 a 的后面填充字节以使得 b 的地址相对于结构的起始地址的偏移至少为 8。所以结构 A的内存布局会是这样:
00 CC CC CC CC CCCC CC 00 00 00 00 00 00 00 00
而 sizeof(A) = 16;0 分别表示 a 和 b 的位置,CC就是填充的 7 个字节。
- # pragma pack(16)
- struct A{
- char a;
- short c;
- double b;
- };
同样指定的对齐参数仍然没任何作用,a 还是在偏移为 0 的地址上,c 在 a 之后,c 的有效对齐就是自身对齐 2,位于相对起始地址偏移为 2 的地址上,满足对齐要求 2 % 2 = 0,c 和 a 之间填充了 1 个字节。b 仍然位于偏移地址为 8 的地址上,b 和 c 之间填充了 4 个字节。结构 A 的内存布局如下:
# pragma pack(16)
struct A{
char a;
short c;
double b;
};
- 00 CC 00 00 CC CCCC CC 00 00 00 00 00 00 00 00
而 sizeof(A) = 16;0 分别代表 a,c,b的位置,CC 就是填充字节。
# pragma pack(16)
struct A{
char a;
double b;
short c;
};
指定对齐仍然没用,a 在偏移为 0 的地址上,b 在偏移为 8 的地址上,c 紧紧挨着 b 的屁股,因为此时的地址偏移 16 已经满足 c 的对齐要求 16 % 2 = 0;所以就没必要填充字节了。但是结构体 A 的总大小也要满足对齐规则的第二条,即 (结构的总大小)%(结构的有效对齐) == 0;而结构 A 的有效对齐就是各个成员中有效对齐最大的那个数,也就是 b的对齐参数 8,所以 A 的有效对齐就是 8,结构的总大小要满足对齐要求还必须在 c 后面填充 6 个字节。此时 A 的内存布局如下:
00 CC CC CC CC CCCC CC 00 00 00 00 00 00 00 00 11 11 CC CC CC CC CC CC
而 sizeof(A) = 24;0 分别代表 a,b 的位置,1111 代表 c 的位置。
# pragma pack(4)
struct A{
char a;
double b;
short c;
};
把指定对齐参数设置成 4,此时 a 和 c 的有效对齐仍然是其自身对齐,而 b 因为它的自身对齐 8 大于了指定的对齐 4,所以 b 的有效对齐现在变成了 4 而不再是 8 了。a 仍然位于偏移 0,b 要满足对齐规则的话,地址偏移必须是其有效对齐的整数倍,所以 b 的偏移应该是 4,c 仍然紧紧跟在 b 的后面,因为此时的偏移 12 满足了 c 的对齐要求12 % 2 = 0;结构 A 的有效对齐现在也变成了 4,即等于成员中最大的有效对齐,b 的有效对齐。A 的总大小要满足对齐规则的话还必须在 c 的后面填充 2 个字节,让总大小变为 16 字节。此时 A 的内存布局如下:
00 CC CC CC 00 0000 00 00 00 00 00 11 11 CC CC
而 sizeof(A) = 16;0 分别代表 a,b 的位置,1111 代表c的位置。
# pragma pack(8)
struct A{
char a;
double b;
};
struct B{
int i;
struct A sa;
int c;
};
我们来看结构 B 的布局,把指定对齐参数设置成 8,其实也还是没起作用,我们最大的内部类型就是 8 的 double 了,刚好和指定的对齐参数相等。第一个成员 i 肯定是位于偏移为 0 的地址上了。然后第二个成员是一个结构成员,我们要找到这个成员的有效对齐参数,结构的有效对齐参数是其成员中最大的那个对齐参数,对于结构 A 来说就是 b 的对齐参数 8,所以 A 的有效对齐是 8。结构成员 sa 要想满足对齐要求,即 偏移 % 有效对齐 8 = 0;它的地址偏移应该为 8。所以 sa 和 i 之间需要填充 4 个字节。成员 c 仍然紧紧跟在 sa 后面,因为 sa 占 16 字节,此时的地址偏移 24 已经可以满足 c 的对齐要求 24 % 4 = 0 ;而结构 B 的总大小也要满足对齐规则,B 的有效对齐就是成员中最大的,sa的有效对齐 8。所以 B 的总大小要能被 8 整除,就必须在 c 的后面再填充 4 个字节。此时结构 B 的内存布局如下:
00 00 00 00 CC CCCC CC 00 CC CC CC CC CC CC CC 00 00 00 00 00 00 00 00 11 11 11 11 CC CC CC CC
而 sizeof(A) = 32;0 分别代表 i,sa.a,sa.b 的位置,11111111代表 c 的位置。
# pragma pack(8)
struct A{
char a;
double b;
};
struct B{
int i;
int c;
struct A sa;
};
把 c 移到 sa 的上面,这样就不需要填充任何字节了。B的所有的成员刚好满足对齐规则。注意,结构 A 中的 b 和 a 之间还是要填充字节的,它内部要满足自己的对齐要求。此时 B 的内存布局如下:
00 00 00 00 11 1111 11 00 CC CC CC CC CC CC CC 00 00 00 00 00 00 00 00
而 sizeof(A) = 24;0分别代表 i ,sa.a,sa.b的位置,11111111 代表 c 的位置。
# pragma pack(4)
struct A{
char a[9];
double b;
char c[29];
int d[7];
};
数组成员 a 的有效对齐是和其成员类型 char 一样,1。成员 b 的有效对齐是指定的对齐参数 4,因为指定的比它自身的小。c 数组同样也是 1 字节对齐,数组 d 是 4 字节对齐,指定的对齐和它自身的对齐一样,都是 4。数组的各个成员是紧密排列的,所以,b 和 a 之间填充了 3 个字节,c 和 b之间不填充字节,d 和 c 之间填充3个字节,c 之后不填充字节。sizeof(A) = 80;