Assembler

Assembler interface and operands.

Overview

AsmJit's Assembler is used to emit machine code directly into a CodeBuffer. In general, code generation with assembler requires the knowledge of the following:

  • BaseAssembler and architecture-specific assemblers:
  • Operand and its variations:
    • BaseReg - Base class for a register operand, inherited by:
      • x86::Reg - Register operand specific to X86 architecture.
    • BaseMem - Base class for a memory operand, inherited by:
      • x86::Mem - Memory operand specific to X86 architecture.
    • Imm - Immediate (value) operand.
    • Label - Label operand.
Note
Assembler examples use x86::Assembler as abstract interfaces cannot be used to generate code.

Operand Basics

Let's start with operands. Operand is a data structure that defines a data layout of any operand. It can be inherited, but any class inheriting it cannot add any members to it, only the existing layout can be reused. AsmJit allows to construct operands dynamically, to store them, and to query a complete information about them at run-time. Operands are small (always 16 bytes per Operand) and can be copied and passed by value. Please never allocate individual operands dynamically by using a new keyword - it would work, but then you would have to be responsible for deleting such operands. In AsmJit operands are always part of some other data structures like InstNode, which is part of Builder tool.

Operands contain only identifiers, but not pointers to any code-generation data. For example Label operand only provides label identifier, but not a pointer to LabelEntry structure. In AsmJit such IDs are used to link stuff together without having to deal with pointers.

AsmJit's operands all inherit from a base class called Operand. Operands have the following properties that are commonly accessible by getters and setters:

  • Operand - Base operand, which only provides accessors that are common to all operand types.
  • BaseReg - Describes either physical or virtual register. Physical registers have id that matches the target's machine id directly whereas virtual registers must be allocated into physical registers by a register allocator pass. Register operand provides:
    • Register Type - Unique id that describes each possible register provided by the target architecture - for example X86 backend provides x86::Reg::RegType, which defines all variations of general purpose registers (GPB-LO, GPB-HI, GPW, GPD, and GPQ) and all types of other registers like K, MM, BND, XMM, YMM, and ZMM.
    • Register Group - Groups multiple register types under a single group - for example all general-purpose registers (of all sizes) on X86 are part of x86::Reg::kGroupGp and all SIMD registers (XMM, YMM, ZMM) are part of x86::Reg::kGroupVec.
    • Register Size - Contains the size of the register in bytes. If the size depends on the mode (32-bit vs 64-bit) then generally the higher size is used (for example RIP register has size 8 by default).
    • Register Id - Contains physical or virtual id of the register.
  • BaseMem - Used to reference a memory location. Memory operand provides:
    • Base Register - A base register type and id (physical or virtual).
    • Index Register - An index register type and id (physical or virtual).
    • Offset - Displacement or absolute address to be referenced (32-bit if base register is used and 64-bit if base register is not used).
    • Flags that can describe various architecture dependent information (like scale and segment-override on X86).
  • Imm - Immediate values are usually part of instructions (encoded within the instruction itself) or data.
  • Label - used to reference a location in code or data. Labels must be created by the BaseEmitter or by CodeHolder. Each label has its unique id per CodeHolder instance.

Operand Manipulation

AsmJit allows to construct operands dynamically, to store them, and to query a complete information about them at run-time. Operands are small (always 16 bytes per Operand) and should be always copied (by value) if you intend to store them (don't create operands by using new keyword, it's not recommended). Operands are safe to be passed to memcpy() and memset(), which becomes handy when working with arrays of operands. If you set all members of an Operand to zero the operand would become NONE operand, which is the same as a default constructed Operand.

The example below illustrates how operands can be used and modified even without using any other code generation classes. The example uses X86 architecture-specific operands.

#include <asmjit/x86.h>
using namespace asmjit;
// Registers can be copied, it's a common practice.
x86::Gp dstRegByValue() { return x86::ecx; }
void usingOperandsExample(x86::Assembler& a) {
// Gets `ecx` register returned by a function.
x86::Gp dst = dstRegByValue();
// Gets `rax` register directly from the provided `x86` namespace.
x86::Gp src = x86::rax;
// Constructs `r10` dynamically.
x86::Gp idx = x86::gpq(10);
// Constructs [src + idx] memory address - referencing [rax + r10].
x86::Mem m = x86::ptr(src, idx);
// Examine `m`: Returns `x86::Reg::kTypeGpq`.
m.indexType();
// Examine `m`: Returns 10 (`r10`).
m.indexId();
// Reconstruct `idx` stored in mem:
// True, `idx` and idx_2` are identical.
idx == idx_2;
// Possible - op will still be the same as `m`.
Operand op = m;
// True (can be casted to BaseMem or architecture-specific Mem).
op.isMem();
// True, `op` is just a copy of `m`.
m == op;
// Static cast is fine and valid here.
static_cast<BaseMem&>(op).addOffset(1);
// However, using `as<T>()` to cast to a derived type is preferred.
op.as<BaseMem>().addOffset(1);
// False, `op` now points to [rax + r10 + 2], which is not [rax + r10].
m == op;
// Emitting 'mov' - type safe way.
a.mov(dst, m);
// Not possible, `mov` doesn't provide mov(x86::Gp, Operand) overload.
a.mov(dst, op);
// Type-unsafe, but possible.
a.emit(x86::Inst::kIdMov, dst, m);
// Also possible, `emit()` is typeless and can be used with raw Operand.
a.emit(x86::Inst::kIdMov, dst, op);
}

Some operands have to be created explicitly by emitters. For example labels must be created by BaseEmitter::newLabel(), which creates a label entry and returns a Label operand with the id that refers to it. Such label then can be used by emitters.

