Easy to use, extremely fast Runtime Assembler.
#include <stdio.h>
#include "asm_x64.h"
int main() {
char* str = "Hello World!";
x64 code = {
{ MOV, rax, imptr(puts) },
{ MOV, rcx, imptr(str) }, // RDI for System V
{ JMP, rax },
};
uint32_t len = 0;
uint8_t* assembled = x64as(code, sizeof(code) / sizeof(code[0]), &len);
if(!assembled) return fprintf(stderr, "%s", x64error(NULL)), 1;
x64exec(assembled, len)(); // Prints "Hello World!"
return 0;
}
Download
asm_x64.candasm_x64.hinto your project and just includeasm_x64.hto start assembling!
x64error(NULL).x64stringify(code, len).This library is useful for any code generated dynamically from user input. This includes:
I would highly recommend using something like example/vec.h (Arena library) to dynamically push code onto a single array throughout your application with very low latency. I show this off in example/bf_compiler.c!
Assembler is built in an optimized fashion where anything that can be precomputed, is precomputed.
In the above screenshot, it's shown that an optimized build can assemble most instructions in about 15 nanoseconds, which goes down to 30 for unoptimized builds.
This equates to about 100MB of code produced per second on my machine, with 1 core!
x64 is an array of x64Ins structs. The first member of the struct is op, or the operation, an enum defined by the asm_x64.h header. The other 4 members are x64Operand structs, which are just a combination of the type of operand with the value.
An example instruction mov rax, 0 would be written as:
x64 code = { MOV, rax, imm(0) };
Notice the use of rax and imm(0). All x86 registers like rax (including mms, ymms etc) are defined as macros with the type x64Operand. Other types of macros:
imm(), im8(), im16, im32(), im64() and imptr() for immediate values, another name for numbers embedded in the instruction encoding.mem(), m8(), m16(), m32(), m64(), m128(), m256() and m512() for memory addresses.rel() for control flow. Please read Relative Instruction References for more information.
rel(0) references the current instruction, so JMP, rel(0) jumps back to itself infinitely! 1 jumps to the next instruction and so on.{PREF66} and {PREFREX_W} are available.Let's start off with an example of lea rax, ds:[rax + 100 + rdx * 2] in chasm:
x64 code = { LEA, rax, mem($rax, 100, $rdx, 2, $ds) };
This is a variable length macro, with each argument being optional. Each of the register arguments of the mem() macro have to be preceeded with a $ prefix. Any 32 bit signed integer can be passed for the offset parameter, and only 1, 2, 4 and 8 are allowed in the 4th parameter, also called the "scale" parameter (ANY OTHER VALUE WILL GO TO 1, x86 limitation). The last parameter is a segment register, also preceeded with a $.
Other valid mem() syntax examples are:
mem($rax),mem($none, 0, $rdx, 8),mem($none, 0x60, $none, 1, $gs),mem($rip, 2) (RIP memory references only use the offset, index and scale do not work),mem($riprel, 2) (read more in Relative Instruction References),mem($rdx, 0, $ymm2, 4) (VSIB).mem():m8(), m16(), m32(), m64(), m128(), m256() and m512() specify the exact size of data referenced, erroring when that size isn't available with that instruction.mem() will not error when there's multiple sizes, instead using the smallest one available, but can cause bugs when it's not the size you intend.x64mem for more flexibility in size, like { a > b ? M8 : M16, x64mem(<normal mem() arguments>) }. Uppercase M<size> are enums for the size.mem() with FPU, as specifying the size doesn't mean much in the encoding. Exception to this exception are loading instructions like FLD where 32bit/64bit matters for integer size.[!important] Make sure to pass in $none for register parameters you are not using, as it will assume eax if you pass in 0! If you omit arguments though,
$noneis assumed :)
In assemblers, when you see $+n (. + n in GAS), it's a special syntax that lets you have a relative instruction based offset, as calculating the actual offset is impossible with a variable length encoding. Chasm also has an answer to this with rel() and mem($riprel). Here's an example of both:
x64 code = {
{ MOV, rax, imm(1) }, // 1 Iteration
{ LEA, rcx, mem($riprel, 2) }, // ━┓ "lea rcx, [$+2]"
{ PUSH, rcx }, // ┃ Pushes this address on the stack. Equivalent to "call $+2"
{ DEC, rax }, // ◄┛
{ JZ, rel(2) }, // ━┓ "jz $+2" (jumps out of the loop).
{ RET }, // ┃ Pops the pushed pointer off and jumps, basically "jmp $-2"
};
Simply, the number supplied is used to reference that many instructions ahead of the current instruction. 0 means the current instruction. { JMP, rel(0) } would halt the processor, so be careful.
More examples in example/bf_compiler.c.
[!Important] To get actual results with this syntax, you need to link your code with
x64as()!
x64 code = {
// Everyday // Intel Syntax/x64stringify result.
{ MOV, al, imm(10) }, // mov al, 10
{ LEA, rcx, mem($r9, 8) }, // lea rcx
{ PUSH, rax }, // push rax
{ CALL, r8 }, // call r8
{ JMP, rel(5) }, // jmp $+5
// Exotic
{ POP, fs, {PREF66} }, // o16 pop fs ; chasm doesn't stringify the o16 yet, but it of course, is encoded properly.
{ RET, {FAR} }, // ret ; Intel actually says it's retf, but chasm doesn't stringify {FAR} or {PREF66}
{ MOVQ, mm1, mem($rdx, 10) }, // movq mm1, [rdx + 10]
{ MOVUPD, xmm14, mem($rdx, 10) }, // movudp xmm14, [rdx + 10]
{ ENTER, imm(10), imm(1) }, // enter 10, 1
{ ADDSUBPS, xmm4, m128($rax) }, // addsubps xmm4, xmmword ptr [rax]
{ PEXT, eax, ebx, mem($ecx) }, // pext eax, ebx, [ecx] ; This is a VEX instruction but no v prefix.
{ RDSEED, eax }, // rdseed eax
{ CMPXCHG16B, mem($rdx) }, // cmpxchg16b [rdx]
{ PHMINPOSUW, xmm0, xmm0 }, // phminposuw xmm0, xmm0
{ PCMPESTRI, xmm5, xmm10, imm(0b01110000) }, // pcmpestri xmm5, xmm10, 0b01110000
{ FLD, m64($rax) }, // fld qword ptr rax
{ FMUL, st0, st1 }, // fmul st0, st1
// SIMD/VEX
{ VPERM2F128, ymm0, ymm2, m256($rdx, 5), imm(0x30) }, // vperm2f128 ymm0, ymm2, ymmword ptr [rdx + 5], 0x30
{ VBLENDVPD, ymm2, ymm1, mem($rax, 10, $rdx), ymm5 }, // vblendvpd ymm2, ymm1, [rax + 10 + rdx], ymm5
{ VCMPPS, xmm3, xmm4, mem($r8, 0, $rdx), imm(0xff) }, // vcmpss xmm3, xmm4, [r8 + rdx], 0xff
{ VGATHERQPD, ymm3, mem($rax, 10, $ymm5, 8), ymm2 }, // vgatherqpd ymm3, [rax + 0xA + ymm5 * 8], ymm2
};
uint8_t* x64as(x64 code, size_t len, uint32_t* outlen);$riprel and rel() syntax and returning the assembled code.x64error().
code, len and outlen for NULL pointers or 0s, ensuring your errors don't cause mayhem too!outlen.free().uint32_t x64emit(const x64Ins* ins, uint8_t* opcode_dest);opcode_dest.opcode_dest needs to be a buffer of at least 15 bytes to accomodate any/all x86 instructions.x64as().void (x64exec(void mem, uint32_t size))();x64exec_free().void x64exec_free(void* mem, uint32_t size);x64exec().[!note] Store the size of the memory you requested with
x64exec()as you will need to pass it in here, at least for Unix.
char* x64stringify(const x64 p, uint32_t num);x64error().mov [rax + rdx * 2], 20. Multiple instructions are preceeded with a tab.char* x64error(int* errcode);errcode is not NULL, it will be set to the error code.ymm(10, k1, z).If people seem to need support for any of these limitations, I will try my best to add them! In my personal use, I haven't needed them so I haven't gone through the effort.
I have tried very hard to add labels, and nothing seems to be elegant. I'm open to it if someone can draft a good plan for it! My goal is to support it fully without limiting strings to string literals only, if I were to support it at all. You can still see remnants of previous attempts in asm_x64.c.
Also, support for other instruction sets will come when I get to them, and I when get some good tables that give me the exact information I need! I currently use a modified table from StanfordPL/x64asm. Their table has some incorrect instructions, so I wouldn't suggest using that one for your own projects.
Chasm is dual licensed under the MIT Licence and Public Domain. You can choose the licence that suits your project the best. The MIT Licence is a permissive licence that is short and to the point. The Public Domain licence is a licence that makes the software available to the public for free and with no copyright.
A chasm is a deep ravine or hole formed through millenia of erosion and natural processes. This name struck a chord in my heart, as I have been working on this library for over a year, and it's the best way I know of getting low and deep into the heart of computing. I also loved that it had "asm" and "c" in it, which are big parts of what this library is about.
- asmjit/asmjit - A popular choice for C++ developers and many times more complex and feature rich than chasm.
- aengelke/fadec - Similar library to chasm but a completely different API that might be more flexible but harder for some.
- bitdefender/bddisasm - Fast, easy to use Disassembler library.
- garc0/CTAsm - Compile time assembler for C++ using only templates.
The first time I saw a library like this was when I found https://github.com/StanfordPL/x64asm. I loved the idea, but I couldn't use their library from C or Windows, so I took the liberty to redesign some of their library. I use their table to generate my own table in
asm_x64.cand while I haven't used any of their code, I did take inspiration from how they did instruction operands.
All source is in aqilc/rustscript. Tests testing all the features and operands are here.
C
94.4%
C++
5.6%
Easy to use, extremely fast Runtime Assembler.
#include <stdio.h>
#include "asm_x64.h"
int main() {
char* str = "Hello World!";
x64 code = {
{ MOV, rax, imptr(puts) },
{ MOV, rcx, imptr(str) }, // RDI for System V
{ JMP, rax },
};
uint32_t len = 0;
uint8_t* assembled = x64as(code, sizeof(code) / sizeof(code[0]), &len);
if(!assembled) return fprintf(stderr, "%s", x64error(NULL)), 1;
x64exec(assembled, len)(); // Prints "Hello World!"
return 0;
}
Download
asm_x64.candasm_x64.hinto your project and just includeasm_x64.hto start assembling!
x64error(NULL).x64stringify(code, len).This library is useful for any code generated dynamically from user input. This includes:
I would highly recommend using something like example/vec.h (Arena library) to dynamically push code onto a single array throughout your application with very low latency. I show this off in example/bf_compiler.c!
Assembler is built in an optimized fashion where anything that can be precomputed, is precomputed.
In the above screenshot, it's shown that an optimized build can assemble most instructions in about 15 nanoseconds, which goes down to 30 for unoptimized builds.
This equates to about 100MB of code produced per second on my machine, with 1 core!
x64 is an array of x64Ins structs. The first member of the struct is op, or the operation, an enum defined by the asm_x64.h header. The other 4 members are x64Operand structs, which are just a combination of the type of operand with the value.
An example instruction mov rax, 0 would be written as:
x64 code = { MOV, rax, imm(0) };
Notice the use of rax and imm(0). All x86 registers like rax (including mms, ymms etc) are defined as macros with the type x64Operand. Other types of macros:
imm(), im8(), im16, im32(), im64() and imptr() for immediate values, another name for numbers embedded in the instruction encoding.mem(), m8(), m16(), m32(), m64(), m128(), m256() and m512() for memory addresses.rel() for control flow. Please read Relative Instruction References for more information.
rel(0) references the current instruction, so JMP, rel(0) jumps back to itself infinitely! 1 jumps to the next instruction and so on.{PREF66} and {PREFREX_W} are available.Let's start off with an example of lea rax, ds:[rax + 100 + rdx * 2] in chasm:
x64 code = { LEA, rax, mem($rax, 100, $rdx, 2, $ds) };
This is a variable length macro, with each argument being optional. Each of the register arguments of the mem() macro have to be preceeded with a $ prefix. Any 32 bit signed integer can be passed for the offset parameter, and only 1, 2, 4 and 8 are allowed in the 4th parameter, also called the "scale" parameter (ANY OTHER VALUE WILL GO TO 1, x86 limitation). The last parameter is a segment register, also preceeded with a $.
Other valid mem() syntax examples are:
mem($rax),mem($none, 0, $rdx, 8),mem($none, 0x60, $none, 1, $gs),mem($rip, 2) (RIP memory references only use the offset, index and scale do not work),mem($riprel, 2) (read more in Relative Instruction References),mem($rdx, 0, $ymm2, 4) (VSIB).mem():m8(), m16(), m32(), m64(), m128(), m256() and m512() specify the exact size of data referenced, erroring when that size isn't available with that instruction.mem() will not error when there's multiple sizes, instead using the smallest one available, but can cause bugs when it's not the size you intend.x64mem for more flexibility in size, like { a > b ? M8 : M16, x64mem(<normal mem() arguments>) }. Uppercase M<size> are enums for the size.mem() with FPU, as specifying the size doesn't mean much in the encoding. Exception to this exception are loading instructions like FLD where 32bit/64bit matters for integer size.[!important] Make sure to pass in $none for register parameters you are not using, as it will assume eax if you pass in 0! If you omit arguments though,
$noneis assumed :)
In assemblers, when you see $+n (. + n in GAS), it's a special syntax that lets you have a relative instruction based offset, as calculating the actual offset is impossible with a variable length encoding. Chasm also has an answer to this with rel() and mem($riprel). Here's an example of both:
x64 code = {
{ MOV, rax, imm(1) }, // 1 Iteration
{ LEA, rcx, mem($riprel, 2) }, // ━┓ "lea rcx, [$+2]"
{ PUSH, rcx }, // ┃ Pushes this address on the stack. Equivalent to "call $+2"
{ DEC, rax }, // ◄┛
{ JZ, rel(2) }, // ━┓ "jz $+2" (jumps out of the loop).
{ RET }, // ┃ Pops the pushed pointer off and jumps, basically "jmp $-2"
};
Simply, the number supplied is used to reference that many instructions ahead of the current instruction. 0 means the current instruction. { JMP, rel(0) } would halt the processor, so be careful.
More examples in example/bf_compiler.c.
[!Important] To get actual results with this syntax, you need to link your code with
x64as()!
x64 code = {
// Everyday // Intel Syntax/x64stringify result.
{ MOV, al, imm(10) }, // mov al, 10
{ LEA, rcx, mem($r9, 8) }, // lea rcx
{ PUSH, rax }, // push rax
{ CALL, r8 }, // call r8
{ JMP, rel(5) }, // jmp $+5
// Exotic
{ POP, fs, {PREF66} }, // o16 pop fs ; chasm doesn't stringify the o16 yet, but it of course, is encoded properly.
{ RET, {FAR} }, // ret ; Intel actually says it's retf, but chasm doesn't stringify {FAR} or {PREF66}
{ MOVQ, mm1, mem($rdx, 10) }, // movq mm1, [rdx + 10]
{ MOVUPD, xmm14, mem($rdx, 10) }, // movudp xmm14, [rdx + 10]
{ ENTER, imm(10), imm(1) }, // enter 10, 1
{ ADDSUBPS, xmm4, m128($rax) }, // addsubps xmm4, xmmword ptr [rax]
{ PEXT, eax, ebx, mem($ecx) }, // pext eax, ebx, [ecx] ; This is a VEX instruction but no v prefix.
{ RDSEED, eax }, // rdseed eax
{ CMPXCHG16B, mem($rdx) }, // cmpxchg16b [rdx]
{ PHMINPOSUW, xmm0, xmm0 }, // phminposuw xmm0, xmm0
{ PCMPESTRI, xmm5, xmm10, imm(0b01110000) }, // pcmpestri xmm5, xmm10, 0b01110000
{ FLD, m64($rax) }, // fld qword ptr rax
{ FMUL, st0, st1 }, // fmul st0, st1
// SIMD/VEX
{ VPERM2F128, ymm0, ymm2, m256($rdx, 5), imm(0x30) }, // vperm2f128 ymm0, ymm2, ymmword ptr [rdx + 5], 0x30
{ VBLENDVPD, ymm2, ymm1, mem($rax, 10, $rdx), ymm5 }, // vblendvpd ymm2, ymm1, [rax + 10 + rdx], ymm5
{ VCMPPS, xmm3, xmm4, mem($r8, 0, $rdx), imm(0xff) }, // vcmpss xmm3, xmm4, [r8 + rdx], 0xff
{ VGATHERQPD, ymm3, mem($rax, 10, $ymm5, 8), ymm2 }, // vgatherqpd ymm3, [rax + 0xA + ymm5 * 8], ymm2
};
uint8_t* x64as(x64 code, size_t len, uint32_t* outlen);$riprel and rel() syntax and returning the assembled code.x64error().
code, len and outlen for NULL pointers or 0s, ensuring your errors don't cause mayhem too!outlen.free().uint32_t x64emit(const x64Ins* ins, uint8_t* opcode_dest);opcode_dest.opcode_dest needs to be a buffer of at least 15 bytes to accomodate any/all x86 instructions.x64as().void (x64exec(void mem, uint32_t size))();x64exec_free().void x64exec_free(void* mem, uint32_t size);x64exec().[!note] Store the size of the memory you requested with
x64exec()as you will need to pass it in here, at least for Unix.
char* x64stringify(const x64 p, uint32_t num);x64error().mov [rax + rdx * 2], 20. Multiple instructions are preceeded with a tab.char* x64error(int* errcode);errcode is not NULL, it will be set to the error code.ymm(10, k1, z).If people seem to need support for any of these limitations, I will try my best to add them! In my personal use, I haven't needed them so I haven't gone through the effort.
I have tried very hard to add labels, and nothing seems to be elegant. I'm open to it if someone can draft a good plan for it! My goal is to support it fully without limiting strings to string literals only, if I were to support it at all. You can still see remnants of previous attempts in asm_x64.c.
Also, support for other instruction sets will come when I get to them, and I when get some good tables that give me the exact information I need! I currently use a modified table from StanfordPL/x64asm. Their table has some incorrect instructions, so I wouldn't suggest using that one for your own projects.
Chasm is dual licensed under the MIT Licence and Public Domain. You can choose the licence that suits your project the best. The MIT Licence is a permissive licence that is short and to the point. The Public Domain licence is a licence that makes the software available to the public for free and with no copyright.
A chasm is a deep ravine or hole formed through millenia of erosion and natural processes. This name struck a chord in my heart, as I have been working on this library for over a year, and it's the best way I know of getting low and deep into the heart of computing. I also loved that it had "asm" and "c" in it, which are big parts of what this library is about.
- asmjit/asmjit - A popular choice for C++ developers and many times more complex and feature rich than chasm.
- aengelke/fadec - Similar library to chasm but a completely different API that might be more flexible but harder for some.
- bitdefender/bddisasm - Fast, easy to use Disassembler library.
- garc0/CTAsm - Compile time assembler for C++ using only templates.
The first time I saw a library like this was when I found https://github.com/StanfordPL/x64asm. I loved the idea, but I couldn't use their library from C or Windows, so I took the liberty to redesign some of their library. I use their table to generate my own table in
asm_x64.cand while I haven't used any of their code, I did take inspiration from how they did instruction operands.
All source is in aqilc/rustscript. Tests testing all the features and operands are here.
C
94.4%
C++
5.6%