大小端判断方法和转换

1.什么是大小端?

这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为 8bit。但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的long型(要看具体的编译器),另外,对于位数大于 8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。

2.大小端定义

大端Big Endian模式:即把数据的高字节放到低地址中
小端Little Endian模式:高字节放到高地址中

3.图解

画张图简单解释下大小端的区别,比如我们要存取一个0x12345678的数据,在大小端机器的存取方式分别是:

大端模式:

高位 -----------------> 低位

0x78 | 0x56 | 0x34 | 0x12

小端模式:

高位 ------------------> 低位
0x12 | 0x34 | 0x56 | 0x78


image.png

4 判断机器大小端方式

1.字符指针判断
在32位平台下,int占4个字节,而char类型的指针是占一个字节的,如果我们把int强传为char类型的指针,只会保存一个字节的数据,那么我们只需要判断char里面的第一个字节和int里面的第一个字节是否是一致即可判断。
如果一致则为小端模式,反之为大端模式。

下面代码我们令 int a=1 如果是小端模式,int下1会存放在在低地址处,而强传为char类型的指针,1也在低地址处,所以可以判断。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main() {
    // judge  large or small

    uint64_t flag = 2;
    uint64_t *raw_ptr = &flag;

    char *p = reinterpret_cast(raw_ptr);
    
    if(*(p) == 2) {
        std::cout << "small" << std::endl;
    } else {
        std::cout << "large" << std::endl;
    }

    return 0;
}

image.png

2.联合体判断
由于联合体所有数据共享一块地址空间,存放数据的所有成员都是从低地址开始存放,所以我们可以在联合体内定义一个int和一个char类型变量,然后在外部实例化的时候创建int变量,用char变量调用,相当于隐式类型转化,如果结果为1,则低字节存放在低地址,既是小端机器,反之大端机器。

void small_large_union() {
    union flag{
        uint64_t flag1;
        char flag2;
    };

    flag f;
    f.flag1 =1;

    if(f.flag2 == 1) {
        std::cout << "small" << std::endl;
    } else {
        std::cout << "large" << std::endl;
    }
    
}

这里复习一下union
union 型数据所占的空间等于其最大的成员所占的空间。对 union 型的成员的存取都是相对于该联合体基地址的偏移量为 0 处开始,也就是联合体的访问不论对哪个变量的存取都是从 union 的首地址位置开始。

联合是一个在同一个存储空间里存储不同类型数据的数据类型。这些存储区的地址都是一样的,联合里不同存储区的内存是重叠的,修改了任何一个其他的会受影响。

共用体表示几个变量共用一个内存位置,在不同的时间保存不同的数据类型和不同长度的变量。在union中,所有的共用体成员共用一个空间,并且同一时间只能储存其中一个成员变量的值。

常见CPU的字节序

Big Endian : PowerPC、IBM、Sun
Little Endian : x86、DEC
ARM既可以工作在大端模式,也可以工作在小端模式。

5.大小端转换

法1:位运算

#include  
  
typedef unsigned int uint_32 ;  
typedef unsigned short uint_16 ;  
 
//16位
#define BSWAP_16(x) \
    (uint_16)((((uint_16)(x) & 0x00ff) << 8) | \
              (((uint_16)(x) & 0xff00) >> 8) \
             )
             
//32位               
#define BSWAP_32(x) \
    (uint_32)((((uint_32)(x) & 0xff000000) >> 24) | \
              (((uint_32)(x) & 0x00ff0000) >> 8) | \
              (((uint_32)(x) & 0x0000ff00) << 8) | \
              (((uint_32)(x) & 0x000000ff) << 24) \
             )  
 
//无符号整型16位  
uint_16 bswap_16(uint_16 x)  
{  
    return (((uint_16)(x) & 0x00ff) << 8) | \
           (((uint_16)(x) & 0xff00) >> 8) ;  
}  
 
//无符号整型32位
uint_32 bswap_32(uint_32 x)  
{  
    return (((uint_32)(x) & 0xff000000) >> 24) | \
           (((uint_32)(x) & 0x00ff0000) >> 8) | \
           (((uint_32)(x) & 0x0000ff00) << 8) | \
           (((uint_32)(x) & 0x000000ff) << 24) ;  
}  
 
