65.9K
CodeProject 正在变化。 阅读更多。
Home

关于大端和小端转换的基本概念

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (48投票s)

2003年8月19日

7分钟阅读

viewsIcon

374864

关于字节序的新手入门介绍。

引言

很久以前,在一个叫做小人国(Lilliput)的遥远岛屿上,社会分裂成两个派别:大端派(Big-Endians),他们从较大的末端(“原始的方式”)掰开他们的软煮鸡蛋;以及小端派(Little-Endians),他们从较小的末端掰开鸡蛋。由于皇帝命令所有臣民都要从较小的末端掰开鸡蛋,这导致了一场具有戏剧性后果的内战:11,000 人曾数次宁愿死去也不愿屈服于从较小的末端掰开鸡蛋。最终,“大端派”与“小端派”的争执也蔓延到了计算机领域,它指的是在多字节数字中,字节的存储顺序,更精确地说,是最高有效字节在前(大端序)还是最低有效字节在前(小端序)[1]

  • 大端序 (Big-Endian) 意味着任何多字节数据字段的最高有效字节存储在最低的内存地址,这也是该字段的较大地址。
  • 小端序 (Little-Endian) 意味着任何多字节数据字段的最低有效字节存储在最低的内存地址,这也是该字段的较大地址。

例如,考虑32位数字 0xDEADBEEF。按照大端序约定,计算机将这样存储它:

Big-Endian

图 1。 大端序:最高有效字节存储在最低字节地址。

而遵循小端序规则的架构将按图 2 所示存储它。

Little-Endian

图 2。 小端序:最低有效字节存储在最低字节地址。

Intel x86 系列和 Digital Equipment Corporation 架构(PDP-11、VAX、Alpha)是小端序的代表,而 Sun SPARC、IBM 360/370 以及 Motorola 68000 和 88000 架构是大端序。然而,其他架构如 PowerPC、MIPS 和 Intel 的 64 位 IA-64 是双端序(Bi-Endian),即它们能够以大端序或小端序模式运行。[1]

字节序也称为 NUXI 问题。想象一下单词 UNIX 存储在两个 2 字节字中。在大端序系统中,它将被存储为 UNIX。在小端序系统中,它将被存储为 NUXI。

哪种格式更好?

就像《格列佛游记》中描述的鸡蛋之争一样,大端序与小端序的计算机之争更多地与政治问题有关,而不是技术优势。实际上,在大多数应用中,两种系统性能都 equally well。然而,在网络设备中使用小端序处理器而不是大端序处理器时,性能上存在显著差异(更多细节见下方)。

如何从一种格式切换到另一种格式?

如果要获取另一种格式,反转多字节数字非常容易,只需交换字节即可,并且转换方向相同。以下示例展示了如何使用简单的 C union 来实现字节序转换函数。

unsigned long ByteSwap1 (unsigned long nLongNumber)
{
   union u {unsigned long vi; unsigned char c[sizeof(unsigned long)];}; 
   union v {unsigned long ni; unsigned char d[sizeof(unsigned long)];};
   union u un; 
   union v vn; 
   un.vi = nLongNumber; 
   vn.d[0]=un.c[3]; 
   vn.d[1]=un.c[2]; 
   vn.d[2]=un.c[1]; 
   vn.d[3]=un.c[0]; 
   return (vn.ni); 
}

请注意,此函数旨在处理 32 位整数。

可以使用位操作实现一个更高效的函数,如下所示:

unsigned long ByteSwap2 (unsigned long nLongNumber)
{
   return (((nLongNumber&0x000000FF)<<24)+((nLongNumber&0x0000FF00)<<8)+
   ((nLongNumber&0x00FF0000)>>8)+((nLongNumber&0xFF000000)>>24));
}

这是一个汇编语言版本:

unsigned long ByteSwap3 (unsigned long nLongNumber)
{
   unsigned long nRetNumber ;

   __asm
   {
      mov eax, nLongNumber
      xchg ah, al
      ror eax, 16
      xchg ah, al
      mov nRetNumber, eax
   }

   return nRetNumber;
}

16 位字节交换函数的版本非常直接:

unsigned short ByteSwap4 (unsigned short nValue)
{
   return (((nValue>> 8)) | (nValue << 8));

}

最后,我们可以编写一个更通用的函数,它可以处理任何原子数据类型(例如 intfloatdouble 等),并自动检测大小。

#include <algorithm> //required for std::swap

#define ByteSwap5(x) ByteSwap((unsigned char *) &x,sizeof(x))

void ByteSwap(unsigned char * b, int n)
{
   register int i = 0;
   register int j = n-1;
   while (i<j)
   {
      std::swap(b[i], b[j]);
      i++, j--;
   }
}

例如,下面的代码片段展示了如何将一个 double 数据数组从一种格式(例如大端序)转换为另一种格式(例如小端序)。

double* dArray; //array in big-endian format
int n; //Number of elements

for (register int i = 0; i <n; i++) 
   ByteSwap5(dArray[i]);

实际上,在大多数情况下,您无需实现上述任何函数,因为有一组套接字函数(参见表 I),在 winsock2.h 中声明,它们是为 TCP/IP 定义的,因此所有支持 TCP/IP 网络的主机都可用。它们以“网络字节序”存储数据,这是一种标准且与字节序无关的格式。