Memory Operands

Some architectures like X86 provide a complex memory addressing model that allows to encode addresses having a BASE register, INDEX register with a possible scale (left shift), and displacement (called offset in AsmJit). Memory address on X86 can also specify memory segment (segment-override in X86 terminology) and some instructions (gather / scatter) require INDEX to be a x86::Vec register instead of a general-purpose register.

AsmJit allows to encode and work with all forms of addresses mentioned and implemented by X86. In addition, it also allows to construct absolute 64-bit memory address operands, which is only allowed in one form of 'mov' instruction.

#include <asmjit/x86.h>
using namespace asmjit;
void testX86Mem() {
// Makes it easier to access x86 stuff...
using namespace asmjit::x86;
// BASE + OFFSET.
Mem a = ptr(rax); // a = [rax]
Mem b = ptr(rax, 15); // b = [rax + 15]
// BASE + INDEX << SHIFT - Shift is in BITS as used by X86!
Mem c = ptr(rax, rbx); // c = [rax + rbx]
Mem d = ptr(rax, rbx, 2); // d = [rax + rbx << 2]
Mem e = ptr(rax, rbx, 2, 15); // e = [rax + rbx << 2 + 15]
// BASE + VM (Vector Index) (encoded as MOD+VSIB).
Mem f = ptr(rax, xmm1); // f = [rax + xmm1]
Mem g = ptr(rax, xmm1, 2); // g = [rax + xmm1 << 2]
Mem h = ptr(rax, xmm1, 2, 15); // h = [rax + xmm1 << 2 + 15]
// Absolute adddress:
uint64_t addr = (uint64_t)0x1234;
Mem i = ptr(addr); // i = [0x1234]
Mem j = ptr(addr, rbx); // j = [0x1234 + rbx]
Mem k = ptr(addr, rbx, 2); // k = [0x1234 + rbx << 2]
// LABEL - Will be encoded as RIP (64-bit) or absolute address (32-bit).
Label L = ...;
Mem m = ptr(L); // m = [L]
Mem n = ptr(L, rbx); // n = [L + rbx]
Mem o = ptr(L, rbx, 2); // o = [L + rbx << 2]
Mem p = ptr(L, rbx, 2, 15); // p = [L + rbx << 2 + 15]
// RIP - 64-bit only (RIP can't use INDEX).
Mem q = ptr(rip, 24); // q = [rip + 24]
}

Memory operands can optionally contain memory size. This is required by instructions where the memory size cannot be deduced from other operands, like inc and dec on X86:

#include <asmjit/x86.h>
using namespace asmjit;
void testX86Mem() {
// The same as: dword ptr [rax + rbx].
x86::Mem a = x86::dword_ptr(rax, rbx);
// The same as: qword ptr [rdx + rsi << 0 + 1].
x86::Mem b = x86::qword_ptr(rdx, rsi, 0, 1);
}

Memory operands provide API that can be used to access its properties:

#include <asmjit/x86.h>
using namespace asmjit;
void testX86Mem() {
// The same as: dword ptr [rax + 12].
x86::Mem mem = x86::dword_ptr(rax, 12);
mem.hasBase(); // true.
mem.hasIndex(); // false.
mem.size(); // 4.
mem.offset(); // 12.
mem.setSize(0); // Sets the size to 0 (makes it sizeless).
mem.addOffset(-1); // Adds -1 to the offset and makes it 11.
mem.setOffset(0); // Sets the offset to 0.
mem.setBase(rcx); // Changes BASE to RCX.
mem.setIndex(rax); // Changes INDEX to RAX.
mem.hasIndex(); // true.
}
// ...

Making changes to memory operand is very comfortable when emitting loads and stores:

#include <asmjit/x86.h>
using namespace asmjit;
void testX86Mem(CodeHolder& code) {
x86::Assembler a(code); // Your initialized x86::Assembler.
x86::Mem mSrc = x86::ptr(eax); // Construct [eax] memory operand.
// One way of emitting bunch of loads is to use `mem.adjusted()`, which
// returns a new memory operand and keeps the source operand unchanged.
a.movaps(x86::xmm0, mSrc); // No adjustment needed to load [eax].
a.movaps(x86::xmm1, mSrc.adjusted(16)); // Loads from [eax + 16].
a.movaps(x86::xmm2, mSrc.adjusted(32)); // Loads from [eax + 32].
a.movaps(x86::xmm3, mSrc.adjusted(48)); // Loads from [eax + 48].
// ... do something with xmm0-3 ...
// Another way of adjusting memory is to change the operand in-place.
// If you want to keep the original operand you can simply clone it.
x86::Mem mDst = mSrc.clone(); // Clone mSrc.
a.movaps(mDst, x86::xmm0); // Stores xmm0 to [eax].
mDst.addOffset(16); // Adds 16 to `mDst`.
a.movaps(mDst, x86::xmm1); // Stores to [eax + 16] .
mDst.addOffset(16); // Adds 16 to `mDst`.
a.movaps(mDst, x86::xmm2); // Stores to [eax + 32].
mDst.addOffset(16); // Adds 16 to `mDst`.
a.movaps(mDst, x86::xmm3); // Stores to [eax + 48].
}

Assembler Examples

Classes

Functions

Variables

Function Documentation

template<typename T>
Imm imm(T val)constexprstaticconstexprnoexcept

Creates a new immediate operand.

Using imm(x) is much nicer than using Imm(x) as this is a template which can accept any integer including pointers and function pointers.

Variable Documentation

const Operand Globals::noneconstexprstaticconstexpr

A default-constructed operand of Operand_::kOpNone type.