原文: Getting Started with VIXL
在上一篇博客文章中,我们介绍了 VIXL - 一个 ARMv8 动态代码生成工具包。本博客文章将告诉您如何使用 VIXL 框架。我们将会看到如何设置 VIXL 汇编程序并生成某些代码。我们还将详细阐述由 VIXL 提供的某些有用功能,并了解如何在 VIXL 模拟器中运行生成的代码。
本指南中开发的示例的源代码可以在示例目录中找到 (examples/getting-started.cc)。
创建宏汇编程序和模拟器
首先,您需要确保包括汇编程序和模拟器的标头文件。您的源文件的开头应该具有以下几行:
#include "a64/simulator-a64.h"
#include "a64/macro-assembler-a64.h"
VIXL 的汇编程序将在运行时生成一些代码,该代码需要存储在缓冲区中。它必须足够大,以包含将要生成的所有指令和数据。在本指南中,我们将使用默认值 4096,但您可以随意将其更改为适合您需求的值。
#define BUF_SIZE (4096)
所有 VIXL 组件在 vixl 命名空间内进行声明,因此,为方便起见,让我们将其加入到该文件的开头:
using namespace vixl;
现在,我们已准备好创建并初始化不同的组件。
首先,我们需要分配代码缓冲区并创建使用该缓冲区的宏汇编程序对象。
byte assm_buf[BUF_SIZE];
MacroAssembler masm(assm_buf, BUF_SIZE);
我们还需要设置模拟器。该模拟器使用解码器对象来从代码缓冲区读取指令并对其进行解码。我们需要创建解码器,并将我们的模拟器绑定到该解码器。
Decoder decoder;
Simulator simulator(&decoder);
生成某些代码
现在,我们可以生成某些代码。宏汇编程序为您可以使用的所有指令提供方法。由于它是宏汇编程序,因此您让它生成的指令可能不会直接映射到单个硬件指令。相反,它可以产生具有相同效果的短指令序列。
例如,硬件添加指令只能采用可以选择移位 12 的 12 位立即数,但宏汇编程序可生成处理任何 64 位立即数的一个或多个指令。例如,Add(x0, x0, -1) 将变成 Sub(x0, x0, 1)。
在了解如何生成某些代码之前,让我们介绍一个简单而方便的宏:
#define __ masm->
它使我们能够写入 __ Mov(x0, 42) 而非 masm->Mov(x0, 42); 来生成代码。
现在,我们将写入 C++ 函数来生成我们的第一个汇编代码片段。
void GenerateDemoFunction(MacroAssembler *masm) {
__ Ldr(x1, 0x1122334455667788);
__ And(x0, x0, x1);
__ Ret();
}
生成的代码对应于具有以下 C 原型的函数:
uint64_t demo_function(uint64_t x);
该函数不执行任何有用操作。其将值 0x1122334455667788 加载到 x1 中,并使用该函数的参数(存储在 x0 中)执行按位与运算。该与运算的结果由 x0 中的函数返回。
现在,在我们的程序主要功能中,我们只需创建一个标签来表示汇编函数的入口点,并调用 GenerateDemoFunction 来生成代码。
Label demo_function;
masm.Bind(&demo_function);
GenerateDemoFunction(&masm);
masm.Finalize();
现在,我们将了解有关该示例中使用的几个有趣 VIXL 功能的更多信息。
标签
VIXL 的汇编程序提供了表示具有标签对象的标签的机制。它们简单易用:只需创建 C++ 对象并将其绑定到生成的指令流中的某个位置。
创建标签非常简单,因为您只需定义该变量并将其绑定到使用该宏汇编程序的某个位置。
Label my_label; // Create the label object.
__ Bind(&my_label); // Bind to the current location.
使用标签的分支的目标将会是其已经绑定的地址。例如,让我们考虑以下代码片段:
Label foo;
__ B(&foo); // Branch to foo.
__ Mov(x0, 42);
__ Bind(&foo); // Actual address of foo is here.
__ Mov(x1, 0xc001);
如果我们运行该代码片段,则 Mov(x0, 42) 将永远不会执行,因为该代码首先要跳转到 foo,其对应于 Mov(x1, 0xc001) 指令。
使用标签时,您需要知道它们仅用于本地分支,且应小心传送。这有两个原因:
文字池
ARMv8 指令长 32 位,因此在指令内进行编码的立即值的大小有限。如果您想加载大于该限制的常数,则您拥有两种可能方法:
__ Mov(x0, 0x1122334455667788);
前面的指令不合法,因为立即值太大。然而,VIXL 的宏汇编程序会自动将这一行改写成多个指令,以高效生成该值。
VIXL 还提供了实现这一目标的方法:
__ Ldr(x0, 0x1122334455667788);
汇编程序将在“文字池”中存储立即值,这是一组嵌入在该代码中的常数。VIXL 将在控制流中的自然中断后发出文字池,如无条件分支或返回指令。
文字池定期发出,以便它们在引用它们的指令范围内。但是,您可以使用 masm.EmitLiteralPool() 强制发出文字池
在模拟器中运行代码
现在,我们将查看如何使用模拟器运行我们之前生成的代码。
使用模拟器将某个值指定到寄存器。我们前面的代码示例将寄存器 x0 用作输入,因此,让我们设置该寄存器的值。
simulator.set_xreg(0, 0x8899aabbccddeeff);
现在,我们可以跳转到条目标签来执行该代码:
simulator.RunFrom(entry.target());
完成执行并返回模拟器时,您可以在执行后检查寄存器的值。例如:
printf("x0 = %" PRIx64 "\n", simulator.xreg(0));
本教程中的示例非常简单,因为我们的目标是要展示 VIXL 框架的基础。VIXL 示例 目录中有更复杂的代码示例,显示了宏汇编程序和 ARMv8 架构两者的其他功能。
ARM 软件工程师 Baptiste Afsa 几乎对每一个有关嵌入式系统技术的主题以及与硬件密切相关的任何种类的代码都感兴趣。他加入 ARM 从事 JIT 编译器方面的工作,非常着迷于动态代码生成问题,试图生成针对 ARM 平台优化的代码。