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

ARM 和 x86 汇编中的字节序转换

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.18/5 (5投票s)

2008 年 10 月 1 日

CPOL

5分钟阅读

viewsIcon

49854

downloadIcon

196

如何解决多平台应用程序中的字节序转换

引言

在本文中,我将探讨字节序转换问题,并提供一套汇编函数来解决它。这些函数适用于 x86 和 ARM 架构,并且可以在 VS 和 GCC 下进行编译。

背景

在一个跨平台项目中,我们遇到了一个老生常谈的问题——字节序转换。如果文件是在小端字节序机器上生成的,一个整数 255 可能会存储为

ff 00 00 00 

但是当它被读取到内存中时,不同平台上的值将不同。这将导致移植问题。

int a;
fread(&a, sizeof(int), 1, file);
// on little endian machine, a = 0xff;
// but on big endian machine, a = 0xff000000; 

第一种方法

解决此问题的一个简单而有效的方法如下。我们可以编写一个名为 readInt() 的函数。

void readInt(void* p, file)
{ 
    char buf[4];
    fread(buf, 4, 1, file); 
    *((uint32*)p) = buf[0] << 24 | buf[1] << 16 
                   | buf[2] << 8 | buf[3];}    

该函数在大小端字节序平台都能工作的优点。但它牺牲了我们读取结构体的便利性。

fread(&header, sizeof(struct MyFileHeader), 1, file);

如果 MyFileHeader 包含许多整数,我们将不得不将其分解为多个 read() 调用。这不仅代码编写起来很麻烦,而且由于增加了 IO 操作,运行速度也很慢。因此,我将提出另一种方法。
我们可以保持代码不变,并使用几个宏来对数据进行后处理。

fread(&header, sizeof(struct MyFileHeader), 1, file);
CQ_NTOHL(header.version);
CQ_NTOHL_ARRAY(&header.box, 4); // box is a RECT structure 

如果机器的字节序与数据文件的字节序不匹配,则定义这些宏来执行某些函数,否则将它们定义为空。

#if defined(ENDIAN_CONVERSION)
#    define CQ_NTOHL(a) {a = ((a) >> 24) | (((a) & 0xff0000) >> 8) | 
	(((a) & 0xff00) << 8) | ((a) << 24); }
#    define CQ_NTOHL_ARRAY(arr, num) {uint32 i; 
	for(i = 0; i < num; i++) {CQ_NTOHL(arr[i]); }}
#else
#    define CQ_NTOHL(a)
#    define CQ_NTOHL_ARRAY(arr, num)
#endif

这种方法的优点是,如果未定义 ENDIAN_CONVERSION ,则不会浪费 CPU 周期。并且代码可以保留其一次读取整个结构的自然形式。

C 语言的衰落

这是我们用 C 语言能达到的最好结果。但我回想起 80x86 系列 CPU 有一个专门的指令来执行字节序转换。稍微搜索一下就证实了它是 BSWAP。对于 ARM 系列 CPU,也有一个算法可以加速它。

遗憾的是,C 语言有一个局限性。由于底层机器结构差异很大,一个高级语言如何能够利用它们的所有强大功能呢?

在最简单的除法场景中,8086 有一个 DIV 指令,它将商和余数分别存储在 AH AL 中。但在 C 语言中,我们必须这样写:

Quotient = a / b; 
Remainder = a % b;

表面上看,计算进行了两次。但一个好的编译器能够优化它。

再举个例子,几乎所有的架构都有右转指令。我想知道为什么 C 语言甚至不考虑它。所以在 C 语言中,如果我们想将 a 右转 b 位,我们必须这样写:

uint32 mask = (1 << b) - 1;
a = (a >> b) | ((a & mask) << (32 - b)); 

像算术左移/右移、循环左移/右移这样的指令,在 C 语言中都没有对应的指令。

对于 ARM 这样的 RISC 机器,其设计巧妙而有趣,指令集与 C 语言语义之间存在巨大的差距,因此必须在编译器上投入大量的精力。我们还能相信编译器生成所需的代码吗?不能。我用 VS2008 尝试了上面的 CQ_NTOHL(),它未能使用 BSWAP。我调试了 winsock.h 中的 ntohl() 函数,它也没有使用 BSWAP。

如何使用代码

我认为这有一个可能的解释。因为字节序转换花费的时间与前面的 IO 相比微不足道,所以我似乎有点得意忘形了。但这正是我。 ;) 我乐于将其推向极限。

