Basic Introduction
AsmJit is a complete JIT (just in time, runtime) assembler for C++. It can generate native code compatible with x86/x64 and AArch64 architectures. It not only supports the entire x86/x64 instruction set (including traditional MMX and the latest vector instruction sets), but also provides an API with compile-time semantic checks. AsmJit has no usage restrictions and is suitable for multimedia, VM backends, remote code generation, and more.
Features
- Full support for the x86/x64 instruction set (including MMX, SSE, AVX1/2, BMI, XOP, FMA3, and FMA4);
- Low-level and high-level code generation models;
- Built-in CPU feature detection;
- Virtual memory management similar to
mallocandfree; - Strong logging and error-handling capabilities;
- Small size, easy to embed in a project, with a compiled size between 150 KB and 200 KB;
- Highly self-contained, with no dependency on any other library (including STL and RTTI).
Code Generation
AsmJit provides two completely different code-generation models, and the difference lies in how code is emitted. One is the low-level model called Assembler, which generates code by directly operating on physical registers. In this mode, AsmJit only encodes instructions, validates them, and handles relocation. The other is the high-level model called Compiler. Compiler places no limit on the number of virtual registers, which is similar to variables in a high-level language and can greatly simplify code generation. After code generation succeeds, Compiler allocates physical registers for those virtual registers (variables). That does introduce some overhead, because Compiler must generate extra information for every node in the code, including instructions, function declarations, and function calls, so that it can analyze variable lifetimes or rewrite variable-using code into assembly that uses physical registers.
Compiler also needs to understand function prototypes and calling conventions. Therefore, code generated by Compiler has function prototypes similar to those in a high-level language. Through these prototypes, Compiler can insert extra code at function prologues and epilogues so that the generated code can be called by other functions. We cannot say that one of the two approaches is strictly better than the other: Assembler gives full control over code generation, while Compiler makes code generation easier and more portable. However, when physical register allocation is involved, Compiler is sometimes not ideal, so in projects that have already been analyzed, pure Assembler-based generation is often the preferred choice.
Configuration and Compilation
From the start, AsmJit was designed to be embedded in any project. We can use macros to add or remove certain AsmJit features. The most direct way to build AsmJit is with the cmake tool www.cmake.org, but if you only want to embed AsmJit source code in a project, you can enable or disable specific features by editing asmjit/config.h. The simplest way to use it is to copy AsmJit source code directly into your project and define the ASMJIT_STATIC macro.
Build Type
- ASMJIT_EMBED —— If this option is specified in CMake, AsmJit will not produce a library; instead, the code is embedded directly into the project.
- ASMJIT_STATIC —— If this option is specified in CMake, AsmJit will build a static library and will not export symbols by default.
- If neither is specified, AsmJit will build a dynamic library by default.
Build Mode
- ASMJIT_DEBUG —— Build a debug version;
- ASMJIT_RELEASE —— Build a release version;
- ASMJIT_TRACE —— Build a version that can be debugged with trace and will print all AsmJit runtime logs to stdcout;
- If none of these are defined, AsmJit will detect the macros used by the current IDE build, such as Debug/Release.
Architecture
- ASMJIT_BUILD_X86 —— Build the x86 backend;
- ASMJIT_BUILD_X64 —— Build the x64 backend;
- ASMJIT_BUILD_HOST —— Detect the current build host’s processor architecture at compile time and build the backend that matches it.
- If none are specified, ASMJIT_BUILD_HOST is used by default.
Features
- ASMJIT_DISABLE_COMPILER —— Disable Compiler functionality;
- ASMJIT_DISABLE_LOGGER —— Disable log generation;
- ASMJIT_DISABLE_NAMES —— Disable string names; if enabled, all instruction and error names become invalid.
Usage
Namespace
AsmJit uses the global namespace asmjit, but it only contains basic content. Code specific to a particular processor uses the processor, its registers, or its operands as a prefix, effectively forming a namespace. For example, classes for x86 and x64 architectures use the X86 prefix. Registers and operands exposed through the X86 namespace are accessible via the kx86 enum. Although this design differs from AsmJit’s earliest version, it is clearly the most portable one today.
Runtime and Code Generators
To generate machine code, AsmJit uses two classes: Runtime and CodeGen. Runtime specifies the code-generation and storage areas; CodeGen specifies how code is emitted and controls the control flow of the whole program. All examples below use Compiler to generate code and JitRuntime to run and store it.
Instruction Operands
Operands are part of processor instructions and specify the data the instruction will operate on. AsmJit has five kinds of operands:
- Reg — physical registers, used only by Assembler
- Var — virtual registers (variables), used only by Compiler
- Mem — used to reference memory addresses
- Label — used to reference code addresses
- Imm — the immediate value itself, used directly in encoding
The base class of all operations is Operand. It provides the interface for all operand types, and most operands are passed by value rather than by pointer.
Reg, Var, Mem, Label, and Imm all inherit from Operand and provide different functionality. Processor-architecture-specific operands are prefixed with the processor name, such as X86Reg and X86Mem. Most processors provide several register classes.
For example, under the X86/X64 architecture, there are X86GpReg, X86MmReg, X86FpReg, X86XmmReg, and X86YmmReg registers, plus some additional segment registers and the rip register. When using a code generator, you must explicitly create certain operands through AsmJit’s interface. For example, labels are created with the code generator class’s newLabel() method, while variables need to be created with architecture-specific methods such as newGpVar(), newMmVar(), and newXmmVar().
Function Prototypes
AsmJit needs to know the function prototype being generated or called. It includes mappings between types and registers to represent function prototypes. The function generator is a template class that uses native C/C++ types to generate prototypes describing function parameters and return values. It converts native C/C++ types into AsmJit-specific identifiers and makes those identifiers available to the compiler.
Practical Usage
1#include <asmjit/asmjit.h>
2#include <stdio.h>
3
4using namespace asmjit;
5
6// Signature of the generated function.
7typedef int (*Func)(void);
8
9int main(int argc, char* argv[]) {
10 // Runtime designed for JIT - it holds relocated functions and controls their lifetime.
11 JitRuntime rt;
12
13 // Holds code and relocation information during code generation.
14 CodeHolder code;
15
16 // Code holder must be initialized before it can be used. The simples way to initialize
17 // it is to use 'Environment' from JIT runtime, which matches the target architecture,
18 // operating system, ABI, and other important properties.
19 code.init(rt.environment(), rt.cpuFeatures());
20
21 // Emitters can emit code to CodeHolder - let's create 'x86::Assembler', which can emit
22 // either 32-bit (x86) or 64-bit (x86_64) code. The following line also attaches the
23 // assembler to CodeHolder, which calls 'code.attach(&a)' implicitly.
24 x86::Assembler a(&code);
25
26 // Use the x86::Assembler to emit some code to .text section in CodeHolder:
27 a.mov(x86::eax, 1); // Emits 'mov eax, 1' - moves one to 'eax' register.
28 a.ret(); // Emits 'ret' - returns from a function.
29
30 // 'x86::Assembler' is no longer needed from here and can be destroyed or explicitly
31 // detached via 'code.detach(&a)' - which detaches an attached emitter from code holder.
32
33 // Now add the generated code to JitRuntime via JitRuntime::add(). This function would
34 // copy the code from CodeHolder into memory with executable permission and relocate it.
35 Func fn;
36 Error err = rt.add(&fn, &code);
37
38 // It's always a good idea to handle errors, especially those returned from the Runtime.
39 if (err) {
40 printf("AsmJit failed: %s\n", DebugUtils::errorAsString(err));
41 return 1;
42 }
43
44 // CodeHolder is no longer needed from here and can be safely destroyed. The runtime now
45 // holds the relocated function, which we have generated, and controls its lifetime. The
46 // function will be freed with the runtime, so it's necessary to keep the runtime around.
47 //
48 // Use 'code.reset()' to explicitly free CodeHolder's content when necessary.
49
50 // Execute the generated function and print the resulting '1', which it moves to 'eax'.
51 int result = fn();
52 printf("%d\n", result);
53
54 // All classes use RAII, all resources will be released before **main()** returns, the
55 // generated function can be, however, released explicitly if you intend to reuse or
56 // keep the runtime alive, which you should in a production-ready code.
57 rt.release(fn);
58
59 return 0;
60}