作者:
Kevin He
,
2003-09-02
原文地址:
http://www.linuxjournal.com/article/6788
译者:
Love. Katherine
,
2007-04-14
译文地址:
http://blog.csdn.net/lovekatherine/archive/2007/04/14/1564731.aspx
转载时务必以超链接形式标明文章原始出处及作者、译者信息。
讨论大端与小端、比特序与节序的区别,以及它们的作用范围
编辑提示:本文自最初发表后已
做过修改
那些不得不和比特序、字节序问题打交道的软件或硬件工程师,都很清楚这过程就像是走迷宫。尽管通常我们都能走出迷宫,但是每次都要牺牲数量可观的脑细胞。本文试图概括需要处理比特序和字节序问题的领域,包括
CPU
、总线、硬件设备以及网络协议。我们将深入问题的细节,并希望能在这个问题上提供有价值的参考。本文同时还试图提供一些从实践中总结出的指导和拇指法则。
大小端
我们对
"endianness"
这个名词估计都很熟悉了。它首先被
Danny Cohen
于
1980
引入,用来表述计算机系统表示多字节整数的方式。
endianness
分为两种:大端和小端。
(
从字节序的角度来看
)
大端方式是将整数中最高位
byte
存放在最低地址中。而小端方式则相反,将整数中的最高位
byte
存放在最高地址中。
对于某个确定的计算机系统,比特序通常与字节序保持一致。换言之,在大端系统中,每个
byte
中最高位
bit
存放在内存最低位;在小端系统中,最低位
bit
存放在内存最低位。
在设计计算机系统时,应该尽一切可能避免通过软件方式执行
bit
换位,因为这样不仅会产生巨大开销,也是件令程序员感到乏味的工作。后文将介绍如何通过硬件方式处理这一问题。
书写规则
正如大部分人是按照从左至右的顺序书写数字,一个多字节整数的内存布局也应该遵循同样的方式,即从左至右为数值的最高位至最低位。正如我们在下面的例子中所看到的,这是书写整数最清晰的方式。
根据上述规则,我们按以下方式分别在大端和小端系统中值为
0x0a0b0c0d
的整数。
在大端系统中书写整数:
byte
addr
0
1
2
3
bit
offset
01234567 01234567 01234567 01234567
binary
00001010 00001011 00001100 00001101
hex
0a
0b
0c
0d
在小端系统中书写整数
byte
addr
3
2
1
0
bit
offset
76543210 76543210 76543210 76543210
binary
00001010 00001011 00001100 00001101
hex
0a
0b
0c
0d
以上两种情形,我们都是按从左至右的顺序读,整数值为
0X0a0b0c0d
假设我们不遵循上述的规则,也许我们会以如下方式书写整数:
byte
addr
0
1
2
3
bit
offset
01234567 01234567 01234567 01234567
binary
10110000 00110000 11010000 01010000
正如你所看到的,这种方式下想要看出我们要表达的整数是件困难的事情。
本文中使用的简化计算机系统
在不失一般性的前提下,在本文中使用下图所描述的简化计算机系统:
CPU
、内部总线和内存
/Cache
这些部件由于通常拥有相同的
endianness
,可以作为一个整体用
CPU
来代表。而对于总线
endianness
讨论,只涉及外部总线。
CPU
寄存器宽度、内存字宽和总线宽度在本文中被设定为
32bits
。
CPU
的
endianness
CPU
的
endianness
是指它在寄存器、内部总线、
Cahce
和内存中表示多字节整数时所采取的字节序和比特序。
小端的
CPU
包括
Intel
和
DEC
。大端
CPU
包括
Motorola 680x0, Sun Sparc and IBM (
如
PowerPC)
。
MIPs and ARM
可以设定为任选其一。
CPU
的
endianness
影响着
CPU
的指令集。对于使用不同
endianness
的
CPU
,应该使用不同的
GNU
工具包来编译代码。例如,
mips-linux-gcc
和
mipsel-linux-gcc
分别用来编译生成运行于大端和小端模式的
MIPS
之上的代码。
如果我们
(
程序员
)
需要访问多字节整数的一部分时,也必须考虑
CPU
的
endianness
。以下的程序展示了该种情形。注意,在访问
32-bit
整数的整体时,
CPU
的
endianness
对于软件
(
程序员
)
是不可见的。
union {
uint32_t my_int;
uint8_t
my_bytes[4];
} endian_tester;
endian_tester et;
et.my_int = 0x0a0b0c0d;
if(et.my_bytes[0] == 0x0a )
printf( "I'm on a big-endian system/n" );
else
printf( "I'm on a little-endian system/n" );
总线的
Endianness
此处我们所谈论的总线是在上图中显示的外部总线。下文以
PCI
总线为例。正如我们所知,总线是联接
CPU
、外设以及其它各种设备的媒介部件。总线的
endianness
是由总线协议定义的、所有联接到其上的部件都必须遵守的比特
/
字节序标准。
以类型为小端的
PCI
总线为例:对于
PCI
的
32
位地址
/
数据线
AD[31:0]
,要求所有联接到
PCI
上的
32-bit
设备将其最高位数据线联接到
AD31
,最低位数据线联接到
AD0
。类型为大端的总线协议则有相反的要求。
对于一个数据宽度不满总线宽度的设备,例如一个
8-bit
设备,小端的总线如
PCI
规定设备的
8
根数据线应联接到
AD
[
7
:
0
],而对于大端的总线协议,则要求联接到
AD
[
24
:
31
]。
此外,对于
PCI
,总线协议要求每个
PCI
设备实现可配置空间——即一组与总线具有相同字节序的可配置寄存器。
正如所有的设备都需要遵守
(
外部
)
总线所规定的比特
/
字节序标准,
CPU
也一样。如果
CPU
与
(
外部
)
总线工作于不同的
endianness
模式,那么总线控制器
/
桥通常是完成转换的部件。
一个机敏的读者现在会提出这样的疑问:“既然如此,如果设备的
endianness
模式与总线的
endianness
模式不匹配,会怎样?“
在这种情况下,必须执行额外的转换工作才能进行信息传递,这将在下一节谈到。
设备的
Endianness
Kevin
定理
#1:
当一个多字节数据单元在两个具有相反
endianness
系统之间传输时,需要执行转换以维护数据单元的内存空间连续性。
我们在下面的讨论中假设
CPU
和总线具备相同的
endianness
。如果设备的
endianness
与
CPU/bus
相同,那么不需要执行转换。
在设备与
CPU/bus
的
endianness
不同的情形下,从硬件接线的角度,我们在此提供两种解决方式。以下的讨论假设
CPU/bus
类型为小端,而设备类型为大端。
字一致方案
在该解决方案中,我们对整个
32-bit
的设备数据线进行变换。我们用
D
[
0
:
31
]表示设备的数据线,其中
D
[
0
]存放最高位,而对于总线用
AD
[
31
:
0
]表示。该方案建议将
D
[
i
]联到
AD
[
31-i
],其中
i=0,...,31
。字一致意味着整个
(32-bit)
字的语义得到了维护。
下图显示的是一个类型为大端的
NIC card
中的
32-bit
描述符寄存器。
在执行字一致交换后
,
在
CPU/bus
上的结果数据为:
注意,转化的结果自动符合
CPU/bus
的字节序和比特序要求,而不需要通过软件
(
程序员
)
进行字节或比特的交换。
上述例子是针对数据并未超过
32-bit
内存边界的简单情形。现在我们看一个穿越边界的例子。在下面的例子中,
vlan[0:24]
的值为
0xabcdef,
并且穿越了
32-bit
内存边界。
在字一致转化后,结果为:
看到这里发生了什么?转换后的
vlan
被分割为两个非连续的内存空间:
bytes[1:0]
和
byte[7]
。这违背了
Kevin
定理
#1,
而且我们无法定义一个结构良好的
C
结构来访问内存空间非连续的
vlan
。
因此,字一致方案只适用于数据位于字边界之内的情形,对于存在边界穿越的数据并不适用。第二种方案可解决该问题。
字节一致方案
在该方案中,我们不执行字节间的变换,但是我们还是要对每个字节中的比特通过硬件绕线进行变换
(
设备中偏移量为
i
的比特转换为
bus
中偏移量为
7-i
的比特,
i=0...7)
。字节一致意味着字节的语义得到了维护。
在应该了该方案后,上图所示大端
NIC
设备中的值转换后的结果为:
现在,
vlan
的三个字节位于连续的内存空间,并且每个字节的内容可以被正确读出。但是转换后的记过在字节序角度看来依然很乱。然而,由于我们现在拥有一块连续的内存空间,可以交给软件来完成图中
5
字节数据交换的任务。最终结果为:
我们看到,在这种解决方案中软件执行的字节交换作为第二阶段。字节交换是由软件完成的,这不同于比特交换。
Kevin
定理
#:2
在
C
中一个包含位域的结构中,如果位域
A
在位域
B
之前定义,那么位域
A
所占据的内存空间永远低于
B
所占用的内存空间。
现在一切都已经分类的井井有条,我们可以定义如下的
C
结构来访问
NIC
中的描述符:
struct nic_tag_reg {
uint64_t vlan:24 __attribute__((packed));
uint64_t rx
:6
__attribute__((packed));
uint64_t tag :10 __attribute__((packed));
};
网络协议的
Endianness
网络协议的
endianness
定义了网络协议头部中整数域发送和传输时所遵循的比特序和字节序。我们在此还要引入一个概念:绕线地址。一个低绕线地址比特或字节在发送和接受时永远位于高绕线地址比特或字节之前。
实际上,对于网络
endianness,
它于我们之前所看到的
endianness
有些许不同。对于网络
endianness
,还存在另外一个影响因素:物理连线上比特的发送和接受顺序。底层协议,例如以太网,对于比特的传输和接受顺序有特定规定,有时这个规定是与上层协议的
endianness
相反的。我们将在下面的例子中考虑这种情形。
NIC
设备的
endianness
通常遵循它们所支持的网络协议所使用的
endianness
类型,因此可能与系统中
CPU
的
endianness
不同。多数网络协议是大端的。此处我们以以太网和
IP
为例。
以太网的
endianness
以太网是大端的。这意味着一个整数域的最高字节存放于低绕线地址,并且在接受和发送时位于最低字节之前。例如,以太网头部值为
0x0806(ARP)
协议域有如下的绕线布局:
wire byte offset:
0
1
hex
:
08
06
注意,以太网头部中
MAC
地址被视为字符串,因此不受字节序的影响。例如,
MAC
地址
12
:
34
:
56
:
78
:
9a
:bc
有如下的绕线布局,并且值为
12
的字节被首先传输。
比特传输
/
接收序
比特传输
/
接受序规定了一个字节内的所有
bit
在物理线路中传输的顺序。对于以太网,顺序是由最不重要
bit(
低绕线地址
)
至最重要
bit(
高绕线地址
)
。这显然属于小端的类型。字节序仍保持为大端,如前所叙。因此,我们看到在这种情况下,字节序和比特传输/接收序是相反的。
下图展示了以太网的比特传输
/
接收序:
我们看到,
MAC
地址第一个字节中的最不重要
bit
,即组
(
多播
)
位,作为第一个
bit
出现在物理线路上。以太网和
802.3
硬件按照上述字节发传输
/
接受顺序一致性的工作。
在协议字节序与比特传输
/
接收序不同的情形下:
NIC
必须在传输时完成由主机
(CPU)
至比特序到以太网比特比特传输序的转换,而在接受时完成由以太网比特接受序至主机
(CPU)
比特序的转换。这样,上层协议就不用担心比特序而只需保证字节序的正确。实际上,这是另一种形式的字节一致转换方案,它保证了数据通过不同
endianness
时字节级语义的完整性。
比特传输
/
接受序通常对于
CPU
和软件是不可见的,但是对于硬件而言是个重要的问题,例如物理层的串并转化,
NIC
的数据线与总线的联接。
基于软件的以太网头部语法分析
对于任何类型的
endianness
,以太网头部可以用下面的
C
结构来完成软件的语法分析:
struct ethhdr
{
unsigned char
h_dest[ETH_ALEN];
unsigned char
h_source[ETH_ALEN];
unsigned short
h_proto;
};
h_dest
和
h_source
域是字节数组,因此不需要转换。
h_proto
域是整数,因此在主机访问该域前需调用
ntohs(),
而在填充该域前需调用
htons()
。
IP
的
endianness
IP
的字节序也为大端。而
IP
的比特序从
CPU
处继承,并由
NIC
负责其与物理传输线路中的比特传输
/
发送序进行转化。
对于大端主机,
IP
头部中的域可以被直接访问。对于小端主机
(
多数为基于
x86
的
PC)
需要对
IP
头部中的整数域进行字节变换才能进行访问和填充。
下面是
Linux Kernel
中定义的
iphdr
结构。我们在读取整数前调用
ntohs()
,在填写整数前调用
htons()
。本质上,这两个函数在大端主机上不执行任何操作,而在小端主机上执行字节变换。
struct iphdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u8
ihl:4,
version:4;
#elif defined (__BIG_ENDIAN_BITFIELD)
__u8
version:4,
ihl:4;
#else
#error
"Please fix <asm/byteorder.h>"
#endif
__u8
tos;
__u16
tot_len;
__u16
id;
__u16
frag_off;
__u8
ttl;
__u8
protocol;
__u16
check;
__u32
saddr;
__u32
daddr;
/*The options start here. */
};
我们来查看
IP
头部中一些有意思的域。
version and ihl
:根据
IP
标准,
IP
头部第一个字节中的最高
4bit
表示
IP
协议的版本。
ihl
表示第一个字节低
4
位
bit
。
有两种方法可以用来访问这些域。方法
1
直接从数据中进行提取。假设
ver_ihl
存放着
IP
头部的第一个字节,那么
(ver_ihl
&
0x0f)
可得到
ihl
域,而
(ver_ihl>>4)
可得到
verion
域。这种方法对于任何一种
endianness
类型都适用。
方法二是定义上述的结构,然后通过结构来访问这些域。在上述结构中,如果主机为小端,那么我们定义
ihl
在
version
之前;如果主机为大端,我们定义
version
在
ihl
之后。如果我们在此应用
Kevin
定理
#2
——
一个先定义的的域永远占据低地址空间,我们可以发现以上的
C
结构定义很好的符合了
IP
标准。
saddr and daddr fields
:这两个域可以被视为整数或字节数组。如果视为字节数组的话,没有必要进行转化。如果被视为整数,那么则需要转化,以下是一个基于整数解释的函数
/*
dot2ip - convert a dotted decimal string into an
*
IP address
*/
uint32_t dot2ip(char *pdot)
{
uint32_t i,my_ip;
my_ip=0;
for (i=0; i<IP_ALEN; ++i) {
my_ip = my_ip*256+atoi(pdot);
if ((pdot = (char *) index(pdot, '.')) == NULL)
break;
++pdot;
}
return my_ip;
}
下面则是基于字节数组的函数:
uint32_t dot2ip2(char *pdot)
{
int i;
uint8_t ip[IP_ALEN];
for (i=0; i<IP_ALEN; ++i) {
ip[i] = atoi(pdot);
if ((pdot = (char *) index(pdot, '.')) == NULL)
break;
++pdot;
}
return *((uint32_t *)ip);
}
分享到:
相关推荐
英文版C++ by dissection, by Ira Pohl,出版社University of California
[AddisonWesley]C++_By_Dissection.zip
the code of Network Dissection from github
4.15.3Bit Fields........ . . 175 Summary......... . . . 177 Review Questions........ 178 Exercises......... . . . 179 5 Ctors, Dtors, Conversions, and Operator Overloading 183 5.1 Classes with ...
A book about C++ for beginner who new to the C++.
C语言解析教程第四版pdf及随书代码, 原书名:C by Dissection The Essentials of C Programming, Fourth Edition
/media/sda7/downloads/Learn/Txt
taxonomies allow a deeper dissection and comparison of the existing solutions. This paper proposes a new, more encompassing and richer multi-level face recognition taxonomy, facilitating the ...
以后如果有好的资源,再上传和大家分享。 本资源是第一本。 这是一本学习android的好书,我浏览了,是老外著的,高清晰的pdf。为了方便大家学习android,我将本书所附的源码一块打包了。方便了不少,如果觉得好就...
c++ 网上应该也有,放到这里看有没有分
Through the dissection of both the code and its design, the authors illustrate how IPv6 and its related protocols have been interpreted and implemented from the specifications. This reference will ...
最近面试了一些人,包括应届本科、硕士和工作多年的程序员,在问到 C 语言相关的问题的时候,总是没几个人能完全答上我的问题。甚至一些工作多年,简历上写着“最得意的语言是 C 语言”“对,C 有很深的研究”“精通 C ...
All of these things lead to student frustration and take away from the joy that comes from discovering the beauty of the human body through touch, observation, and dissection. This laboratory manual ...
Dissection of the potential characteristic of miRNA–miRNA functional synergistic regulations
Mayavi 的参考手册,适合初学者和expert。 User guide: full table of contents An overview of Mayavi Introduction What is Mayavi2? Technical details Using Mayavi as an application, or a library? Scenes...
In endoscopic submucosal dissection (ESD), the narrow gastrointestinal space can cause difficulty in surgical interventions. Tissue ablation apparatuses with high-power CO
可执行文件的剖析可执行文件,共享库和可重定位目标代码的表示通过多种文件格式进行了标准化,这些文件格式提供了汇编指令和数据的封装。 两种这样的格式分别是Windows和Linux分别使用的可移植可执行(PE)文件格式...
我拒绝加厚,并为此和几个出版社僵 持了一年多。我认为经典的东西一定要精炼,不要废话。这次由于面 试别人,所以终于记起了我还写过这么一本小册子。想了想,还是决 定挂到网上免费让大家看得了。并为此专门为本书...
packet-tns.c Routines for Oracle TNS packet dissection
Information technology — Languageindependent arithmetic —Part 1:Integer and floating point arithmetic Technologies de l'information — Arithmétique indépendante de langage —Partie 1: Arithmé...