这一部分先介绍大端(big-endian)和小端(little-endian), 然后讨论两种情况下(缺省对齐和#pragma pack指令)的字节码对齐方式。最后用一套面试题讨论结构体嵌套情况下的对齐问题。下一部分将介绍bit fields的对齐方式及在大端和小端组织方式下的数据转换。
1. 大端(big-endian)和小端(little-endian):
一般情况下,big-endian和little-endian针对的数据原子单元是一个字节(byte),因此它们只对多字节数(比如整数,浮点数)有影响,对类似于字符串中char类型是没影响的。一个多字节数的MSB(Most Significant Byte)如果存储在低地址,说明该数据是按big-endian方式组织的。big-endian跟阿拉伯数字的书写顺序类似,比如0x1234的MSB是0x12,LSB是0x34,MSB写在前面(相当于存储器低地址)。如果LSB存储在低地址,说明该数据是按little-endian方式组织的,比如在little-endian的系统中,0x1234在存储器中按从低地址到高地址的存储顺序为0x34-0x12。
x86机器中的字节码是按little-endian组织的,TCP/IP协议中的字节码是按big-endian组织的。
测试程序: Berkeley Socket API中的htonl()或htons()将主机中的little-endian或big-endian字节码转换为TCP/IP协议中的big-endian,如果主机字节顺序已经是big-endian,则htonl是空操作。
/* copyrighted 2011 ljsspace; endian.c */
#include
int bigendian_host(void)
{
union {
char c[4];
unsigned i; //big-endian: 00-00-00-01; little-endian: 01-00-00-00
} u = { .i = 1 };
if(u.c[0] == 0) return 1;
else return 0;
}
int bigendian_net(void)
{
union {
char c[4];
unsigned i;
} u = { .i = htonl(1) }; //big-endian: 00-00-00-01
if(u.c[0] == 0) return 1;
else return 0; //impossible because tcp/ip protocol is big-endian.
}
main() {
int i;
//check host's endianness
unsigned char chs[2] = {0x23,0x45};
unsigned short x = *(short *) chs;
if(x == 0x2345)
printf("host is BIG-ENDIAN\n");
else if(x == 0x4523)
printf("host is LITTLE-ENDIAN\n");
//display v's byte-order by host's endianness
unsigned int v = 0x12345678;
unsigned char *ptr = (char *) &v; //byte pointer
/* host byte order */
if(bigendian_host())
printf("On BIG-ENDIAN host, ");
else
printf("On LITTLE-ENDIAN host, ");
printf("v(0x%X) in bytes(hex): ", v);
for (i=0; i < sizeof(int); i++)
printf("%02X ", ptr[i]);
printf("\n");
//display v's byte-order by network byte order
if(bigendian_net())
printf("As network byte order(BIG-ENDIAN), ");
else
printf("As network byte order(LITTLE-ENDIAN), "); //impossible
printf("v(0x%X) in bytes(hex): ", v);
v = htonl(v);
for (i=0; i < sizeof(int); i++)
printf("%02X ", ptr[i]); //ptr still points to v
printf("\n");
}
输出:
host is LITTLE-ENDIAN
On LITTLE-ENDIAN host, v(0x12345678) in bytes(hex): 78 56 34 12
As network byte order(BIG-ENDIAN), v(0x12345678) in bytes(hex): 12 34 56 78
2. 字节对齐(align):
为了提高数据的访问性能,在struct这样的复合结构中,每个元素都会按一定的字节偏移值对齐,struct结构体本身也有对齐要求:缺省时使用它包含的元素中最宽类型的对齐值(即所有元素中最大对齐值作为结构体本身对齐值),这里假设为r。比如,在缺省情况下,char类型按1字节对齐;int类型按4字节对齐,这种对齐字节值成为该类型的缺省对齐值,这里假设为m;如果一个struct只有char,int两个类型的元素,那么r等于int的对齐值,即r=4。
另外,也可以在struct,union和class前面添加编译器指令#pragma pack(n)-n可以取值1,2,4,8,16-给这些复合结构和其中的元素指定特定的字节紧缩方式,但是pack(n)是否生效,要看元素类型的缺省对齐值m和复合结构整体的对齐值r。复合结构中元素的对齐值=min(n,m);复合结构本身的对齐值=min(n,r)。
测试程序:
/* copyrighted 2011 ljsspace; packing1.c */
#include
#include
const int i = 1; //big-endian: 00-00-00-01; little-endian: 01-00-00-00
#define bigendian() ( (*(char*)&i) == 0 )
struct FinalPad {
double x;
char n[1];
};
struct S {
char i; // 1 byte
unsigned short j; // 2 bytes
char k; // 1 byte
unsigned int m; // 4 bytes
char n; // 1 byte
} s;
#pragma pack(2)
struct T {
char i;
unsigned short j;
char k;
unsigned int m;
char n;
} t;
main() {
int i;
printf ("Size of FinalPad is %d\n",
sizeof (struct FinalPad));
printf ("Size of S is %d\n",
sizeof (struct S));
printf ("Size of T is %d\n",
sizeof (struct T));
//dump FinalPad
struct FinalPad p;
unsigned char *ptr = (char *) &p; // byte pointer
memset(ptr,0,sizeof(p));
p.x = 0;
p.n[0] = 0x34;
if(bigendian())
printf("By BIG-ENDIAN, ");
else
printf("By LITTLE-ENDIAN, ");
printf("FinalPad in bytes(hex): ");
for (i=0; i < sizeof(p); i++)
printf("%02X ", ptr[i]);
printf("\n");
//dump struct S
ptr = (char *) &s; // byte pointer
memset(ptr,0,sizeof(s));
s.i=0x12;
s.j=0x3456;
s.k=0x78;
s.m=0x090A0B0C;
s.n=0xDE;
if(bigendian())
printf("By BIG-ENDIAN, ");
else
printf("By LITTLE-ENDIAN, ");
printf("s in bytes(hex): ");
for (i=0; i < sizeof(s); i++)
printf("%02X ", ptr[i]);
printf("\n");
//dump struct T
ptr = (char *) &t; // byte pointer
memset(ptr,0,sizeof(t));
t.i=0x12;
t.j=0x3456;
t.k=0x78;
t.m=0x090A0B0C;
t.n=0xDE;
if(bigendian())
printf("By BIG-ENDIAN, ");
else
printf("By LITTLE-ENDIAN, ");
printf("t in bytes(hex): ");
for (i=0; i < sizeof(t); i++)
printf("%02X ", ptr[i]);
printf("\n");
}
输出:
Size of FinalPad is 12
Size of S is 16
Size of T is 12
By LITTLE-ENDIAN, FinalPad in bytes(hex): 00 00 00 00 00 00 00 00 34 00 00 00
By LITTLE-ENDIAN, s in bytes(hex): 12 00 56 34 78 00 00 00 0C 0B 0A 09 DE 00 00 00
By LITTLE-ENDIAN, t in bytes(hex): 12 00 56 34 78 00 0C 0B 0A 09 DE 00
通过调整结构体中元素的排列顺序,可以较少padding字节,从而节省空间,同时又保证字节对齐。另外一种节省空间,但是元素类型不按缺省对齐方式的做法是使用#pragma pack(1)。
例如:
/* copyrighted 2011 ljsspace; packing2.c */
#include
#include
const int i = 1; //big-endian: 00-00-00-01; little-endian: 01-00-00-00
#define bigendian() ( (*(char*)&i) == 0 )
struct MixedData
{
char Data1;
short Data2;
int Data3;
char Data4;
};
struct MixedDataPacked1 /* after reordering */
{
char Data1;
char Data4;
short Data2;
int Data3;
};
struct MixedDataPacked2 /* after reordering */
{
int Data3; // 4 bytes
short Data2; // 2 bytes
char Data1; // 1 byte
char Data4; // 1 byte
};
#pragma pack(1)
struct MixedDataPacked3 //forced unalignment
{
char Data1;
short Data2;
int Data3;
char Data4;
};
int main() {
int i;
printf ("Size of MixedData is %d\n",
sizeof (struct MixedData));
printf ("Size of MixedDataPacked1 is %d\n",
sizeof (struct MixedDataPacked1));
printf ("Size of MixedDataPacked2 is %d\n",
sizeof (struct MixedDataPacked2));
printf ("Size of MixedDataPacked3 is %d\n",
sizeof (struct MixedDataPacked3));
struct MixedData s;
unsigned char *ptr = (char *) &s; // byte pointer
memset(ptr,0,sizeof(s));
//dump s
s.Data1=0x56;
s.Data2=0x1234;
s.Data3=0x0708090A;
s.Data4=0xBC;
if(bigendian())
printf("By BIG-ENDIAN, ");
else
printf("By LITTLE-ENDIAN, ");
printf("s in bytes(hex): ");
for (i=0; i < sizeof(s); i++)
printf("%02X ", ptr[i]);
printf("\n");
}
输出:
Size of MixedData is 12
Size of MixedDataPacked1 is 8
Size of MixedDataPacked2 is 8
Size of MixedDataPacked3 is 8
By LITTLE-ENDIAN, s in bytes(hex): 56 00 34 12 0A 09 08 07 BC 00 00 00
在x86 32位系统中,gcc编译器对某些类型(比如double,long long)的缺省对齐值并不等于sizeof值,例如sizeof(double)=8,但是它的缺省对齐值却是4字节。如果使用gcc选项-malign-double可以设置double类型的对齐值等于它的sizeof值(即8字节)。 参考下面的测试long long类型的程序。
测试程序: 由于long long类型(8字节)在c99中才支持,因此以下程序不要用gcc选项-std=c99才能编译通过。而且,虽然sizeof(long long)等于8,但是缺省情况下long long的对齐值等于4字节(即32位系统的一个Word长度),而不是8字节,如果使用gcc选项-malign-double可以设置long long类型的对齐值等于8字节(即32位系统的双Word长度)。
/* copyrighted 2011 ljsspace; packing3.c
Note: in order to support 'long long' type, compile as: "gcc packing4.c -std=c99" */
#include
#include
#include
const int i = 1; //big-endian: 00-00-00-01; little-endian: 01-00-00-00
#define bigendian() ( (*(char*)&i) == 0 )
#pragma pack(8)
typedef struct {
unsigned char a;
unsigned long long b;
} S;
int main() {
int i;
printf ("Size of long long is %d\n\n",
sizeof (long long));
printf ("Size of S is %d\n",
sizeof (S));
S s;
unsigned char *ptr = (char *) &s; // byte pointer
memset(ptr,0,sizeof(s));
//dump s
s.a=0x56;
s.b=LLONG_MAX;
if(bigendian())
printf("By BIG-ENDIAN, ");
else
printf("By LITTLE-ENDIAN, ");
printf("s in bytes(hex): ");
for (i=0; i < sizeof(s); i++)
printf("%02X ", ptr[i]);
printf("\n");
}
测试输出:(使用“gcc packing3.c -std=c99“编译)
Size of long long is 8
Size of S is 12
By LITTLE-ENDIAN, s in bytes(hex): 56 00 00 00 FF FF FF FF FF FF FF 7F
测试输出:(使用“gcc packing3.c -std=c99 -malign-double“编译)
Size of long long is 8
Size of S is 16
By LITTLE-ENDIAN, s in bytes(hex): 56 00 00 00 00 00 00 00 FF FF FF FF FF FF FF 7F
曾经有一道面试题,大意是:有两个结构体s和t,其中一个结构体t的元素d的类型是另一个结构体s,需要计算sizeof(t)和t的内存组织方式。
这种题目需要指定编译器和计算机CPU架构,这里假设在x86 32位系统(Ubuntu OS),编译器是gcc。
这是题目中给定的结构体s和t:
#pragma pack(8)
struct S{
unsigned short a;
unsigned long b;
} s;
struct T {
char c;
S d;
unsigned long long e;
} t;
#pragma pack()
分析: sizeof(short)=2, sizeof(long)=4。因此sizeof(s)=2+2+4=8字节(short后面有2字节padding)。但是结构体s的对齐值r=min(8,4)=4,其中4是最宽类型long的对齐值。结构体s的对齐值r会在结构体t中用到。
在结构体t中,char占用1字节,然后需要填充3字节确保结构S的4字节对齐,接下来S占用8字节,e的前面有12个字节(12=1+3+8),由于缺省时long long使用4字节对齐,而12 mod 4 = 0,因此不需要为e填充字节,e占用8字节。至此总的字节数为12+8=20字节,而结构体t的对齐值=min(8,4)=4,其中4可以认为是S或者e的对齐值,20 mod 4 = 0,所以结构体t不需要填充即可以对齐边界。
如果使用"gcc packing3.c -std=c99 -malign-double",那么S结构体后面需要填充4个字节,这是因为此时long long使用8字节对齐,而目前为止c到S总共占用12个字节(12=1+3+8), 12 mod 8不等于0,因此需要填充4个字节,使得(12+4) mod 8 = 0; 所以总的字节数等于(12+4)+8=24字节。(因为结构体t的对齐值=min(8,8)=8,其中一个8表示t中最宽类型e的对齐值,而24 mod 8=0,所以e后面不需要填充)
测试程序:
/* copyrighted 2011 ljsspace; packing4.c
Note: in order to support 'long long' type, compile as: "gcc packing3.c -std=c99" */
#include
#include
const int i = 1; //big-endian: 00-00-00-01; little-endian: 01-00-00-00
#define bigendian() ( (*(char*)&i) == 0 )
#pragma pack(8)
typedef struct {
unsigned short a;
unsigned long b;
} S;
struct T {
char c;
S d;
unsigned long long e;
} t;
#pragma pack()
int main() {
int i;
printf ("Size of short is %d\n",
sizeof (short));
printf ("Size of long is %d\n",
sizeof (long));
printf ("Size of long long is %d\n\n",
sizeof (long long));
printf ("Size of S is %d\n",
sizeof (S));
printf ("Size of T is %d\n\n",
sizeof (struct T));
S s;
unsigned char *ptr = (char *) &s; // byte pointer
memset(ptr,0,sizeof(s));
//dump s
s.a=0x1234;
s.b=0x56789ABC;
if(bigendian())
printf("By BIG-ENDIAN, ");
else
printf("By LITTLE-ENDIAN, ");
printf("s in bytes(hex): ");
for (i=0; i < sizeof(s); i++)
printf("%02X ", ptr[i]);
printf("\n");
//dump t
ptr = (char *) &t; // byte pointer
memset(ptr,0,sizeof(t));
t.c=0xDE;
t.d=s;
t.e=0x0102030405060708;
if(bigendian())
printf("By BIG-ENDIAN, ");
else
printf("By LITTLE-ENDIAN, ");
printf("t in bytes(hex): ");
for (i=0; i < sizeof(t); i++)
printf("%02X ", ptr[i]);
printf("\n");
}
测试输出:(使用“gcc packing3.c -std=c99“编译)
Size of short is 2
Size of long is 4
Size of long long is 8
Size of S is 8
Size of T is 20
By LITTLE-ENDIAN, s in bytes(hex): 34 12 00 00 BC 9A 78 56
By LITTLE-ENDIAN, t in bytes(hex): DE 00 00 00 34 12 00 00 BC 9A 78 56 08 07 06 05 04 03 02 01
测试输出:(使用“gcc packing3.c -std=c99 -malign-double“编译)
Size of short is 2
Size of long is 4
Size of long long is 8
Size of S is 8
Size of T is 24
By LITTLE-ENDIAN, s in bytes(hex): 34 12 00 00 BC 9A 78 56
By LITTLE-ENDIAN, t in bytes(hex): DE 00 00 00 34 12 00 00 BC 9A 78 56 00 00 00 00 08 07 06 05 04 03 02 01
3.小结:
数据存储和传输离不开big-endian和little-endian的数据组织方式,而出于数据访问性能的考虑,复合结构(比如struct)也离不开字节码对齐的方式,两方面的内容可以完整地确定在指定平台和编译器环境下计算机内存中任何数据类型的字节顺序。