eVC 的 ARM 汇编,带 Mono Jit 宏
eVC 的 ARM 汇编,带 Mono Jit 宏

引言
Microsoft 的 eMbedded Visual C 编译器除了生成原始操作码之外,无法为 ARM 系列微处理器使用内联汇编。Mono Jit for ARM 的宏可以更方便地完成这项工作。本文仅讨论 ARMV4 版本的 Windows CE 的汇编,即 Windows Mobile 5 之前的版本。此处提供的确切方法可能不适用于 ARMV4I 和 ARMV4T 平台;这尚未经过测试。
背景
机器语言编写的代码通常仍比 C 语言编写的代码快,但我们通常不再使用汇编来与 C 语言竞争速度。然而,在过去几年里,Lua 和 Cil 等字节码语言变得越来越重要,并且可以通过 JIT(Just-In-Time)大大加速。JIT 首先将原本“官僚化”解释的单个字节码转换为一个或多个汇编指令,将它们串联起来,然后才执行。这是 JIT 启动缓慢但随后速度极快的原因之一。
我认为抽象字节码和 JIT 是未来的发展方向,因为它们的组合在理论上是平台无关的,并且对普通程序员来说更易于使用。不幸的是,尚未出现通用的字节码标准:虽然主题上有很多(过多的)变种,但希望随着时间的推移这种情况会改变。
我发现通过查看 Sourceforge 上可下载的 Windows CE 的 Ogl/Es 实现的源代码,可以轻松地将 Mono ARM Jit 的宏用于 eVC。作者似乎为宏开发了更通用的包装器,但我在这里不使用它们,尽管我认为抽象的机器语言本身可能非常有用。它甚至可能成为制定字节码标准的条件。
Using the Code
Mono ARM 汇编宏位于几个头文件中;其中最重要的是 arm-codegen.h 和 arm_dpimacros.h,可选的 arm-dis.h 用于反汇编生成的代码,我觉得它非常棒。我使用的是 Mono 版本 1.2.4,并修正了向后分支的一个 bug
#define ARM_DEF_BR(offs, l, cond) \
((offs) | ((l) << 24) | (ARM_BR_TAG) | (cond << ARMCOND_SHIFT))
在 arm-codegen.h 中应该是
#define ARM_DEF_BR(offs, l, cond) \
((offs & 0x00FFFFFF) | ((l) << 24) |(ARM_BR_TAG) | (cond << ARMCOND_SHIFT))
因为 ARM 的分支偏移是 24 位,使用分支宏时负偏移量会扩展到 32 位,这会干扰操作码的其余部分。可能 Mono 使用向后分支的方式不同;我也只是粗略地看了看,无法确定。分支的使用方式是将分支指令本身视为偏移量 -2,下一条指令为 -1,前一条指令为 -3,以此类推。
一些更复杂的 ARM 基本操作不是作为宏实现的,而是作为函数实现的;例如 arm_mov_reg_imm32()
。在这些函数中,指令数组的索引指针是局部的,因此不会自动更新,但会返回其新值。我将这些函数和反汇编函数放入了一个名为 ArmJit.lib
的库中。要使用宏,您需要在源代码中包含相应的头文件,如果需要函数,则与该库链接。
为了使过程更容易理解,我制作了一个小型测试程序,它用 13 条 ARM 指令实现了简单的斐波那契基准测试。为了好玩,还将速度与常用的 C 实现进行了比较,结果 ARM 版本速度快了 30% 以上,但如前所述,与 C 竞争通常已不再是使用汇编的目的。这是程序,我将在下面对其进行注释
#include <windows.h>
#include <stdio.h>
#include <arm-codegen.h>
#include <arm-dis.h>
unsigned long fib_c(unsigned long n) {
if (n < 2)
return(1);
else
return(fib_c(n-2) + fib_c(n-1));
}
void setup_fib_jit (unsigned int *pins) {
/* label1 */
ARM_CMP_REG_IMM8 (pins, ARMREG_R0, 2); /* is n < 2 ? */
ARM_MOV_REG_IMM8_COND (pins, ARMREG_R0, 1, ARMCOND_LO); /* if yes return value is 1 */
ARM_MOV_REG_REG_COND (pins, ARMREG_PC, ARMREG_LR, ARMCOND_LO);
/* if yes return address in PC; */
/* and exit to main or previous recursive call */
ARM_PUSH2 (pins, ARMREG_R0, ARMREG_LR); /* save n and return address to the stack*/
ARM_SUB_REG_IMM8(pins, ARMREG_R0, ARMREG_R0, 2); /* n = n-2 */
ARM_BL (pins, -7); /* recurse to label1 for fib(n-2) */
ARM_LDR_IMM (pins, ARMREG_R1, ARMREG_SP, 0); /* load n from the stack */
ARM_STR_IMM (pins, ARMREG_R0, ARMREG_SP, 0); /* store result fib(n-2) */
ARM_SUB_REG_IMM8(pins, ARMREG_R0, ARMREG_R1, 1); /* n = n-1 */
ARM_BL (pins, -11); /* recurse to label1 for fib(n-1) */
ARM_POP2 (pins, ARMREG_R1, ARMREG_LR); /* pop result fib(n-2) and return address */
ARM_ADD_REG_REG (pins, ARMREG_R0, ARMREG_R0, ARMREG_R1); /* add both results */
ARM_MOV_REG_REG (pins, ARMREG_PC, ARMREG_LR);
/* return address in PC; */
/* and exit to main or previous recursive call */
}
int main (int argc, char *argv[]) {
unsigned int n, ins[500], *pins = ins;
unsigned long (*fib_jit)(int n) = (unsigned long (*)(int n)) ins;
unsigned long r1, r2, t0, t1, t2;
setup_fib_jit (pins);
_armdis_dump (stdout, ins, 56);
if (argc <= 2) {
if (argc == 1)
n=1;
else
n=atoi (argv[1]);
t0 = GetTickCount();
r1 = fib_c (n);
t1 = GetTickCount();
r2 = fib_jit (n);
t2 = GetTickCount();
}
else {
fprintf (stderr, "%s: Wrong number of arguments\n", argv[0]);
exit (-1);
}
printf (" fib_c(%d) result: %d\n\texcution time: %lf\n", n, r1, (t1-t0) / 1000.0);
printf ("fib_jit(%d) result: %d\n\texcution time: %lf\n", n, r2, (t2-t1) / 1000.0);
return 0;
}
使用宏需要大量的通用机器语言编程基础知识,以及一些 ARM 微处理器家族的基础知识。`setup_fib_jit()` 函数中的汇编指令在程序中已加注释,我在此不再赘述;这超出了本文的范围。将其与上面的 C 版本进行比较,很可能会对正在发生的事情有一个充分的了解;它基本上是对 C 算法的一对一翻译。我现在更愿意专注于设置代码并在实践中使用的宏。
首先,我们需要一个用于实际操作码指令的数组;在这种情况下,该数组名为 ins[]
。在 ARMV4 平台上,ARM 操作码每个是 32 位,在我使用的 Windows CE 版本中相当于无符号 int
。如果想更安全,可以使用 UINT32
作为数组的类型。还需要一个指向该数组的索引指针 *pins
,供宏使用以确定在哪里放置操作码。宏会自动更新此指针,因此我们可以使用它们而无需关心这一点。请注意,与正常编程相比,实际的汇编函数正在被“设置”而不是函数 *就是* 代码:实际代码不会在 setup_fib_jit()
中,而是在 ins[]
数组中!
用指令填充操作码数组后,我们必须通过将数组强制转换为函数变量来使自己能够跳转到它。这通过声明来完成
unsigned long (*fib_jit)(int n) = (unsigned long (*)(int n)) ins;
然后我们可以像普通函数一样调用 fib_jit()
。n
参数将作为 ARM 寄存器 0 传递给它,这在 ARM WinCE 平台上是 `__cdecl` 函数的第一个参数的常规做法。寄存器 0 也用于在函数退出时将返回值传递给 main()
。我认为 main()
的其余部分是不言自明的。基准测试的输入和输出来自/到控制台;运行此程序需要一个控制台。我个人使用 PocketConsole(我已将其适配为可在 VGA 中工作,因此项目中有 hidpi.res),它可以在互联网上找到。
关注点
一旦我开始,我就发现使用 Mono 代码出奇地容易且非常有趣。我一直计划为我的 Toshiba E800 Pocket PC 编写一个 C 解释器,也许这个工具能让我真正做到这一点。不过别指望它;最好自己写吧;)。
历史
原始 zip 文件和文章进行了一次小更新:测试程序的 ARM 函数的速度得到了提升。