Chinaunix首页 | 论坛 | 博客
  • 博客访问: 253835
  • 博文数量: 60
  • 博客积分: 1222
  • 博客等级: 少尉
  • 技术积分: 585
  • 用 户 组: 普通用户
  • 注册时间: 2011-04-16 17:28
个人简介

从学通信的博士到从事IT行业的工程师 从原华为项目经理,到现任职公司架构师

文章分类

全部博文(60)

文章存档

2013年(18)

2012年(42)

我的朋友

分类: LINUX

2012-12-23 19:54:52

软件和硬件工程师不得不处理字节及比特序问题,这个过程就像走迷宫。尽管最终我们能够走出来,但我们往往筋疲力尽。本文尝试对字节和比特序发挥影响力的领域,包括CPU,总线,设备及网络协议进行总结。我们深入细节期望对这个主题给出清晰的说明。同时,本文尽力从实际应用的角度给出指导和规则。

字节序:大小端问题

我们也许知道字的大小端问题。1980年,Danny Cohen提出了此问题。它描述的是如何在计算机系统中表示多字节整数。

目前存在两种字节序:大字节序和小字节序。大字节序指在低地址存储整数的高字节。小字节序相反。它指在高地址存储高字节。

对某一种计算机系统而言,比特序通常和字节序一致。这就是说,在大字节序系统上,高比特位存储在低比特地址。在小字节序系统上,高比特位存储在高比特地址。

由于比特交换既耗费系统资源又很繁琐,所以我们在设计系统时往往避免在软件中实现比特交换。后面章节将说明如何使用硬件实现比特交换。

就像人们通常由左往右书写数字一样。多字节整数也是从左到右排列。这就是说从高字节到低字节排列。我们将在后面的例子中看到,这是书写整数最清晰的方法。

根据以上规则,我们给出在大、小字节序系统中整数0x0a0b0c0d的表示方式。

对大字节序系统

byte addr       0         1      2        3

bit offset  01234567 01234567 0123456701234567

    binary  00001010 00001011 0000110000001101

        hex    0a       0b     0c        0d

WriteInteger for Little Endian System对小字节序系统

byte addr      3         2      1        0

bit offset  76543210 76543210 7654321076543210

    binary  00001010 00001011 0000110000001101

        hex    0a       0b     0c        0d

上面两种情况,我们可以由左到右读出这个整数:0x0a0b0c0d

如果不遵从上面的规则,我们可能像下面一样写出这个数字。

byte addr      0         1      2        3

bit offset  01234567 01234567 0123456701234567

    binary  10110000 00110000 1101000001010000

很显然,我们很难辨认出我们所写的数字是什么。

本文中使用的简化计算机系统

为了不失通用性,这里给出本文所讨论的计算机系统简化图

由于CPU,本地总线和内部内存/缓存通常具有相同的字节序,所以我们把它们都视作CPU的一部分。在讨论总线大小端问题时,我们指的是外部总线。在本文中我们假定,CPU寄存器宽度,内存字宽度以及总线宽度都是32比特。

CPU大小端问题

CPU大小端是指解释来自于片上寄存器,本地总线,in-line缓存及内存的多字节整数的字节序和比特序

小字节序CPU包括Intel和DEC。大字节序包括Motorola 680x0,Sun Sparc和IBM(例如PowerPC)。而MIPS和ARM两种字节序都可以配置。

CPU字节序影响CPU的指令集。不同字节序的CPU应该使用不同的GNU C工具集编译C源代码。例如,mips-linux-gcc和mipsel-linux-gcc分别用来编译大字节序和小字节序的MIPs代码。

如果我们需要访问多字节整数的不同部分,那么CPU字节序对软件程序也会产生影响。下面的程序说明这种情况。如果只是访问整个32比特整数,那么CPU字节序对软件程序是不可见的。

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" );

Endiannessof Bus 总线字节序

这里我们提到的总线是指上图中的外部总线。我们使用PCI作为例子。我们知道,总线是指在系统上连接CPU,外部设备和其他不同设备的中间媒体。总线的字节序是由总线协议定义的,其他设备必须遵守。

以一个小字节序PCI总线为例。在32个地址/数据总线Line AD[31:0]中,总线需要连接32比特的设备,高比特数据线连接到AD31,低比特数据线连接到AD0。大字节序总线协议则相反。

