一、基本介绍
AsmJit 是一个完整的 JIT ( just In Time, 运行时刻)的针对 C++ 语言的汇编器,可以生成兼容 x86 / x64 和 Aarch64 架构的原生代码,不仅支持整个x86/x64 的指令集(包括传统的 MMX 和最新的向量指令集),而且提供了一套可以在编译时刻进行语义检查的 API 。AsmJit 的使用也没有任何的限制,适用于多媒体,虚拟机的后端,远程代码生成等等。
二、特性
- 完全支持x86/x64指令集(包括MMX,SSEx,AVX1/2,BMI,XOP,FMA3和FMA4);
- 底层次和高层次的代码生成概念;
- 内置检测处理器特性功能;
- 实现虚拟内存的管理,类似于malloc和free;
- 强大的日志记录和错误处理能力;
- 体积小,可直接嵌入项目,编译后的体积在150至200kb之间;
- 独立性强,不需要依赖其他任何的库(包括STL和RTTI )。
三、代码生成
AsmJit 有着两种完全不同的代码生成概念,其不同点就在于生成代码的方式。一种是称为 Assembler 的低层次的代码生成方法,通过直接操作物理寄存器的方式生成代码,这种情况下 AsmJit 所做的工作只是简单的对指令进行编码,验证和重定位。而另外的一种是称为 Compiler 的高层次代码生成方法,Compiler 对使用虚拟寄存器的数量没有限制,这就类似于高级程序设计语言中的变量,可以极大的简化代码的生成过程。Compiler 在代码生成成功以后再给这些虚拟寄存器(变量)分配相应的物理寄存器,这就需要一些额外的消耗,因为 Compiler 必须为代码中的每一个结点(包括指令,函数声明,函数调用)生成额外的信息,用来对变量的生命周期进行分析或者将使用变量的代码转换成使用物理寄存器的汇编语句。
此外,Compiler 也需要了解函数原型和函数之间的调用约定。因此 Compiler 产生的代码具有类似于高级程序设计语言一样的函数原型,通过函数原型,Compiler 可以通过在函数头部和尾部插入额外的代码来达到被可以其他函数调用的目的。但是我们不能说明上面两种代码的生成方式孰优孰劣,因为利用 Assmebler 的方式可以充分控制代码的生成,而利用 Compiler 可以使得代码的生成更方便,可移植性更强,然而,当涉及到物理寄存器分配时,Compiler 有时效果并不太好,所以在已经进行分析的项目中,纯粹的 Assembler 方式的生成是首选。
四、配置和编译
AsmJit 在设计之初的目的就是为了嵌入到任何项目之中。但是我们可以使用一些宏定义来添加或者删除 AsmJit 库的某些特性。生成AsmJit 项目最直接的方法是使用 cmake 工具www.cmake.org ,但是如果只是在项目中嵌入 AsmJit 的源代码,可以通过编辑 asmjit /config.h 文件来打开或者关闭某些特定的特性,最简便的使用方法就是直接复制 AsmJit 的源代码到项目中,然后定义 “ASMJIT_STATIC” 宏。
4.1 生成类型
- ASMJIT_EMBED —— 如果在 cmake 中指定这个参数,AsmJit 则不会产生库,而是将代码直接嵌入到工程当中;
- ASMJIT_STATIC ——如果在 cmake 中指定这个参数,AsmJit 则会生成静态库,默认将不会导出符号; 如果都不指定,AsmJit 则会默认生成动态库文件。
4.2 生成模式
- ASMJIT_DEBUG —— 生成调试版本;
- ASMJIT_RELEASE —— 生成发行版本;
- ASMJIT_TRACE —— 生成的版本可以使用 trace 来调试 bug ,并且会使用 stdcout 将 AsmJit 运行的日志全部输出; 如果这些都没有定义的话,AsmJit 就会检测当前 IDE 编译时用到的宏定义,例如:Debug/Release 等等。
4.3 体系结构
- ASMJIT_BUILD_X86 —— 生成 X86 体系的后端;
- ASMJIT_BUILD_X64 —— 生成 x64 体系的后端;
- ASMJIT_BUILD_HOST —— 通过在编译时检测当前环境处理器的架构,生成和当前处理器架构一致的后端; 如果都不指定,则默认使用 ASMJIT_BUILD_HOST。
4.4 特性
- ASMJIT_DISABLE_COMPILER —— 禁用 Compiler 功能;
- ASMJIT_DISABLE_LOGGER —— 禁止产生日志;
- ASMJIT_DISABLE_NAMES —— 禁止使用字符串,如果使用则所有的指令和错误名称将变成无效。
五、使用
5.1 命名空间
AsmIit库使用的是全局命名空间 asmjit ,但是其中只包含一些基本的内容,而针对特定处理器的代码是用处理器、处理器的寄存器或者操作数作为前缀当成命名空间。例如针对 x86 和 x64 体系结构的类都会带有有 X86 的前缀。通过 kx86 枚举的寄存器和操作数在 X86 的命名空间下都是可访问的。虽然这种设计和 AsmJit 最初的版本不同,但是现在无疑是可移植性最好的。
5.2 运行时刻和代码生成器
要产生机器码就要用到AsmJit的两个类—— Runtime 和 CodeGen 。RunTime 会指定代码生成区域和存储区域; CodeGen 会指定代码的生成方式和产生整个程序的控制流。接下来的所有的例子都将使用 Compiler 来生成代码,并使用 JitRunTime 类来运行和存储。
5.3 指令操作数
操作数是处理器指令的一部分,指定了指令将要操作的数据,AsmJit 中有5种操作数
- Reg 物理寄存器,只被Assembler使用
- Var 虚拟寄存器(变量),只被 Compiler使用
- Mem 用于引用内存地址
- Label 用于引用代码地址
- Imm 直接用于编码的立即数本身
所有操作的基类都是 “Operand , 它包含使用所有类型的操作数的接口,并且大多数是通过值传递,而不是通过指针传递。
Reg , Var , Mem , Label 和 Imm 类都是继承自 Operand 并且提供不同的功能。依赖于处理器体系结构的操作数都会带有处理器结构作为前缀,例如 X86Reg , X86Mem 。大多数的处理器都会提供几种寄存器,
例如 X86/X64 体系结构下的 X86GpReg , X86MmReg , X86FpReg , X86XmmReg 和 X86YmmReg 寄存器加上一些额外的段寄存器和 ”rip” 寄存器。在使用代码生成器时,必须使用 AsmJit 的接口来显式地创建一些操作数。例如,labels 是用代码生成器类的 newLabel() 方法创建,而变量需要用针对不同体系结构的特定方法来创建,例如 newGpVar() , newMmVar() 和 newXmmVar() 。
5.4 函数原型
AsmJit 需要知道产生或调用的函数原型。AsmJit 包含类型和寄存器之间的映射关系,并且用来表示函数原型。函数生成器是一个模板类,通过使用 C/C++ 原生类型来生成可以描述函数参数和返回值的函数原型。它把 C/C++ 原生类型转化为 AsmJit 特定的标识符并且使这些标识符访问编译器。
5.5 实际使用
#include <asmjit/asmjit.h>#include <stdio.h>using namespace asmjit;// Signature of the generated function.typedef int (*Func)(void);int main(int argc, char* argv[]) { // Runtime designed for JIT - it holds relocated functions and controls their lifetime. JitRuntime rt; // Holds code and relocation information during code generation. CodeHolder code; // Code holder must be initialized before it can be used. The simples way to initialize // it is to use 'Environment' from JIT runtime, which matches the target architecture, // operating system, ABI, and other important properties. code.init(rt.environment(), rt.cpuFeatures()); // Emitters can emit code to CodeHolder - let's create 'x86::Assembler', which can emit // either 32-bit (x86) or 64-bit (x86_64) code. The following line also attaches the // assembler to CodeHolder, which calls 'code.attach(&a)' implicitly. x86::Assembler a(&code); // Use the x86::Assembler to emit some code to .text section in CodeHolder: a.mov(x86::eax, 1); // Emits 'mov eax, 1' - moves one to 'eax' register. a.ret(); // Emits 'ret' - returns from a function. // 'x86::Assembler' is no longer needed from here and can be destroyed or explicitly // detached via 'code.detach(&a)' - which detaches an attached emitter from code holder. // Now add the generated code to JitRuntime via JitRuntime::add(). This function would // copy the code from CodeHolder into memory with executable permission and relocate it. Func fn; Error err = rt.add(&fn, &code); // It's always a good idea to handle errors, especially those returned from the Runtime. if (err) { printf("AsmJit failed: %s\n", DebugUtils::errorAsString(err)); return 1; } // CodeHolder is no longer needed from here and can be safely destroyed. The runtime now // holds the relocated function, which we have generated, and controls its lifetime. The // function will be freed with the runtime, so it's necessary to keep the runtime around. // // Use 'code.reset()' to explicitly free CodeHolder's content when necessary. // Execute the generated function and print the resulting '1', which it moves to 'eax'. int result = fn(); printf("%d\n", result); // All classes use RAII, all resources will be released before **main()** returns, the // generated function can be, however, released explicitly if you intend to reuse or // keep the runtime alive, which you should in a production-ready code. rt.release(fn); return 0;}