四
总结
让我们回到引言部分的问题。
缺省情况下,C/C++编译器默认将结构、栈中的成员数据进行内存对齐。因此,引言程序输出就变成"c1
-> 0, s -> 2, c2 -> 4, i -> 8"。
也正是这个原因,引言例子中sizeof(T_
FOO)为12,而不是8。
总结说来,就是
在结构体中,综合考虑变量本身和指定的对齐值;
在栈上,不考虑变量本身的大小,统一对齐到4字节。
五
附录
5.1
字节序与网络序
5.1.1
字节序
字节序,顾名思义就是字节的高低位存放顺序。
对于单字节,大部分处理器以相同的顺序处理比特位,因此单字节的存放和传输方式一般相同。
对于多字节数据,如整型(32位机中一般占4字节),在不同的处理器的存放方式主要有两种(以内存中0x0A0B0C0D的存放方式为例)。
1)
大字节序(Big-Endian,又称大端序或大尾序)
在计算机中,存储介质以下面方式存储整数0x0A0B0C0D则称为大字节序:
数据以8bit为单位
低地址方向
0x0A
0x0B
0x0C
0x0D
高地址方向
数据以16bit为单位
低地址方向
0x0A0B
0x0C0D
高地址方向
其中,最高有效位(MSB,Most Significant
Byte)0x0A存储在最低的内存地址处。下个字节0x0B存在后面的地址处。同时,最高的16bit单元0x0A0B存储在低位。
简而言之,大字节序就是“高字节存入低地址,低字节存入高地址”。
这里讲个词源典故:“endian”一词来源于乔纳森·斯威夫特的小说《格列佛游记》。小说中,小人国为水煮蛋该从大的一端(Big-End)剥开还是小的一端(Little-End)剥开而争论,争论的双方分别被称为Big-endians和Little-endians。
1980年,Danny
Cohen在其著名的论文"On Holy Wars and a Plea for
Peace"中为平息一场关于字节该以什么样的顺序传送的争论而引用了该词。
借用上面的典故,想象一下要把熟鸡蛋旋转着稳立起来,大头(高字节)肯定在下面(低地址)^_^
2)
小字节序(Little-Endian,又称小端序或小尾序)
在计算机中,存储介质以下面方式存储整数0x0A0B0C0D则称为小字节序:
数据以8bit为单位
高地址方向
0x0A
0x0B
0x0C
0x0D
低地址方向
数据以16bit为单位
高地址方向
0x0A0B
0x0C0D
低地址方向
其中,最低有效位(LSB,Least Significant
Byte)0x0D存储在最低的内存地址处。后面字节依次存在后面的地址处。同时,最低的16bit单元0x0A0B存储在低位。
可见,小字节序就是“高字节存入高地址,低字节存入低地址”。
C语言中的位域结构也要遵循比特序(类似字节序)。例如:
1 structbitfield{2 unsigned char a: 2;3 unsigned char b: 6;4 }
该位域结构占1个字节,假设赋值a =
0x01和b=0x02,则大字节机器上该字节为(01)(000010),小字节机器上该字节为(000010)(01)。因此在编写可移植代码时,需要加条件编译。
注意,在包含位域的C结构中,若位域A在位域B之前定义,则位域A所占用的内存空间地址低于位域B所占用的内存空间。
另见以下联合体,在小字节机器上若low=0x01,high=0x02,则hex=0x21:
1 int main(void){2 3 unsigned charhex;4 struct{5 unsigned char low : 4;6 unsigned char high :4;7 };8 }convert;9 convert.low = 0x01;10 convert.high = 0x02;11 printf("hex = 0x%0x\n", convert.hex);12 return 0;13 }
5.1.2
网络序
网络传输一般采用大字节序,也称为网络字节序或网络序。IP协议中定义大字节序为网络字节序。
对于可移植的代码来说,将接收的网络数据转换成主机的字节序是必须的,一般会有成对的函数用于把网络数据转换成相应的主机字节序或反之(若主机字节序与网络字节序相同,通常将函数定义为空宏)。
伯克利socket
API定义了一组转换
注意:在大小字节序转换时,必须考虑待转换数据的长度(如5.1.1节的数据单元)。另外对于单字符或小于单字符的几个bit数据,是不必转换的,因为在机器存储和网络发送的一个字符内的bit位存储顺序是一致的。
5.1.3 位序
用于描述串行设备的传输顺序。一般硬件传输采用小字节序(先传低位),但I2C协议采用大字节序。网络协议中只有数据链路层的底端会涉及到。
5.1.4 处理器字节序
不同处理器体系的字节序如下所示:
X86、MOS Technology
6502、Z80、VAX、PDP-11等处理器为Little endian;
Motorola 6800、Motorola 68000、PowerPC
970、System/370、SPARC(除V9外)等处理器为Big endian;
ARM、PowerPC (除PowerPC 970外)、DEC
Alpha,SPARC V9,MIPS,PA-RISC and IA64等的字节序是可配置的。
5.1.5 字节序编程
请看下面的语句:
1 printf("%c\n", *((short*)"AB") >> 8);
在大字节序下输出为‘A‘,小字节序下输出为‘B‘。
下面的代码可用来判断本地机器字节序:
1 //字节序枚举类型 2 typedef enum{3 ENDIAN_LITTLE = (INT8U)0X00,4 ENDIAN_BIG = (INT8U)0X01 5}E_ENDIAN_TYPE;6 7 E_ENDIAN_TYPE GetEndianType(VOID)8 {9 INT32U dwData = 0x12345678;10 11if(0x78 == *((INT8U*)&dwData))12 returnENDIAN_LITTLE;13 else 14 returnENDIAN_BIG;15 }16 17//Start of GetEndianTypeTest//18 #include 19 VOID GetEndianTypeTest(VOID)20 {21 #if_BYTE_ORDER == _LITTLE_ENDIAN 22 printf("[%s] Result: %s, EndianType = %s!\n", __FUNCTION__,23 (ENDIAN_LITTLE != GetEndianType()) ? "ERROR" : "OK", "Little");24 #elif_BYTE_ORDER == _BIG_ENDIAN 25 printf("[%s] Result: %s, EndianType = %s!\n", __FUNCTION__,26 (ENDIAN_BIG != GetEndianType()) ? "ERROR" : "OK", "Big");27 #endif 28 }29 //End of GetEndianTypeTest//
在字节序不同的平台间的交换数据时,必须进行转换。比如对于int类型,大字节序写入文件:
1 int i = 100;2 write(fd, &i, sizeof(int));
小字节序读出后:
1 inti;2 read(fd, &i, sizeof(int));3 char buf[sizeof(int)];4 memcpy(buf, &i, sizeof(int));5 for(i = 0; i <sizeof(int); i++)6 {7 int v = buf[sizeof(int) - i - 1];8 buf[sizeof(int) - 1] =buf[i];9 buf[i] =v;10 }11memcpy(&i, buf, sizeof(int));
上面仅仅是个例子。在不同平台间即使不存在字节序的问题,也尽量不要直接传递二进制数据。作为可选的方式就是使用文本来交换数据,这样至少可以避免字节序的问题。
很多的加密算法为了追求速度,都会采取字符串和数字之间的转换,在计算完毕后,必须注意字节序的问题,在某些实现中可以见到使用预编译的方式来完成,这样很不方便,如果使用前面的语句来判断,就可以自动适应。
字节序问题不仅影响异种平台间传递数据,还影响诸如读写一些特殊格式文件之类程序的可移植性。此时使用预编译的方式来完成也是一个好办法。
5.2
对齐时的填充字节
代码如下:
1 structA{2 charc;3 inti;4 shorts;5 };6 int main(void){7 structA a;8 a.c = 1; a.i = 2; a.s = 3;9printf("sizeof(A)=%d\n", sizeof(structA));10 return 0;11 }
执行后输出为sizeof(A)=12。
VC6.0环境中,在main
可见填充字节为0xCC,即int3中断。
5.3
pragma pack语法说明
#pragma pack(n)
#pragma pack(push,
1)
#pragma pack(pop)
1) #pragma
pack(n)
该指令指定结构和联合成员的紧凑对齐。而一个完整的转换单元的结构和联合的紧凑对齐由/ Z
p选项设置。紧凑对齐用pack编译指示在数据说明层设置。该编译指示在其出现后的第一个结构或者联合说明处生效。该编译指示对定义无效。
当使用#pragma
pack (n) 时,n 为1、2、4、8 或1 6
。第一个结构成员后的每个结构成员都被存储在更小的成员类型或n字节界限内。如果使用无参量的#pragma pack,结构成员被紧凑为以/
Z p指定的值。该缺省/ Z p紧凑值为/ Z p 8。
2.
编译器也支持以下增强型语法:
#pragma pack( [ [ { push | pop
} , ] [identifier, ] ] [ n] )
若不同的组件使用pack编译指示指定不同的紧凑对齐,
这个语法允许你把程序组件组合为一个单独的转换单元。
带push参量的pack编译指示的每次出现将当前的紧凑对齐存储到一个内部编译器堆栈中。编译指示的参量表从左到右读取。如果使用push,则当前紧凑值被存储起来;如果给出一个n值,该值将成为新的紧凑值。若指定一个标识符,即选定一个名称,则该标识符将和这个新的的紧凑值联系起来。
带一个pop参量的pack编译指示的每次出现都会检索内部
若使用pop且指定一个
pack编译指示的新的增强功能让你在编写头文件时,确保在遇到该头文件的前后的紧凑值是一样的。
5.4
Intel关于内存对齐的说明
以下内容节选自《Intel Architecture 32 Manual》。
字、双字和四字在自然边界上不需要在内存中对齐。(对于字、双字和四字来说,自然边界分别是偶数地址,可以被4整除的地址,和可以被8整除的地址。)
无论如何,为了提高程序的性能,
一个字或双字操作数跨越了4字节边界,或者一个四字操作数跨越了8字节边界,被认为是未对齐的,从而需要两次总线周期来访问内存。一个字起始地址是奇数但却没有跨越字边界被认为是对齐的,能够在一个总线周期中被访问。
某些操作双四字的指令需要内存操作数在自然边界上对齐。如果操作数没有对齐,这些指令将会产生一个通用保护异常(#GP)。双四字的自然边界是能够被16
整除的地址。其他操作双四字的指令允许未对齐的访问(不会产生通用保护异常),然而,需要额外的内存总线周期来访问内存中未对齐的数据。
5.5
不同架构处理器的对齐要求
RISC指令集处理器(MIPS/ARM):这种处理器的设计以效率为先,要求所访问的多字节数据(short/int/
CISC指令集处理器(X86):没有上述限制。
对齐处理策略
访问非对齐多字节数据时(pack数据),
访问对齐数据时则只用一条指令获取数据,因此对齐数据必须确保其起始地址是在对齐边界上。如果不是在对齐的边界,对X86
CPU是安全的,但对MIPS/ARM这种RISC CPU会出现“总线访问异常”。
为什么X86是安全的呢?
X86
CPU是如何进行数据对齐的。X86 CPU的EFLAGS寄存器中包含一个特殊的位标志,称为AC(对齐检查的英文缩写)标志。按照默认设置,当CPU首次加电时,该标志被设置为0。当该标志是0时,CPU能够自动执行它应该执行的操作,以便成功地访问未对齐的数据值。然而,如果该标志被设置为1,每当系统试图访问未对齐的数据时,CPU就会发出一个INT
17H中断。X86的Windows 2000和Windows 98版本从来不改变这个CPU标志位。因此,当应用程序在X86处理器上运行时,你根本看不到应用程序中出现数据未对齐的异常条件。
为什么MIPS/ARM不安全呢?
因为MIPS/ARM CPU不能自动处理对未对齐数据的访问。当未对齐的数据访问发生时,CPU就会将这一情况通知操作系统。这时,操作系统将会确定它是否应该引发一个数据未对齐异常条件,对vxworks是会触发这个异常的。
5.6
ARM下的对齐处理
有部分摘自ARM
对齐的使用:
1)
__align(num)
用于修改最高级别对象的字节边界。在汇编中使用LDRD或STRD时就要用到此命令__align(8)进行修饰限制。来保证数据对象是相应对齐。
这个修饰对象的命令最大是8个字节限制,可以让2字节的对象进行4字节对齐,但不能让4字节的对象2字节对齐。
__align是存储类修改,只修饰最高级类型对象,不能用于结构或者
2)
__packed
进行一字节对齐。需注意:
不能对packed的对象进行对齐;
所有对象的读写访问都进行非对齐访问;
float及包含float的结构联合及未用__packed的对象将不能字节对齐;
__packed对局部整型变量无影响。
强制由unpacked对象向packed对象转化时未定义。整型指针可以合法定义为packed,如__packed int*
p(__packed int 则没有意义)
对齐或非对齐读写访问可能存在的问题:
1 //定义如下结构,b的起始地址不对齐。在栈中访问b可能有问题,因为栈上数据对齐访问 2 __packed structSTRUCT_TEST{3 chara;4 intb;5 charc;6 };7 //将下面的变量定义成全局静态(不在栈上) 8 static char *p;9static structSTRUCT_TEST a;10 voidMain(){11 __packed int *q; //定义成__packed来修饰当前q指向为非对齐的数据地址下面的访问则可以 12 13 p = (char*)&a;14 q = (int*)(p + 1);15 *q = 0x87654321;16 34 //这样很清楚地看到非对齐访问如何产生错误,以及如何消除非对齐访问带来的问题35//也可看到非对齐访问和对齐访问的指令差异会导致效率问题 36 }
5.7
《The C Book》之位域篇
While we‘re
on the subject of structures, we might as well look at bitfields.
They can only be declared inside a structure or
a
1 struct{2 unsigned field1 :4; //field 4 bits wide 3 unsigned :3; //unnamed 3 bit field(allow for padding)4 signed field2 :1; //one-bit field(can only be 0 or -1 in two‘s complement) 5 unsigned :0; //align next field on a storage unit 6 unsigned field3 :6;7 }full_of_fields;
Each field
is accessed and manipulated as if it were an ordinary member of a
structure. The keywords signed and unsigned mean what you would
expect, except that it is interesting to note that a 1-bit signed
field on a two‘s complement machine can only take the values 0 or
-1. The declarations are permitted to include the const and
volatile qualifiers.
The main
use of bitfields is either to allow tight packing of data or to be
able to specify the fields within some externally produced data
files. C gives no guarantee of the ordering of fields within
machine words, so if you do use them for the latter reason, you
program will not only be non-portable, it will be
compiler-dependent too. The Standard says that fields are packed
into ‘storage units’, which are typically machine words. The
packing order, and whether or not a bitfield may cross a storage
unit boundary, are implementation defined. To force alignment to a
storage unit boundary, a zero width field is used before the one
that you want to have aligned.
Be careful
using them. It can require a surprising amount of run-time code to
manipulate these things and you can end up using more space than
they save.
Bit fields
do not have addresses—you can‘t have pointers to them or arrays of
them.
5.8
C语言字节相关面试题
5.8.1
Intel/微软C语言面试题
请看下面的问题:
1 #pragma pack(8) 2 structs1{3 shorta;4 longb;5 };6 structs2{7 charc;8 s1 d;9 long long e;//VC6.0下可能要用__int64代替双 10 };11 #pragma pack()
问:1.
sizeof(s2) = ? 2. s2的s1中的a后面空了几个字节接着是b?
【分析】
成员对齐有一个重要的条件,即每个成员分别按自己的方式对齐。
也就是说上面虽然指定了按8字节对齐,但并不是所有的成员都是以8字节对齐。其对齐的规则是:每个成员按其类型的对齐参数(通常是这个类型的大小)和指定对齐参数(这里是8字节)中较小的一个对齐,并且结构的长度必须为所用过的所有对齐参数的整数倍,不够就补空字节。
s1中成员a是1字节,默认按1字节对齐,而指定对齐参数为8,两值中取1,即a按1字节对齐;成员b是4个字节,默认按4字节对齐,这时就按4字节对齐,所以sizeof(s1)应该为8;
s2中c和s1中a一样,按1字节对齐。而d
是个8字节结构体,其默认对齐方式就是所有成员使用的对齐参数中最大的一个,s1的就是4。所以,成员d按4字节对齐。成员e是8个字节,默认按8字节对齐,和指定的一样,所以它对到8字节的边界上。这时,已经使用了12个字节,所以又添加4个字节的空,从第16个字节开始放置成员e。此时长度为24,并可被8(成员e按8字节对齐)整除。这样,一共使用了24个字节。
各个变量在内存中的布局为:
c***aa**
bbbb****
dddddddd ——这种“矩阵写法”很方便看出结构体实际大小!
因此,sizeof(S2)结果为24,a后面空了2个字节接着是b。
这里有三点很重要:
1)
每个成员分别按自己的方式对齐,并能最小化长度;
2)
复杂类型(如结构)的默认对齐方式是其最长的成员的对齐方式,这样在成员是复杂类型时可以最小化长度;
3)
对齐后的长度必须是成员中最大对齐参数的整数倍,这样在处理数组时可保证每一项都边界对齐。
还要注意,“空结构体”(不含数据成员)的大小为1,而不是0。试想如果不占空间的话,一个空结构体变量如何取地址、两个不同的空结构体变量又如何得以区分呢?
5.8.2
上海网宿科技面试题
假设硬件平台是intel x86(little endian),以下程序输出什么:
1 //假设硬件平台是intel x86(little endian) 2 typedef unsigned intuint32_t;3 void inet_ntoa(uint32_t in){4 char b[18];5 register char *p;6 p = (char *)∈7 #define UC(b) (((int)b)&0xff) //byte转换为无符号int型 8 sprintf(b, "%d.%d.%d.%d\n", UC(p[0]), UC(p[1]), UC(p[2]), UC(p[3]));9 printf(b);10 }11 intmain(void){12 inet_ntoa(0x12345678);13 inet_ntoa(0x87654321);14 return 0;15 }
先看如下程序:
1 int main(void){2 int a = 0x12345678;3 char *p = (char *)&a;4 char str[20];5sprintf(str,"%d.%d.%d.%d\n", p[0], p[1], p[2], p[3]);6 printf(str);7 return 0;8 }
按照小字节序的规则,变量a在计算机中存储方式为:
高地址方向
0x12
0x34
0x56
0x78
低地址方向
p[3]
p[2]
p[1]
p[0]
注意,p并不是指向0x12345678的开头0x12,而是指向0x78。p[0]到p[1]的操作是&p[0]+1,因此p[1]地址比p[0]地址大。输出结果为120.86.52.18。
反过来的话,令int
a = 0x87654321,则输出结果为33.67.101.-121。
为什么有负值呢?因为系统默认的char是有符号的,本来是0x87也就是135,大于127因此就减去256得到-121。
想要得到正值的话只需将char *p = (char
*)&a改为unsigned char *p = (unsigned char *)&a即可。
综上不难得出,网宿面试题的答案为120.86.52.18和33.67.101.135。