深入理解C语言(二)——比特序和字节序

本文详细介绍了比特序和字节序的概念,以及大端和小端的差异。讲述了在编程中如何处理不同字节序的兼容性问题,包括网络数据传输、内存存取、大小端转换以及编译器支持等内容。
摘要由CSDN通过智能技术生成

比特序的概念

我们知道一个字节有8位,也就是8个比特位。从第0位到第7位共8位。比特序就是用来描述比特位在字节中的存放顺序的。字节中比特位的顺序有两种, LSB 0 位序和MSB 0 位序。

LSB是指 least significant bit, LSB 0 位序是指:字节的第0位存放数据的least significant bit,即我们的数据的最低位存放在字节的第0位。因此下图中的数值为0x95,对应十进制149。

LSB 0: A container for 8-bit binary number with the highlighted least significant bit assigned the bit number 0

MSB是指 most significant bit。MSB 0 位序是指:字节的第0位存放数据的most significant bit,即我们的数据的最高位存放在字节的第0位。下图中MSB在最左侧,因此下图中的数值同样为0x95。如果下图中最左边的1是LSB,则数值就变成0xa9

MSB 0:A container for 8-bit binary number with the highlighted most significant bit assigned the bit number 0

字节序的概念

如果计算机中所有的数据都可以在一个字节byte中描述,那么就不存在字节序的问题。正是因为存在多字节数据,所以字节之间也存在顺序问题,也就是字节序。由于历史的原因,业界存在两种字节序标准:Big-Endian(大端)和Little-Endian(小端)。Power PC是大端,X86是小端,MIPS ARM均可以通过寄存器设置字节序。

所谓大端就是高位在低字节,低位在高字节,小端则与此相反。例如0x12345678在大端和小端CPU中的字节排布如下图所示:

从上图可知,大端更符合我们的思维方式,所见即所得。但实际上字节序没有优劣之分。纯粹是历史原因导致硬件间的不一致。

比特序通常与字节序一致,也就是说,大字节序系统上,高比特位存放在低比特地址,在小字节序系统上,高比特位存储在高比特地址。

注意:字节序只是数据在内存中存放的顺序问题。我们对代码中数的理解(比如C代码中的常量),与CPU对通用寄存器中数据的理解方式是一样的。都是右边为低位(Least Significant Bit,缩写LSB)左边为高位(Most Significant Bit,缩写MSB)。CPU对数据位移等操作都是基于这种理解,与字节序无关。只有将寄存器数据放入内存或从内存取出时,才会面临字节序问题,也就是LSB是放在低内存地址还是高内存地址。

判断本机字节序的方式

代码获取本机字节序有多种方式

方式一:通过将int类型强制类型转换成char单字节,通过判断起始存储位置可以获取本机字节序。

示例:

BOOL IsBigEndian()  

{  

    int a = 0x1234;  

    char b =  *(char *)&a;   

    return (b == 0x12 ? TRUE : FALSE);

}

方式二:联合体union的存放顺序是所有成员都从低地址开始存放,利用该特性可以获得本机字节序。

BOOL IsBigEndian()  

{  

    union NUM  

    {  

        int a;  

        char b;  

    }num;  

    num.a = 0x1234;  

    return (num.b == 0x12 ? TRUE : FALSE);

}

字节序对开发者的影响

一、考虑代码兼容不同字节序的CPU

二、考虑数据在不同字节序的系统中流转

例如:

a. 网络数据传输

b. CPU与外设或其他板内其他芯片的通信或共享内存

三、考虑本机自身对内存数据的存取不一致

a. 指针强制类型转换后的读写

b. 联合体不同位宽的重叠数据访问

c. 调试时查看内存

字节序——大小端转换

     比如32位的整形数字 0x12345678,在内存中存储为:

     低地址   | 0x78 | 0x56 | 0x34 | 0x12 | 高地址

     如果为大端模式,那么在内存中存储为:

     低地址   | 0x12 | 0x34 | 0x56 | 0x78 | 高地址

所以按照一种字节序读出为0x12345678的值,转换为另一种字节序后就是0x78563412

按照大小端转换的思想,可以轻松自定义类似的转换宏:

#define    NTOHL(val)          ((((val) & 0xff) << 24) |\

                               (((val) & 0xff00) <<  8) |\

                               (((val) & 0xff0000) >>  8) |\

                               (((val) & 0xff000000) >> 24))

字节序——网络字节序

在网络传输过程中,本端CPU无法了解到对端CPU的字节序,因此产生了“网络字节序”的概念——网络字节序采用 Big-Endian(大端)。网络字节序对应的概念是“主机字节序”,即本CPU访问内存时的字节序

