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






3.18/5 (5投票s)
如何解决多平台应用程序中的字节序转换
引言
在本文中,我将探讨字节序转换问题,并提供一套汇编函数来解决它。这些函数适用于 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.c 或 ec_arm_linux.s。
我对汇编语言或汇编器不太熟悉。我只是自学了完成转换所需的东西,不多了。因此,如果存在任何错误或遗漏,请原谅我。
关注点
从这次经历中,我了解到机器指令和 C 语言之间存在巨大的鸿沟。因此,我对编译器编写者更加感激。这确实是一项非常艰巨的工作。我还了解到,C 语言是平台无关的,编译器无关的。但有时,编写汇编代码会更快、更直接地完成工作。
参考文献
历史
- 2008 年 10 月 1 日 - 第一个版本