大小端和内存对齐大小端和内存对齐
很多的博文以及论坛曾经探讨过关于大小端定义,内存地址对齐的问题。最近刚刚完成了一个关于COS(chip operating system )平台移植的项目,多处遇到此类问题,对此也有了全新的了解,阅读了一些前辈的文章,受益匪浅。自己水平有限,不敢写文论述,这里整理两位前辈的文章,希望对大家有所帮助。
文一 大小端和存储器对齐
作者:dycxin
我们常常看到“alignment", "endian"之类的字眼, 但很少有C语言教材提到这些概念。 实际上它们是与处理器与内存接口, 编译器类型密切相关的。...
大小端和内存对齐
很多的博文以及论坛曾经探讨过关于大小端定义,内存地址对齐的问题。最近刚刚完成了一个关于COS(chip operating system )平台移植的项目,多处遇到此类问题,对此也有了全新的了解,阅读了一些前辈的文章,受益匪浅。自己水平有限,不敢写文论述,这里整理两位前辈的文章,希望对大家有所帮助。
文一 大小端和存储器对齐
作者:dycxin
我们常常看到“alignment", "endian"之类的字眼, 但很少有C语言教材提到这些概念。 实际上它们是与处理器与内存接口, 编译器类型密切相关的。考虑这样一个例子: 两个异构的CPU进行通信, 定义了这样一个结果来传递消息:
struct Message
{
short opcode;
char subfield;
long message_length;
char version;
short destination_processor; }message;
用这样一个结构来传递消息貌似非常方便, 但也引发了这样一个问题: 若这两种不同的CPU对该结构的定义不一样, 两者就会对消息有不同的理解。 有可能导致二义性。 会引发二义性的有这两个方面:
1.内存地址对齐
2.大小端定义
本文先介绍内存地址对齐和大小端的概念, 再回头来看这个例子就豁然开朗了。 内存地址对齐
洋名叫做" Byte Alignment"。
大部分16位和32位的CPU不允许将字或者长字存储到内存中的任意地址。 比如Motorola 68000不允许将16位的字存储到奇数地址中, 将一个16位的字写到奇数地址将引发异常。
实际上, 对于c中的字节组织,有这样的对齐规则:
1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
2) 结构体每个成员相对于结构首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);
3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing padding)。
不同CPU的对其规则可能不同, 请参考手册。
为什么会有上述的限制呢? 理解了内存组织, 就会清楚了
CPU通过地址总线来存取内存中的数据,32位的CPU的地址总线宽度既为32位置, 标为A[0:31]。在一个总线周期内,CPU从内存读/写32位。 但是CPU只能在能够被4整除的地址进行内存访问,这是因为: 32位CPU不使用地址总线的A1和A2(比如ARM,它的A[0:1]用于字节选择, 用于逻辑控制, 而不和存储器相连, 存储器连接到A[2:31])。访问内存的最小单位是字节(byte), A0和A1不使用, 那么对于地址来说, 最低两位是无效的, 所以它只能识别能被4整除的地址了。 在4字节中,通过A0和A1确定某一个字节。
再看看刚才的message结构, 你想想它占了多少字节? 别想当然的以为是10个字节。 实际上它占了12个字节。 不信?
用sizeof(message)看吧。 对于结构体, 编译器会针对起中的元素添加"pad"以满足字节对齐规则。
message会被编译器改为下面的形式:
struct Message
{
short opcode;
char subfield;
char pad1; // Pad to start the long word at a 4 byte boundary
long message_length;
char version;
char pad2; // Pad to start a short at a 2 byte boundary
short destination_processor;
char pad3[4]; // Pad to align the complete structure to a 16
// byte boundary };
如果不同的编译器采用不同的对齐规则, 对传递message可就麻烦了。
大端(Big Endian)与小端(Little Endian)
Byte Endian是指字节在内存中的组织,所以也称它为Byte Ordering。
对于数据中跨越多个字节的对象, 我们必须为它建立这样的约定:
(1) 它的地址是多少?
(2) 它的字节在内存中是如何组织的?
针对第一个问题,有这样的解释:
对于跨越多个字节的对象,一般它所占的字节都是连续的, 它的地址等于它所占字节最低地址。(链
可能是个例外, 但链表的地址可看作链表头的地址)。
比如: int x, 它的地址为0x100。 那么它占据了内存中的Ox100, 0x101, 0x102, 0x103这四个字节。
上面只是内存字节组织的一种情况: 多字节对象在内存中的组织有一般有两种约定。 考虑一个W位的整数。 它的各位表达如下:
[Xw-1, Xw-2, ... , X1, X0]
它的MSB (Most Significant Byte, 最高有效字节)为[Xw-1, Xw-2, ... Xw-8]; LSB (Least
Significant Byte, 最低有效字节)为 [X7,X6,..., X0]。 其余的字节位于MSB, LSB之间。
LSB和MSB谁位于内存的最低地址, 即谁代表该对象的地址? 这就引出了大端(Big Endian)与小端(Little Endian)的问题。
如果LSB在MSB前面, 既LSB是低地址, 则该机器是小端; 反之则是大端。 DEC (Digital Equipment Corporation, 现在是Compaq公司的一部分)和Intel的机器一般采用小端。 IBM, Motorola, Sun的机器一般采用大端。 当然, 这不代表所有情况。 有的CPU即能工作于小端, 又能工作于大端, 比如ARM, PowerPC, Alpha。 具体情形参考处理器手册。
举个例子来说名大小端: 比如一个int x, 地址为0x100, 它的值为0x1234567。 则它所占据的0x100, 0x101, 0x102, 0x103地址组织如下图:
0x01234567的MSB为0x01, LSB为0x67。 0x01在低地址(或理解为"MSB出现在LSB前面,因为这里讨论的地址都是递增的), 则为大端; 0x67在低地址则为小端。
认清这样一个事实: C中的数据类型都是从内存的低地址向高地址扩展,取址运算"&"都是取
低地址。
两个测试Bit Endian的小程序:
method_1
,i nclude
int main(int argc, char *argv[])
{
int c = 1;
if ((*(char *)&c) == 1)
{
printf("little endian\n");
}
else
printf("big endian");
return 0;
}
int c 在内存中的表达为: 0x00000001。 (这里假设int为4字节)。 用char可以截取一个字节。 LSB为0x01, 若它出现在c的低地址, 则为小端。
method_2
,i nclude
int main(void)
{
/* Each component to a union type is allocated storage at the
beginning of the union */
union
{
short n;
char c[sizeof(short)];
}un;
un.n = 0x0102;
if ((un.c[0] == 1 && un.c[1] == 2))
printf("big endian\n");
else if ((un.c[0] == 2 && un.c[1] == 1))
printf("little endian\n");
else
printf("error!\n");
return 0;
}
union中元素的起始地址都是相同的——位于联合的开始。 用char来截取感兴趣的字节。
区分大端与小端有什么用呢? 如果两个不同Endian的机器进行通信时, 就有必要区分了。
文二 大小端问题
计算机从诞生时起就确定一个一个字节(byte)的大小是8bit,从此以后就没用变动过。一个机器的字长通常包含数个byte,在存储 数据的上出现了大端点(big endian)和小端点(little endian)两种结构,前者如PowerPC和Sun Sparc,后者如Intel x86系列。大端点机使用机器字长的高字节存储数字逻辑编码的低字节,字节的屋里顺序和逻辑顺序相反;小端点机使用机器字长的高字节存储数字逻辑编码的高 字节,字节的屋里顺序和逻辑顺序相同。TCP/IP等传输使用的都是大端点序。大端点机和小端点机实际上各有优点,《深入理解计算机系统》中写道:
由于Little Endian提供了逻辑顺序与物理顺序的一致性,让编程者摆脱了不一致性所带来的困扰,C语言开发者可以无所顾忌的按照自己的意愿进行强制类型转换,所以 现代体系结构几乎都支持Little Endian。但Big Endian也有其优点,尤其对于汇编程序员:他们对于任意长度的整数,总是可以通过判断Byte 0的bit-7来查看一个整数的正负;对于Little Endian则不得不首先知道当前整数的长度,然后查看最高byte的bit-7来判断其正负。对于这种情况,big endian的开发者可以写出非常高效的代码。
两派的支持者争论不休,正像他们所支持名词(big endian和little endian)的典故所讲述的那样:Little Endian和Big Endian这两个名词来源于Jonathan Swift的《格利佛游记》其中交战的两个派别无法就应该从哪一端,,小端还是大端,,打开一个半熟的鸡蛋达成一致。:) 在那个时代,Swift是在讽刺英国和法国之间的持续冲突,Danny Cohen,一位网络协议的早期开创者,第一次使用这两个术语来指代字节顺序,后来这个术语被广泛接纳了。 然而这个差别却给跨平台的程序编写和不同平台主机间的通信带来了相当的困扰。在c/c++中使用强制类型转换,如int到char数组的转换在有些 时候可以写出简洁高效的程序,但字节序的不
跨平台的程序,以及处理网络数据传输和文件数据转换年同确实这种写法有些时候变得很困难,
的时候,必须要考虑字 节序不同的问题。其实在C++中检测和转换字节序并不困难,写出的程序可以多种多样,基本的思想却是相同的。
下面这段代码可以用来测试一下你的编译器是大端模式还是小端模式:
short int x;
char x0,x1;
x=0x1122;
x0=((char*)&x)[0]; //低地址单元
x1=((char*)&x)[1]; //高地址单元
若x0=0x11,则是大端;若x0=0x22,则是小端......
上面的程序还可以看出,数据寻址时,用的是低位字节的地址。
也可以用下面的程序测试:
。 若处理器是Big_endian的,则返回0;若是Little_endian的,则返回1
u8 checkCPU( )
{
{
union w
{
u8 a;
u16 b;
} c;
c.a=0;
c.b = 1;
return (u8)(c.a!=0?1:0);
}
}
剖析:
嵌入式系统开发者应该对Little-endian和Big-endian模式非常了解。采用Little-endian模式的CPU对操作数的存放方式是从低字节到高字节,而Big-endian模式对操作数的存放方式是从高字节到低字节。例如,16bit宽的数0x1234在Little-endian模式CPU内存中的存放方式(假设从地址0x4000开始存放)为:
内存地址 0x4000 0x4001
存放内容 0x34 0x12
而在Big-endian模式CPU内存中的存放方式则为:
内存地址 0x4000 0x4001
存放内容 0x12 0x34
32bit宽的数0x12345678在Little-endian模式CPU内存中的存放方式(假设从地址0x4000开始存放)为:
内存地址 0x4000 0x4001 0x4002 0x4003
存放内容 0x78 0x56 0x34 0x12
而在Big-endian模式CPU内存中的存放方式则为:
内存地址 0x4000 0x4001 0x4002 0x4003
存放内容 0x12 0x34 0x56 0x78
上面利用联合体union的存放顺序是所有成员都从低地址开始存放的特性,获取CPU对内存采用Little-endian还是Big-endian模式读写。
我用WINAVR GCC编程,利用上面的方法测试了一下发现是小端模式,所以我就不用交换字节啦~
本文档为【大小端和内存对齐】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑,
图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。
本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。
网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。