对于连接到总线的非完整字设备,如8比特设备,小字节序总线(如PCI)要求设备的八个数据线连接到AD[7:0]。而对于大字节序总线协议,它将连接到AD[24:31]

此外,对于PCI总线,协议要求PCI设备实现一个配置空间。这是一组配置寄存器,它们和总线具有相同的字节序。

像所有设备必须遵守总线字节/比特大小端规则一样,CPU也必须如此。如果CPU以不同于总线的大小端方式工作,总线控制器/桥通常需要进行大小端转换。

现在,读者可能要问这样的问题,如果设备的大小端序和总线大小端序不一致会怎么样?在这种情况下,我们需要做一些额外工作才能保证正常通信,下面章节会详细说明。

设备大小端序

Kevin原理1:当多字节数据通过两个不同的大小端系统时,转换必须保证这块数据的内存连续性。

在下面的讨论中,我们假定CPU和总线具有相同的大小端序。如果设备和CPU/总线大小端序相同,那么不需要转换。

当设备和CPU/总线大小端序不同时,我们从硬件的视角提供两个方法。以下讨论假定CPU/总线为小字节序,设备为大字节序。

字一致性

我们交换设备数据线整个32比特字。我们将设备数据线表示为D[0:31],D[0]存储高字节;总线数据线表示为AD[31:0]。这种方法要求D(i)对应AD(31-i),i=0,31.字一致性意味着保持这个字的语义不变。

下图给出大字节序网卡中32比特描述符寄存器内容

进行字一致交换后,在CPU/总线中描述符将表示为下图所示

我们注意到,编码已经自动转为小字节序。不需要软件进行字节或者比特交换。

上面的例子只适用于数据不跨越32比特内存边界的简单情况。下面我们看看vlan[0:24]= 0xabcdef的情况,这时整个编码超过32比特。

进行字一致性置换后,结果如下图所示:

你能看到发生了什么吗?vlan已经被分成了内存不连续的两个字段:bytes[1:0] 和byte[7].它违反Kevin原理1,我们不能定义一个合适的C结构访问不连续的vlan字段。

因此,字一致性方法只适用于字边界内的数据,不适用于可能跨越字边界的数据。第二个方法为我们解决这个问题。

字节一致性

在这种方法中,我们不交换字节,而是交换字节内各个比特(bit[i]对应到总线bit[7-i])。

应用这种方法后,大字节序网卡设备数据变成下图所示的CPU/总线数据值:

现在vlan字段的三个字节在内存上是连续的。每个字节中的内容也是正确的。但是这个结果在字节序上是混乱的。然而,由于现在是占用的连续内存空间,我们可以让软件对这5字节数据做一个字节交换。我们得到下面的结果:

我们看到,软件字节交换在这种方法中作为第二步执行。不像比特交换,字节交换对软件是可承受的。

Kevin原理2:在包含比特字段的C结构中,如果字段A定义在字段B前面,那么字段A总是比字段B占用较低的bit地址。

既然所有数据都可以正确排序,我们可以定义下面的C结构访问网卡中的描述符:

struct nic_tag_reg {

        uint64_t vlan:24 __attribute__((packed));

        uint64_t rx  :6 __attribute__((packed));

        uint64_t tag :10__attribute__((packed));

};

网络协议大小端序

网络协议大小端序定义了网络协议头中整数字段中比特和字节的发送及接收顺序。我们同时介绍一个术语:wire地址。低wire地址比特或者字节总是先于高wire地址比特或者字节发送和接收。

事实上,网络大小端序和我们所见的稍有不同。另外一个因素是:传输线中的比特发送/接收顺序。低层协议,如以太网,拥有自己的规格定义比特发送/接收顺序。有时候,它和上层协议的大小端序是相反的。我们举例解释这种情况。

网卡设备的大小端序通常和所支持的网络协议大小端序相同,所以它可能不同于系统中CPU的大小端序。大多数网络协议是大字节序;这里我们以以太网和IP为例。

以太网大小端序

以太网是大字节序。这意味着整数字段的高字节放置在低wire地址,先于低字节发送/接收。例如,以太头中协议字段为0x0806(ARP)时,其wire排列如下所示

wire byte offset:     0      1

hex            :    08     06

