Assembler interface and operands.


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.
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 (RegType) - Unique id that describes each possible register provided by the target architecture - for example X86 backend provides general purpose registers (GPB-LO, GPB-HI, GPW, GPD, and GPQ) and all types of other registers like K, MM, BND, XMM, YMM, ZMM, and TMM.
    • Register Group (RegGroup) - Groups multiple register types under a single group - for example all general-purpose registers (of all sizes) on X86 are part of RegGroup::kGp and all SIMD registers (XMM, YMM, ZMM) are part of RegGroup::kVec.
    • 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 `RegType::kX86_Gpq`.
// Examine `m`: Returns 10 (`r10`).
// Reconstruct `idx` stored in mem:
x86::Gp idx_2 = x86::Gp::fromTypeAndId(m.indexType(), m.indexId());
// 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).
// True, `op` is just a copy of `m`.
m == op;
// Static cast is fine and valid here.
// However, using `as<T>()` to cast to a derived type is preferred.<BaseMem>().addOffset(1);
// False, `op` now points to [rax + r10 + 2], which is not [rax + r10].
m == op;
// Emitting 'mov' - type safe way., m);
// Not possible, `mov` doesn't provide mov(x86::Gp, Operand) overload., 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;
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 address:
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






Typedef Documentation

typedef uint32_t RegMask

Register mask is a convenience typedef that describes a mask where each bit describes a physical register id in the same RegGroup.

At the moment 32 bits are enough as AsmJit doesn't support any architecture that would provide more than 32 registers for a register group.

Enumeration Type Documentation

OperandType : uint32_tenumstrong

Operand type used by Operand_.


Not an operand or not initialized.


Operand is a register.


Operand is a memory.


Operand is an immediate value.


Operand is a label.


Maximum value of OperandType.

RegType : uint8_tenumstrong

Register type.

Provides a unique type that can be used to identify a register or its view.


No register - unused, invalid, multiple meanings.


This is not a register type.

This value is reserved for a Label that used in BaseMem as a base.

Label tag is used as a sub-type, forming a unique signature across all operand types as 0x1 is never associated with any register type. This means that a memory operand's BASE register can be constructed from virtually any operand (register vs. label) by just assigning its type (register type or label-tag) and operand id.


Universal type describing program counter (PC) or instruction pointer (IP) register, if the target architecture actually exposes it as a separate register type, which most modern targets do.


8-bit low general purpose register (X86).


8-bit high general purpose register (X86).


16-bit general purpose register (X86).


32-bit general purpose register (X86|ARM).


64-bit general purpose register (X86|ARM).


8-bit view of a vector register (ARM).


16-bit view of a vector register (ARM).


32-bit view of a vector register (ARM).


64-bit view of a vector register (ARM).

This is never used for MMX registers on X86, MMX registers have its own category.

128-bit view of a vector register (X86|ARM).


256-bit view of a vector register (X86).


512-bit view of a vector register (X86).


1024-bit view of a vector register (future).


View of a vector register, which width is implementation specific (AArch64).


Mask register (X86).


Start of architecture dependent register types.


Instruction pointer (RIP), only addressable in x86::Mem in 64-bit targets.


Low GPB register (AL, BL, CL, DL, ...).


High GPB register (AH, BH, CH, DH only).


GPW register.


GPD register.


GPQ register (64-bit).


XMM register (SSE+).


YMM register (AVX+).


ZMM register (AVX512+).


K register (AVX512+).


MMX register.


Segment register (None, ES, CS, SS, DS, FS, GS).


Control register (CR).


Debug register (DR).


FPU (x87) register.


Bound register (BND).


TMM register (AMX_TILE)


Program pointer (PC) register (AArch64).


32-bit general purpose register (R or W).


64-bit general purpose register (X).


8-bit view of VFP/ASIMD register (B).


16-bit view of VFP/ASIMD register (H).


32-bit view of VFP/ASIMD register (S).


64-bit view of VFP/ASIMD register (D).


128-bit view of VFP/ASIMD register (Q|V).


Maximum value of RegType.

RegGroup : uint8_tenumstrong

Register group.

Provides a unique value that identifies groups of registers and their views.


General purpose register group compatible with all backends.


Vector register group compatible with all backends.

Describes X86 XMM|YMM|ZMM registers ARM/AArch64 V registers.


Extra virtual group #2 that can be used by Compiler for register allocation.


Extra virtual group #3 that can be used by Compiler for register allocation.


Program counter group.


Extra non-virtual group that can be used by registers not managed by Compiler.


K register group (KReg) - maps to RegGroup::kExtraVirt2 (X86, X86_64).


MMX register group (MM) - maps to RegGroup::kExtraVirt3 (X86, X86_64).


Instruction pointer (X86, X86_64).


Segment register group (X86, X86_64).


CR register group (X86, X86_64).


DR register group (X86, X86_64).


FPU register group (X86, X86_64).


BND register group (X86, X86_64).


TMM register group (X86, X86_64).


First group - only used in loops.


Last value of a virtual register that is managed by BaseCompiler.


Maximum value of RegGroup.

ImmType : uint32_tenumstrong

Type of the an immediate value.


Immediate is integer.


Immediate is a floating point stored as double-precision.

Function Documentation

template<typename T>
Imm imm(const T& val)constexprstaticnoexcept

Creates a new immediate operand.

Variable Documentation

const Operand Globals::noneconstexprstatic

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