int main(int argc,char *argv[])  
{  
    printf("------------带参宏-------------\n");  
    printf("%#x\n",BSWAP_16(0x1234)) ;  
    printf("%#x\n",BSWAP_32(0x12345678));  
    printf("------------函数调用-----------\n");  
    printf("%#x\n",bswap_16(0x1234)) ;  
    printf("%#x\n",bswap_32(0x12345678));  
      
    return 0 ;  
}  
输出结果:
------------带参宏-------------
0x3412
0x78563412
------------函数调用-----------
0x3412
0x78563412

法2:arpa.inet.h网络库中有一些将数据转换为网络字节顺序的函数

htonl() //32位无符号整型的主机字节顺序到网络字节顺序的转换(小端->>大端)
htons() //16位无符号短整型的主机字节顺序到网络字节顺序的转换 (小端->>大端)
ntohl() //32位无符号整型的网络字节顺序到主机字节顺序的转换 (大端->>小端)
ntohs() //16位无符号短整型的网络字节顺序到主机字节顺序的转换 (大端->>小端)

注,主机字节顺序,X86一般多为小端(little-endian),网络字节顺序,即大端(big-endian);

举个栗子:

#include 
#include 
#include 
#include 

int main(int argc, char * argv[]) {
    union flag{
        int a;
        char b[2];
    };

    flag f;
    f.b[0] = 0x11;
    f.b[1] = 0x22;
    //0x2211 为小端  0x1122 为大端
    

    //htons
    printf("original: 0x%x\n",f.a);
    printf("trans:    0x%x\n",htons(f.a));

    return 0;
}
结果

6.大小端问题的来源

大多数编程语言有关于位域都有相关的定义,区别于我们常说的硬件有关的大小端,编译器也需要保证位分配顺序是确定、有迹可循的。

The order of allocation of bit-fields within a unit (C90 6.5.2.1[1], C99 6.7.2.1[2]).
而标准中规定如果一个结构体中含有位域,那么位域的写入顺序和当前系统字节序有关。也就是说编译器需要保证有位域的类型分配顺序。先定义的位域在大端环境从MSB开始分配,如果是小端环境则先定义的位域从LSB开始分配。
先补充两个关键词,MSB和LSB:
MSB:MoST Significant Bit ------- 最高有效位
LSB:Least Significant Bit ------- 最低有效位

struct bitfield {
    _u8 a:1;
    _u8 b:2;
    _u8 c:3;
}bf;
bf.a=1;
bf.b=2;
bf.c=3;

那么在大端序机器上

bf.a--、
bf.b--+--、
bf.c--+--+--、
      |  |  |
      |  |  |
      V  V  V
     +-+--+---+--+
      1 10 011 xx
     +-+--+---+--+
MSB                  LSB

在小端机器上

bf.a------------+
bf.b---------+  |
bf.c------+  |  |
          |  |  |
          |  |  |
          V  V  V
     +--+---+--+-+
      xx 011 10 1
     +--+---+--+-+
MSB                  LSB

在编译器层面保证了单个字节的IO顺序,实质上可以认为是从汇编/cpu层面对上层隐藏了这一问题。在FPGA或者其他领域,就要涉及到线路传输时,通常一个字节会被拆分成8个比特进行传输,此时MSB会先传输高位,而LSB将会先传输低位,这就是我们看到的不论大小端机器单个字节内顺序一致的原因。

理解了硬件上大小端的概念,就能很容易的掌握位域操作在大小端硬件上的分配布局,从而不会在编码时产生混淆。

处理器在硬件上由于端模式问题在设计中有所不同,主要是计算机业界早期发展先驱们做的取舍不同。大端序机器首先会读到高位,而高位一般会是符号位置,经常用来做标记,早期缓冲设备缓存很小的情况下,先收到高字节能快速判断报文信息,需要多大的缓存、ip掩码计算等等,而小端序则非常适合进行加法器,进位的顺序从地位加到高位依次进位就可以了,而大端序需要加载完全部字节。从系统的角度上看,端模式问题对软件和硬件的设计带来了不同的影响,当一个处理器系统中大小端模式同时存在时,必须要对这些不同端模式的访问进行特殊的处理。

TCP/IP协议的RFC7000[3]规定了网络传输使用大端序,也是因为早期网络设备缓冲区小,性能弱,采用了大端序,主导了网络协议。

7.Reference

[1].C89/C90 standard (ISO/IEC 9899:1990) 6.5.2.1
[2].C99 standard (ISO/IEC 9899:1999) 6.7.2.1

你可能感兴趣的:(大小端判断方法和转换)