字节与比特序

2023-05-16

软件和硬件工程师不得不处理字节及比特序问题,这个过程就像走迷宫。尽管最终我们能够走出来,但我们往往筋疲力尽。本文尝试对字节和比特序发挥影响力的领域,包括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<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和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字段:这两个字段可以当做字节或者整数数组。如果将它们当做字节数组,那么就没有必要进行大小字节序转换。如果将它们当做整数,那么就需要进行字节序转换。下面是一个解析整数的函数:


下面是对字节数组进行解析的函数:

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

}

总结

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

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

 

原文链接:http://www.linuxjournal.com/article/6788


转载于:https://www.cnblogs.com/hanhuilee/archive/2012/01/08/5221435.html

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

字节与比特序 的相关文章

  • 怎么增加照片的KB大小

    之前都是要想办法压缩图片的大小 今天有人发来一张1 8MB的图片让我帮忙调到30MB左右 一下子放大这么多着实有点茫然 之后想到了一个办法 首先把图片占用体积变大 xff0c 是不会增加清晰度的 xff0c 而减小占用体积却会降低清晰度 第
  • 参数 返回值

    1 函数 函数是对功能的封装 语法 def 函数名 形参列表 函数体 代码块 return 调用 函数名 实参列表 2 返回值 return 在函数执行的时候 如果遇到return 直接返回 1 如果函数什么都不写 不写return 没有返
  • 抽象类调用自己的抽象方法,实现来自实现类(很常用)

    直接上代码 public abstract class Parent public abstract void dosomething public void say dosomething System out println 34 ww
  • 学习笔记:51单片机(STC89C52)如何定时10ms

    1 定时器如何定时 首先大致描述一下定时器的定时原理 xff0c 其实本质就一句话 xff1a 每经过一个机器周期 xff0c 寄存器就加1 这里就又要解释什么是时钟周期 xff0c 什么是机械周期 我们的51单片机无论是开发板还是最小系统
  • cmake 的使用

    官网教程 xff1a https cmake org cmake tutorial 第一个简单的例子 源文件 xff1a tutorial cpp 1 A simple program that computes the square ro
  • python 读取一个文件夹下的所jpg文件保存到txt中

    最近需要使用统计一个目录下的所有文件 xff0c 使用python比较方便 xff0c 就整理了一下代码 1 import os 2 3 def gci filepath 4 files 61 os listdir filepath 5 f
  • cmake 单个目录多个文件的情况

    参考 xff1a https www hahack com codes cmake 源文件一共有三个 xff1a main cpp MathFunctions h MathFunctions cpp 文件内容分别如下 xff1a main
  • k8s config配置文件

    接着上面的博客继续写 pwd gt etc kubernetes cat config kubernetes system config The following values are used to configure various
  • html5手机web页面底部菜单

    一 效果图 二 HTML代码 lt header class 61 34 text center 34 gt TOP lt header gt lt div id 61 34 content 34 gt lt div gt lt div i
  • redis hset hmset过期时间

    hmset m k v 127 0 0 1 6379 gt hset m k v integer 1 127 0 0 1 6379 gt hget m k 34 v 34 127 0 0 1 6379 gt expire m 30 inte
  • Mac下 .bash_profile 和 .zshrc 两者之间的区别

    这是我碰到的需要 source 之后才能使用环境变量的问题 xff0c 我就不细究了 xff0c 说说我的看法 bash profile 中修改环境变量只对当前窗口有效 xff0c 而且需要 source bash profile才能使用
  • h5页面使用js实现保存当前图片到手机相册

    很可惜 xff0c 这个鬼东西微信内置浏览器不适用 页面 xff1a lt doctype html gt lt html gt lt head gt lt meta charset 61 34 UTF 8 34 gt lt meta co
  • HTTP认证之基本认证——Basic(一)

    导航 HTTP认证之基本认证 Basic xff08 一 xff09 HTTP认证之基本认证 Basic xff08 二 xff09 HTTP认证之摘要认证 Digest xff08 一 xff09 HTTP认证之摘要认证 Digest x
  • android给方法设置进度,Android自定义View实现多节点进度条功能的方法

    Android自定义View实现多节点进度条功能的方法 发布时间 xff1a 2020 07 28 16 05 13 来源 xff1a 亿速云 阅读 xff1a 122 作者 xff1a 小猪 这篇文章主要讲解了Android自定义View
  • Linux 系统中如何查看日志 (常用命令)

    cat tail f 日志文件 日 志 文 件说 明 var log message系统启动后的信息和错误日志 xff0c 是Red Hat Linux中最常用的日志之一 var log secure与安全相关的日志信息 var log m
  • 智能指针(shared_ptr,unique_ptr)作为函数参数或者返回值时的一些注意事项

    智能指针 shared ptr unique ptr 作为函数参数或者返回值时的一些注意事项 当智能指针作为函数的参数或者返回值时 xff0c 一直在纠结到底是用智能指针对象本身还是用原始指针 Herb Sutter大师的文章很好的解决了这
  • CSP-S 模拟测试 51 题解

    考试过程 xff1a 惯例先看一遍三道题 xff0c T1 一开始反应要求割点 xff0c 但是这是有向图 xff0c 肯定不能求割点 xff0c 康了一下数据范围 xff0c 有40 是树的 xff0c 还不错 xff0c 决定待会在打
  • CSP-S 模拟测试57题解

    人生第一次A B层一块考rank2 xff0c 虽然说分差没几分 xff0c 但还是值得纪念 题解 xff1a T1 天空龙 xff1a 大神题 xff0c 因为我从不写快读也没有写考场注释的习惯 xff0c 所以不会做 xff0c 全hz
  • CSP-S 模拟53 题解

    题解 xff1a T1 u xff1a 一看到修改这么多 xff0c 但询问其实只有一个不难想到差分 xff0c 但是他这个形状可以说很不规则 xff0c 于是我们想到分别维护竖着的和斜着的差分 xff0c 然后最后合并即可 考场上瞎调了一
  • Springboot+Mybaits之两张表同时插入数据

    项目需求是 xff0c 一张表添加数据的同时 xff0c 另外一张表也需要添加数据 xff0c 话不多说 xff0c 直接上代码 1 Controller xff0c 我把两个DTO直接放到一个 64 RequestBody中 其中thro

随机推荐