分类:
2009-05-17 21:14:36
1、 什么是字节对齐问题?
一般来说,计算机按照其字长方式来寻址可以提高运行效率,比如32位(4字节)的X86
结构下,如果每次访问的变量其地址都是4的倍数,则每次对变量的访问只需要一次总线
操作。因此,编译器为了迎合CPU的这一特性,一般在编译的时候都会对变量的存储方式
进行对齐处理。当然X86结构在不满足字节对其的情况也可以运行,只是损失了效率而已
,而有些体系结构,如POWERPC或RISC处理器(如ARM),如果访问非对齐方式的变量,
则会导致error。一个简单的例子:
struct test1{
char a;
int b;
};
sizeof(struct test1)结果是8而不是5 ,这就是字节对齐问题了。
编译器在a和b之间将插入3个填充字节以满足字节对齐。
2、 字节对齐的原则
"成员对齐有一个重要的条件,即每个成员按自己的方式对齐.其对齐的规则是,每个成
员按其类型的对齐参数(通常是这个类型的大小)和指定对齐参数(或者说设置字节对齐时
所允许的最大值.这里默认是gcc为4字节,VC6.0为8字节)中较小的一个对齐.并且结构的
长度必须为所用过的所有对齐参数的整数倍,不够就补空字节."(引用)对于结构体的对齐
方式,ANSI C标准规定结构体类型的对齐要求不能比它所有字段中要求最严格的那个宽
松,可以更严格。
各基本类型的对齐原则是:
char 与字节边界对齐
short(16 位) 与偶数字节边界对齐
int 和 long(32 位) 与 32 位边界对齐
float 与 32 位边界对齐
double 与 64 位边界对齐
structures 任何成员的最大对齐要求
unions 第一个成员的对齐要求
我们来分析一下上面的结构体test1,(编译器为gcc)
(1)成员a本身的方式为1字节,gcc默认的最大值为4 ,取两者之中较小的一个,所以
成员a的对齐方式为1字节;
(2)成员b本身的对齐方式为4字节,gcc默认的最大值为4 ,取两者之中较小的一个,
所以成员b的对齐方式为4字节;
(3)结构体test1的对齐方式的最小值为:所有字段中所用到的对齐方式中的最大值,
即a和b的对齐方式之中的最大值,即4字节对齐,一般结构其就直接取这个值为对齐方式
了;
(4)结构体的长度必须是1和4的公倍数,而根据(1)(2)(3)的对齐方式排列下来
,结构体已经占用了8个字节了 ,是4的倍数,因此结构体大小为8字节。
因此,编译器在分配内存的时候,首先把整个结构体的首地址存放在4字节对齐的位置(
或者说能整除4的地址),成员a的地址与结构体相同 ,成员b的地址必须4字节对齐,因
此,a后面空出3个字节,在偏移4的地方存入成员b。
(5)对于结构体长度的规定,可以这样理解,假设定义了一个结构体
struct test2{
float c;
char d;
}
定义结构体变量struct test2 t2; t2的首地址只需要满足4字节对齐方式即可,假设为0
x0,成员c的地址也为0x0,占用0x0-0x7的内存空间 ,成员d的对齐方式为字节,占用0x8即
可。感觉上好像整个结构体只需要占用9个字节,实际上如果我们定义结构体数组,str
uct test2 t2[10];t2[0]的首地址为4的倍数,假设为0x0,如果整个结构体大小为9字节
,则t2[1]的首地址将是0xA,不满足4字节对齐,因此结构体大小必须是12,以保证定义
数组时也满足对齐原则。
(6)对于结构体内有复杂数据类型成员(如结构体)时,以此类推,如
struct test3{
int c;
char d;
struct test1 t1;
}
成员c的对齐方式是min(4,4)
成员d的对齐方式是min(1,4)
成员t1的对齐方式是min(4,4)
整个结构体的对齐方式是max( min(4,4), min(1,4), min(4,4) )
整个结构体的大小必须是min(4,4), min(1,4), min(4,4)的最接近的公倍数
。
3、 设计结构体
设计结构体时需要充分考虑字节对齐的问题, TCP/IP各报头结构就设计的非常巧妙。举
个简单的例子,如果需要设计一个结构体,有3个成员,两个 short,则应这样设计:
struct test4{
char t41;
char t42;
short t43 ;
short t44 ;
};
这样整个结构体将占用6字节(注意不是8字节)。而如果设计成这样:
struct test5{
char t51;
short t52 ;
char t53;
short t54 ;
};
4、 改变系统默认对齐方式的宏
gcc的默认对齐方式是4,VC6.0的默认对齐方式是8,如果想改变这些值时,可以使用#p
ragma pack(n),如
struct test1{
#pragma pack(1)
char a;
int b;
};
则sizeof(struct test1)结果就是5了。因为b的对齐方式将在其本身的对齐方式(4字节
)和允许的最大对齐方式(或者说默认对齐方式,这里被#pragma pack(1)改为1了)之
中取小的那个,则b的对齐方式是1字节,整个结构体的对齐方式也是1字节。这样,a和
b之间将不会有空余字节,结构体a的首地址也不需要4字节对齐。
5、 一个问题
struct test6{
//#pragma pack(8)
char b1;
int b2;
int b3;
double b4;
char b5;
};
这个结构体大小在gcc下是24,在VC60下是8,地址及偏移都满足前面分析的原则,但是
在gcc下把#pragma pack(8)加上以后,其大小还是24,b4的地址并不是8字节对齐,不知
道为什么了。感觉gcc不能把允许的最大对齐方式(或者说默认对齐方式)改为比4大的
,即使改了也不起作用,不知道能吧能这样理解,而#pragma pack(2)、#pragma pack(
1)都结果符合预期。
6、 计算偏移的宏
#define OFFSET(STRUCT,MEMBER) (&((STRUCT*)0)->MEMBER)
printf ("%d, %d \n", OFFSET(struct test1, a), OFFSET(struct test1, b) );
7、 判断大小端几种方法,第1种和第4种不依赖平台
int a = 0x12345678
假设a的地址为0x0
则我们习惯的小端模式下:
0x0 0x12
0x1 0x34
0x2 0x56
0x3 0x78
而如果大端模式:
0x0 0x78
0x1 0x56
0x2 0x43
0x3 0x21
(1)
int
main ()
{
/* Are we little or big endian? From Harbison&Steele. */
union
{
long l;
char c[sizeof (long)];
} u;
u.l = 1;
exit (u.c[sizeof (long) - 1] == 1);
}
(2)
#include
#include
int
main ()
{
#if BYTE_ORDER != BIG_ENDIAN
not big endian
#endif
;
return 0;
}
(3)
#include
#include
int
main ()
{
#if !BYTE_ORDER || !BIG_ENDIAN || !LITTLE_ENDIAN
bogus endian macros
#endif ;
return 0;
}
(4) int is_little_endian()
{
int i = 1;
char *p = (char*)&i;
if (*p) /* the first (low address) byte is 1 */
return 1; /* little endian */
return 0; /* big endian */
}
8
网络里面所有传输的数据均是大端模式,而通常的X86一般都采用小端模式(其他的一些
体系结构则可以通过跳线设置)。但是有几点稍微需要注意一下:
(1) 大小端转换宏
htons htonl
ntohs ntohl
(2) 大小端问题对单字节数据是无意义的,如:
if (ntohs (p_eth->h_proto) == ETH_P_IP) {
if (p_ip->protocol == IPPROTO_UDP) {
p_ip->protoco是一个unsigned8 类型的,所以不需要转换
(3) 对于传输的数据,如果buffer定义成char型,则抓包可以发现是按照ASCII码编码
的,对于其他类型,也不需要做大小端的处理 ,即数据部分用户是不需要考虑大小端问
题的,如:
int send_buff[10] = {1};
…
ret = sendto (broadcast_fd, send_buff, sizeof (int), 0, (struct sockaddr *)&
to_addr, sizeof (to_addr));
…
则抓包可以发现内部数据部分为01 00 00 00,即仍旧是小端模式。