以下有些内容参考了周卫兄的博客:http://blog.csdn.net/ce123_zhouwei/article/details/6971544 ,在这里对周卫兄表示感谢。
以前在socket编程和笔试中也接触过小端模式和大端模式,那时只知道如何调用API进行大小端转换,并不清楚何为小端模式,何为大端模式。
小端模式(Little-Endian)是指将数据的低有效位(LSB)放在内存的低地址中,数据的高有效位(MSB)存放在内存的高地址中。
大端模式(Big-Endian)是指将数据的低有效位(LSB)放在内存的高地址中,数据的高有效位(MSB)存放在内存的低地址中。
=======================================================================================
首先,要了解数据的高低字节是如何分的(之前我就是搞不清楚数据0x12345678的高字节数据为什么是0x12,低字节数据为什么是0x78,囧了好久)。
如果是十进制数123,就很容易理解:1是高有效位,3是低有效位,因为他可以表现为:100*1+2*10+3*1的格式
同理,0x12345678是十六进制,它可以表示为:1*16^7+2*16^6+...+*16+8*1 。对应的0x12就是高有效位(MSB),0x78是低有效位(LSB)。
其次,要搞清楚为什么要把“将数据的低有效位(LSB)放在内存的低地址中,数据的高有效位(MSB)存放在内存的高地址中。”称为小端模式,而将“将数据的低有效位(LSB)放在内存的高地址中,数据的高有效位(MSB)存放在内存的低地址中”称为大端模式。这得结合大端模式和小端模式的起源来说。
<span style="white-space: pre;"> </span>关于大端小端名词的由来,有一个有趣的故事,来自于Jonathan Swift的《格利佛游记》:Lilliput和Blefuscu这两个强国在过去的36个月中一直在苦战。战争的原因:大家都知道,吃鸡蛋的时候,原始的方法是打破鸡蛋较大的一端,可以那时的皇帝的祖父由于小时侯吃鸡蛋,按这种方法把手指弄破了,因此他的父亲,就下令,命令所有的子民吃鸡蛋的时候,必须先打破鸡蛋较小的一端,违令者重罚。然后老百姓对此法令极为反感,期间发生了多次叛乱,其中一个皇帝因此送命,另一个丢了王位,产生叛乱的原因就是另一个国家Blefuscu的国王大臣煽动起来的,叛乱平息后,就逃到这个帝国避难。据估计,先后几次有11000余人情愿死也不肯去打破鸡蛋较小的端吃鸡蛋。这个其实讽刺当时英国和法国之间持续的冲突。Danny Cohen一位网络协议的开创者,第一次使用这两个术语指代字节顺序,后来就被大家广泛接受。
所以计算机的小端模式就是先把低字节数据存放到内存的低地址中(从小的一端开始打破鸡蛋),大端模式就是先把低字节数据存放到内存的高地址中(从大的一端开始打破鸡蛋)。这下够清晰了吧,这下应该不会被大端、小端、高字节、低字节搞迷糊了。
然后,结合代码来看一下在windows中数据是如何存放的(在Linux中也是一样)。以下是试验代码:
#include<stdio.h>
#include<stdlib.h>
int var;
void main(int agrc, char* argv[])
{
var = 0x12345678;
}
接下来进行反汇编,var = 0x12345678;行对应的反汇编结果为:
6: var = 0x12345678;
012A139E C7 05 54 81 2A 01 78 56 34 12 mov dword ptr ds:[012A8154h],12345678h
左边的序列012A139E表示该语句的存放地址(其实是该语句对应的机器码存放地址);中间的C7 05 54 81 2A 01 78 56 34 12是该语句的机器码;后边的mov dword ptr ds:[012A8154h],12345678h是该语句对应的汇编指令,对于ds:[012A8154h]应了解:ds是段地址,012A8154h是变量var的内存地址。所以,要看数据0x12345678在内存中是如何存放的,只需查看地址012A8154h里的数据即可。
根据截图可见,程序执行后,变量var的值0x12345678在内存中的存储方式是低地址(0x012A8154)存放的是数据低有效位(0x78),高地址(0x012A8158)存放数据的高有效位(0x12)。根据截图可见,程序执行后,变量var的值0x12345678在内存中的存储方式是低地址(0x012A8154)存放的是数据低有效位(0x78),高地址(0x012A8158)存放数据的高有效位(0x12)。可见,windos是小端模式,其实不只windows也是小端模式,而网络传输使用的是大端模式。
还有点要注意,无论是在windows还是Linux中,所有段寄存器都指向一个位置,所以相当于只有一个段。但我们也听说过数据段、代码段,这两者有什么关系呢,这是下篇博客要讨论的。
接下来研究下大小端模式的转换以及如何判断系统是大端模式还是小端模式。转换以及判断代码如下:
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h> //c99引入了bool关键字
bool islittleendian();
bool isbigendian();
void bigtolittle(int* p);
int main(int argc,char** argv)
{
bool mode;
int mode_to_cast = 0x12345678;
/*模式转换*/
bigtolittle(&mode_to_cast);
// mode = islittleendian();
mode = isbigendian();
printf("mode = %d\n",mode);
if(mode == false)
printf("系统工作于大端模式\n");
else
printf("系统工作于小端模式");
return;
}
/*判断系统处于大端模式还是小端模式*/
bool islittleendian()
{ int base = 0x12345678;
char base_0x = (char)base;
/*右半部分解读:取base的内存地址强转为char类型地址,然后取出这个地址的内容*/
if(base_0x==0x78)
return true;
else
return false;
}
/*利用联合体在内存中的存储特性测试
*联合体的成员是共享内存的
*/
bool isbigendian()
{
union{
int base;
char base_0x;
}x;
x.base = 0x12345678;
printf("x.base_0x=%c\n",x.base_0x);
if(x.base_0x==0x12)
return false;
else
return true;
}
/*大小端转换,即将一个数的高低字节换位。
*该函数只能处理int类型数据的大小端转换,其它位数据的处理方式类似
*/
void bigtolittle(int *ptr)
{
char *temp=NULL; //现在才注意到在c里有NULL关键字,而null是不存在的
printf("转换前的低地址:%d\n",*(char *)ptr);
*ptr =(((*ptr)&(0xff000000))>>24)|(((*ptr)&(0x00ff0000))>>8)|(((*ptr)&(0x0000ff00))<<8)|(((*ptr)&(0x000000ff))<<24);
temp = (char *)ptr;
printf("转换后的低地址:%d\n",*temp);
}
该程序在Ubuntu上运行测试通过,gcc 版本 4.6.3 (Ubuntu/Linaro 4.6.3-1ubuntu5),GNU Make 3.81。运行结果如下:
root@vm:/mnt/hgfs/windows_ubuntu_shared/linux_c_practice/hello# big_little_endian
转换前的低地址:120
转换后的低地址:18
x.base_0x=x
mode = 1
系统工作于小端模式