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
4 判断机器大小端方式
1.字符指针判断
在32位平台下,int占4个字节,而char类型的指针是占一个字节的,如果我们把int强传为char类型的指针,只会保存一个字节的数据,那么我们只需要判断char里面的第一个字节和int里面的第一个字节是否是一致即可判断。
如果一致则为小端模式,反之为大端模式。
下面代码我们令 int a=1 如果是小端模式,int下1会存放在在低地址处,而强传为char类型的指针,1也在低地址处,所以可以判断。
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <string>
#include <string.h>
#include <memory>
#include <stdint.h>
int main() {
// judge large or small
uint64_t flag = 2;
uint64_t *raw_ptr = &flag;
char *p = reinterpret_cast<char*>(raw_ptr);
if(*(p) == 2) {
std::cout << "small" << std::endl;
} else {
std::cout << "large" << std::endl;
}
return 0;
}
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<stdio.h>
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 <stdio.h>
#include <stdint.h>
#include <string.h>
#include <arpa/inet.h>
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