在接下来的章节中,我将为 80486 和 ARM CPU 编写汇编代码,并告诉您如何同时在 VS 和 GCC 下编译它们。但首先,让我们声明我们将要编写的函数。

uint32 cq_ntohl(uint32 value);
void cq_ntohl_array(uint32* arr, uint32 num);
uint16 cq_ntohs(uint16 value);
void cq_ntohs_array(uint16* arr, uint32 num);
#define CQ_NTOHL(a) a = cq_ntohl(a);
#define CQ_NTOHL_ARRAY(p, n) cq_ntohl_array(p, n);
#define CQ_NTOHS(a) a = cq_ntohs(a);
#define CQ_NTOHS_ARRAY(p, n) cq_ntohs_array(p, n); 

至于实现,我将为您提供 4 个版本:

  • Visual Studio。x86
  • Visual Studio。智能设备
  • GCC arm-linux-gcc 内联汇编
  • GCC arm-linux-as 汇编器

源代码已打包成 zip 文件。此处我只解释如何在项目中进行使用以及需要注意的一些事项。

Visual Studio。x86

Visual Studio 支持内联 x86 汇编。因此,只需将编译的 ec_x86.c 添加到您的 VS 项目中即可。32 位整数使用 80486 指令 BSWAP 进行转换,16 位整数使用简单的 8 位右转进行转换。

uint32 cq_ntohl(uint32 a) {
    __asm{
        mov eax, a;
        bswap eax; 
    }
}

void cq_ntohl_array(uint32* p, uint32 num) {
    __asm{
        mov eax, dword ptr [p];
        mov ecx, num;
next:
        mov ebx, [eax];
        bswap ebx;
        mov dword ptr[eax], ebx;
        add eax, 4;
        loop next;
    }
} 
uint16 cq_ntohs(uint16 a) {
    __asm{
        mov ax, a;
        ror ax, 8;
    }
}

void cq_ntohs_array(uint16* arr, uint32 num) {
    __asm{
        mov eax, dword ptr [arr];
        mov ecx, num;
next: 
        mov bx, word ptr [eax];
        ror bx, 8;
        mov word ptr[eax], bx;
        add eax, 2;
        loop next;
    }
}  

Visual Studio。智能设备

Visual Studio 不支持内联 arm 汇编。因此,您需要使用 armasm.exe 编译 ec_arm.s 文件。

armasm.exe ec_arm.s ec_arm.obj 

然后像普通的静态链接库一样,将您的 EXE 与 .obj 文件链接起来。或者,您可以将 .s 添加到您的项目中并设置自定义构建步骤。但我不会在此处详细说明。

32 位字节序转换算法涉及异或、右转和右移。

    eor r1, r0, r0, ROR #16
    bic r1, r1, #0xFF, 16
    mov r0, r0, ror #8
    eor r0, r0, r1, lsr #8

这有点难理解。但我们可以验证它。设

r0 = 0xaabbccdd. 
r1 = r0 ^ (r0 rotate-right 16) 
   = 0xaabbccdd ^ 0xccddaabb 
   = 0x(aa^cc,bb^dd,cc^aa,dd^bb); 
r1 = r1 & not(0xff0000) 
   = r1 & 0xff00ffff 
   = 0x(aa^cc, 0, cc^aa, dd^bb);
r0 = r0 rotate-right 8 = 0xddaabbcc;
r0 = r0 ^ (r1 >> 8) 
   = 0xddaabbcc ^ 0x(0, aa^cc, 0, cc^aa) 
   = 0x(dd, aa^aa^cc, bb, cc^cc^aa)
   = 0x(dd,cc,bb,aa);

令人惊叹的是,这些计算可以浓缩成短短的四个 32 位 arm 指令。

GCC Arm-Linux-gcc

arm-linux-gcc 支持内联汇编和 .s 文件。因此,您可以选择使用 ec_arm_linux.cec_arm_linux.s

我对汇编语言或汇编器不太熟悉。我只是自学了完成转换所需的东西,不多了。因此,如果存在任何错误或遗漏,请原谅我。

关注点

从这次经历中,我了解到机器指令和 C 语言之间存在巨大的鸿沟。因此,我对编译器编写者更加感激。这确实是一项非常艰巨的工作。我还了解到,C 语言是平台无关的,编译器无关的。但有时,编写汇编代码会更快、更直接地完成工作。

参考文献

历史

  • 2008 年 10 月 1 日 - 第一个版本
© . All rights reserved.