1. 首先建立如下的物理内存概念(独立于字节序)
如下面的图-1所示,内存中有连续的4个字节,左边是低地址,右边是高地址。
我们这里假设4个字节的地址分别是0,1,2,3。
低地址 高地址
|<--------------------------bits-------------------------------->
0 31
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| | | | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
0 1 2 3
|<--------------------------bytes------------------------------->
图-1
接下来,我们要进一步引入bit地址的概念。
我们认为,每个bit也有地址。并且,与字节的地址顺序一样,左边是低地址,右边是高地址。
这样一来,左边是低地址的字节,右边是高地址的字节。
在每一个字节内部呢,左边是低地址的bit,右边是高地址的bit。
好了,现在我们建立了这样的物理内存概念。
注意,这个物理内存概念是独立与字节序的,并且是独立与cpu类型、内存类型的。
也就是说,不管你是什么系统,这个概念都是一样的。
2. 字节序的概念
假设有一个整数,占据了上图中的一片连续的bit位。那么这个整数的数值是多少呢?
这就取决于cpu的字节序了。
大字节序机器:低地址的bit是高有效位,高地址的bit是低有效位
小字节序机器:低地址的bit是低有效位,高地址的bit是高有效位
3. 存储空间的分配(独立于字节序)
typedef struct
{
char a;
char b1:3;
char b2:5;
uint16_t c1:6;
uint16_t c2:10;
} t_my_type;
上述C语言定义的结构体中,各成员所占的内存空间是如何分配的呢?
答案是,按成员定义的顺序,从低地址向高地址依次分配空间。
注意,这种分配方式是独立与cpu类型、内存类型的。
也就是说,不管你是什么系统,分配方式都是如此。
按照上述分配方式,上述结构的各成员在内存中的分布如下面图-2所示:
低地址 高地址
|<--------------------------bits-------------------------------->
0 31
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| a | b1 | b2 | c1 | c2 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
0 1 2 3
|<--------------------------bytes------------------------------->
图-2
按照字节地址从低到高的顺序发送:低地址的字节在前,高地址的字节在后。
如果再细化到bit的话,则同一个字节内:高有效位的bit在前,低有效位的bit在后。
这样一来,对于每一个字节内的bit位的发送顺序,大小字节序的机器就有如下区别了:
大字节序机器:按地址由低到高的顺序发送bit位
小字节序机器:按地址由高到低的顺序发送bit位
5. 数据的网络接收
先到达的字节为低地址字节,后到达的字节为高地址字节。
同样,如果再细化到bit的话,则同一个字节内:高有效位的bit先到达,低有效位的bit后到达。
这样一来,对于每一个字节内的bit位的存储位置,大小字节序的机器就有如下区别了:
大字节序机器:按bit到达顺序从低地址向高地址存储
小字节序机器:按bit到达顺序从高地址向低地址存储
6. 应用
由于gcc工具链自然知道自己是为什么cpu编译代码,因此工具链自身是包含了目标cpu的字节序信息的。
对于程序员而言,只要包含endian.h,然后通过检查__BYTE_ORDER宏的值,就能知道当前是为什么字节序的cpu生成代码。
下面举个小例子,说明主机间通过网络通信时,如何正确的处理字节序问题。
假设两台主机(字节序可能相同,也可能不同),需要通过网络传递一个结构化的信息,信息由a,b,c,d这4个整数构成。
那么,此结构可以定义成如下形式。
#include <stdint.h>
#include <endian.h>
typedef struct
{
uint16_t a;
uint32_t b;
#if __BYTE_ORDER == __BIG_ENDIAN
uint16_t c:3;
uint16_t d:13;
#else if __BYTE_ORDER == __LITTLE_ENDIAN
uint16_t d:13;
uint16_t c:3;
#else
#error can not get byte order info
#endif
} __attribute__((packed)) t_msg;
发送消息时,按如下方式封装此结构(不管封装机器字节序如何)
#include <arpa/inet.h>
t_msg the_msg;
uint16_t *p16=(void *)&the_msg + sizeof(the_msg.a) + sizeof(the_msg.b); //p16指向成员b后面的内容
the_msg.a = htons(1234);
the_msg.b = htonl(1234);
the_msg.c = 5;
the_msg.d = 8;
*p16 = htons(*p16);
//封装完毕,可以发送了
收到消息时,按如下方式取出此结构中的数值(不管封装机器字节序如何)
#include <arpa/inet.h>
t_msg the_msg;
//接收消息到the_msg变量中。
uint16_t *p16=(void *)&the_msg + sizeof(the_msg.a) + sizeof(the_msg.b); //p16指向成员b后面的内容
the_msg.a = ntohs(the_msg.a);
the_msg.b = ntohl(the_msg.b);
*p16 = ntohs(*p16);
//好了,现在the_msg中的成员,都变成主机序了,可以直接使用了
最后,再看看如下Linux源码中tcp头的定义,再结合字节序转换函数的功能作用,对于字节序您是否有所领悟呢:)
struct tcphdr {
__be16 source;
__be16 dest;
__be32 seq;
__be32 ack_seq;
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u16 res1:4,
doff:4,
fin:1,
syn:1,
rst:1,
psh:1,
ack:1,
urg:1,
ece:1,
cwr:1;
#elif defined(__BIG_ENDIAN_BITFIELD)
__u16 doff:4,
res1:4,
cwr:1,
ece:1,
urg:1,
ack:1,
psh:1,
rst:1,
syn:1,
fin:1;
#else
#error "Adjust your <asm/byteorder.h> defines"
#endif
__be16 window;
__sum16 check;
__be16 urg_ptr;
};