读写用于网络通信的数据,都需要做“主机字节序”与“网络字节序”之间的转换。这包括下面两个场景

  • 当两台主机通信时,发送数据前需要将数据转换成为网络字节序后再进行传输。
  • 当两台主机通信时,接收数据后需要将数据从网络字节序转换到本机后再读内容

数据在网络字节序与主机字节序之间互相转换的函数或宏(在使用big endian类型的系统中这些函数会定义成空宏)如下:

一些栗子:

  • htonl()      把32位值从主机字节序转换成网络字节序
  • htons()      把16位值从主机字节序转换成网络字节序
  • ntohl()      把32位值从网络字节序转换成主机字节序
  • ntohs()      把16位值从网络字节序转换成主机字节序

使用举例:

typedef struct tag_MSG {

        U8 type;

        U8 opcode;

        U16 arg;

        U32 result;

} MSG_S

MSG_S msg;

//发送消息时按网络字节序填充消息

msg.type = 2;

msg.opcode = 6;

msg.arg = htons(129);

msg.result = htonl(255);

 //接收解析消息时将网络字节序转换成主机字节序

注意:单字节的对象不需要做大小端转换,可以回忆一下字节序的定义

字节序——外设总线字节序

外设总线是指在系统上连接CPU,外部设备和其他不同设备的中间媒体。总线的字节序是由总线协议定义的,其他设备必须遵守。以一个小字节序PCI总线为例。在32个地址/数据总线Line AD[31:0]中,总线需要连接32比特的设备,高比特数据线连接到AD31,低比特数据线连接到AD0。大字节序总线协议则相反。对于PCI总线,协议要求PCI设备实现一个配置空间。这是一组配置寄存器,它们和总线具有相同的字节序。像所有设备必须遵守总线字节/比特大小端规则一样,CPU也必须如此。如果CPU以不同于总线的大小端方式工作,总线控制器/桥通常需要进行大小端转换。

这里除了字节序以外,还涉及到比特序的问题,问题会更加复杂,我们在下一篇位域的章节中再展开讨论。

字节序——存取不一致

通过指针操作不同基本数据类型情况(涉及指针强转),需要考虑字节序问题。

Typedef struct _S {

       U32 i;

       U8 c1;

       U8 c2;

U8 c3;

U8 c4;

}

指针p原本指向结构体起始地址,为了使用4字节原子操作来把修改c3为0,就涉及到大小端的区别

word = *((U32*)p+1);

在大端情况下,修改c3为0:

word_new = word & 0xffff00ff;

在小端情况下,修改c3为0;

Word_new = word&0xff00ffff

最后再用4字节原子操作整个word,实现对c3成员的原子读写。

字节序——编译器的支持

以GCC为例,

GCC通过以下预定义宏可以区分大小端:

__BYTE_ORDER__==__ORDER_LITTLE_ENDIAN__时是小端模式

__BYTE_ORDER__==__ORDER_BIG_ENDIAN__时是小端模式

__BYTE_ORDER__,__ORDER_LITTLE_ENDIAN__,__ORDER_BIG_ENDIAN__都是gcc预定义的宏,在代码中可以直接使用。

通过GCC命令” gcc -posix -E -dM - </dev/null”可以查看预定义宏内容

gcc还提供了大小端转换内置函数:

Built-in Function: uint16_t __builtin_bswap16 (uint16_t x)

Built-in Function: uint32_t __builtin_bswap32 (uint32_t x)

Built-in Function: uint64_t __builtin_bswap64 (uint64_t x)

_bswap16,_bswap32,_bswap64三个函数分别提供了16位,32位,64位数字的字节反转功能,正好可以用来实现16,32,64位数字的大小端转换

相对于自定义转换宏,使用内置函数进行大小端转换可以获得性能上的优势。这是因为现代的CPU都在指令级别对于字节序翻转类的操作提供了更高效的指令,从而可以大大的提升执行性能。

GCC编译器已经利用了这一点,提供了内置的函数。直接使用内置的函数,就可以提升性能。

字节序——总结

为了实现代码跨运行平台做到大小端无关,需要在代码中识别以上提到的两类场景

对于本机对内存数据读写不一致的场景,需要使用大小端编译宏区别对待

对于数据在不通系统间流转的场景,需要在数据发送前和接收后进行字节序转换。当然如果传输中涉及到位域,那么我们在下一篇中再进行讨论。

下一篇:

上一篇:

深入理解C语言(一)——字节对齐

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Bluetangos

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值