需要注意的是,以太头的MAC地址字段被当做字符串,因此不用考虑字节序。例如,MAC地址12:34:56:78:9a:bc的wire排列如下所示,字节0x12先发送。

比特发送/接收序

比特发送/接收序规定如何发送/接收一个字节内的各个比特。对于以太网,低比特先于高比特发送。显然这是小字段序。字节序保持为大字节序。因此,我们这里看到的就是发送/接收的字节序和比特序相反的情况。

以下解释以太网字节发送/接收序

可以看到,多播bit标记位,也就是说第一个字节的第一个低比特作为wire的第一个比特。以太网和802.3硬件都是这种比特发送/接收序。

这个例子中,协议字节序和比特发送/接收序不同。发送/接收数据时,网卡必须依据主机(CPU)比特序对数据进行转换。这样,高层协议就不必担心比特序,只需要排列字节序。事实上,这是另外一种字节一致性方法,字节的语义在穿越不同大小端序域时得以保留。

一般来讲,比特发送/接收序对CPU和软件是不可见的,但考虑硬件功能如serdes(serializer/deserializer)和网卡数据线和总线wire是很重要的。

在软件中解析以太头

对任何一种大小端序,以太头都可以通过下面的C结构来解析。

struct ethhdr

{

        unsigned char   h_dest[ETH_ALEN];      

        unsigned char   h_source[ETH_ALEN];    

        unsigned short  h_proto;               

};

h_desth_source字段是字节数组,所以不需要转换。h_proto字段是个整数,在主机访问这个字段前,需要使用ntohs()进行转换,主机填充这个字段前,需要htons()进行转换。

IP的大小端序

IP的字节序也是大字节序。IP的比特大小端序和CPU一致,网卡负责将数据转为数据线上的发送/传输序。

对于大字端序主机,IP头各个字段可以直接访问。对于小字端序主机,也就是说世界上大多数PC机(X86),需要软件对IP头各个整数字段进行字节交换。

下面是从Linux内核中的iphdr结构。我们在读取整数字段前和写入整数字段前分别使用ntohs()和htons()进行转换。事实上,对于大字节序主机,这两个函数没有进行任何操作,它们只在小字节序主机上才进行字节交换。

struct iphdr {

#ifdefined(__LITTLE_ENDIAN_BITFIELD)

        __u8   ihl:4,

                version:4;

#elif defined(__BIG_ENDIAN_BITFIELD)

        __u8   version:4,

                ihl:4;

#else

#error  "Please fix"

#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和ihl字段:根据IP标准,version是IP头中第一个字节的4个高比特位,ihl是剩余的4个低比特位。

有两种方法访问这两个字段。方法1是直接从数据获取它们。如果ver_ihl表示IP头的第一个字节,那么(ver_ihl & 0x0f)就是ihl字段,(ver_ihl> > 4)是版本字段。这种方法适用于任意字节序。

方法2是定义如上所示的结构体,并且直接访问结构体中的各个字段。在上面的结构体中,如果主机是小字节序,那么我们先定义ihl后定义version;如果主机是大字节序,我们在ihl之前定义version。如果我们应用Kevin原理2,也就是说先定义的字段总是占用低内存地址,我们发现上面的C结构体和IP标准保持一致。

saddr和daddr字段:这两个字段可以当做字节或者整数数组。如果将它们当做字节数组,那么就没有必要进行大小字节序转换。如果将它们当做整数,那么就需要进行字节序转换。下面是一个解析整数的函数:

/* 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

   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[i] = atoi(pdot);

   if ((pdot = (char *) index(pdot, '.')) == NULL)

        break;         

    ++pdot;

 }

 return *((uint32_t *)ip);

}

总结

字节和比特序问题比我们这里所讨论的更复杂。我希望这篇文章已经覆盖了所有主要方面。下次在迷宫见吧。

Kevin Kaichuan He: 作者是任职于Solustek公司的高级软件工程师。目前(不是2012)的工作内容是board bring-up,嵌入式Linux和网络协议栈项目。之前的工作经验包括作为软件工程师任职于Cisco公司,任职于普渡大学计算机科学系研究助理。他闲暇时间爱好数字摄影,PS2游戏和电影。

原文链接:


阅读(2649) | 评论(0) | 转发(0) |
0

上一篇:没有了

下一篇:ISO C语言新标准(C11)

给主人留下些什么吧!~~