函数 目的
ntohs 将 16 位数据从网络字节序转换为主机字节序(大端序转换为小端序)。
ntohl 将 32 位数据从网络字节序转换为主机字节序(大端序转换为小端序)。
htons 将 16 位数据从主机字节序转换为网络字节序(小端序转换为大端序)。
htonl 将 32 位数据从主机字节序转换为网络字节序(小端序转换为大端序)。

表 I: Windows Sockets 字节序转换函数 [2]

套接字接口指定了一种称为网络字节序的标准字节顺序,碰巧是大端序。因此,所有网络通信都应该是大端序,无论客户端或服务器的架构如何。

假设您的机器使用小端序。要通过 TCP/IP 连接传输 32 位值 0x0a0b0c0d,您必须调用 htonl() 并传输结果。

TransmitNum(htonl(0x0a0b0c0d)); 

同样,要转换传入的 32 位值,请使用 ntohl()

int n = ntohl(GetNumberFromNetwork()); 

如果运行 TCP/IP 堆栈的处理器本身也是大端序,则四个宏(即 ntohsntohlhtonshtonl)都将定义为不执行任何操作,运行时不会有性能影响。但是,如果处理器是小端序,这些宏将相应地重新排列字节。构建和解析网络数据包以及创建套接字连接时,通常会调用这些宏。在小端序处理器上使用 TCP/IP 时,会出现严重的运行时性能损失。因此,对于路由器或网关等具有大量网络功能的设备,选择小端序处理器可能是不明智的。(摘自参考文献 [1])。

主机到网络 API 的一个额外问题是它们无法处理 64 位数据元素。但是,您可以编写自己的 ntohll()htonll() 对应函数。

  • ntohll:将 64 位整数转换为主机字节序。
  • htonll:将 64 位整数转换为网络字节序。

实现足够简单:

#define ntohll(x) (((_int64)(ntohl((int)((x << 32) >> 32))) << 32) | 
                     (unsigned int)ntohl(((int)(x >> 32)))) //By Runner
#define htonll(x) ntohll(x)

如何动态测试运行时字节序类型?

Computer Animation FAQ 中所述,您可以使用以下函数来查看您的代码是在小端序还是大端序系统上运行。

#define BIG_ENDIAN      0
#define LITTLE_ENDIAN   1

int TestByteOrder()
{
   short int word = 0x0001;
   char *byte = (char *) &word;
   return(byte[0] ? LITTLE_ENDIAN : BIG_ENDIAN);
}

此代码将值 0001h 分配给一个 16 位整数。然后,将一个字符指针分配给指向该整数值的第一个(最低有效)字节。如果整数的第一个字节是 0x01h,则该系统是小端序(0x01h 位于最低地址,即最低有效地址)。如果它是 0x00h,则该系统是大端序。

同样,

bool IsBigEndian()
{
   short word = 0x4321;
   if((*(char *)& word) != 0x21 )
     return true;
   else 
     return false;
}

这只是同一枚硬币的另一面。

您还可以使用标准的字节序 API 在运行时确定系统的字节顺序。例如:

bool IsBigEndian() { return( htonl(1)==1 ); }

自动检测数据文件的正确字节序格式

假设您正在开发一个导入核磁共振 (NMR) 光谱的 Windows 应用程序。高分辨率 NMR 文件通常在 Silicon 或 Sun 工作站上记录,但最近 Windows 或 Linux 兼容的光谱仪正成为可行的替代方案。事实证明,您需要提前知道文件的字节序格式才能正确解析所有信息。以下是一些您可以遵循的实用指南,以弄清楚数据文件的正确字节序:

  1. 通常,二进制文件包含一个包含字节序格式信息的头。
  2. 如果不存在头,您可以根据您知道的文件来源计算机的本机格式来猜测字节序格式。例如,如果文件是在 Sun 工作站上创建的,则字节序格式很可能是大端序。
  3. 如果以上几点都不适用,可以通过试错法确定字节序格式。例如,如果在假设一种格式读取文件后,光谱没有意义,那么您就知道必须使用另一种格式。

如果文件中的数据点是浮点格式(double),则 _isnan() 函数可能有助于确定字节序格式。例如:

double dValue;
FILE* fp;
(...)
fread(&nValue, 1, sizeof(double), fp);
bool bByteSwap = _isnan(dValue) ? true : false

请注意,此方法仅保证当 _isnan() 返回非零值(TRUE)时需要进行字节交换操作;如果它返回 0(FALSE),则在没有进一步信息的情况下,无法确定正确的字节序格式。

致谢

感谢 Santiago Domínguez、Ehsan Akhgari、Santiago Fraga 和 Ignacio Sordo 提出的宝贵建议。

参考文献

  1. Michael Barr 撰写的《字节序入门》Introducction to Endianness,发表于 Embedded Systems Programming。
  2. 《Visual C++ 概念:添加功能。 Windows 套接字:字节排序》Visual C++ Concepts: Adding Functionality. Windows Sockets: Byte Ordering

深入阅读

© . All rights reserved.