
Assembleur compile-time C++2x en en-tête uniquement pour l'encodage d'instructions x86/x86-64
Une bibliothèque C++20 uniquement header pour l'encodage d'instructions x86/x86-64 à la compilation.
Ce projet est sous double licence Boost Software License 1.0 et MIT License. Vous pouvez choisir l'une ou l'autre.
#include "static_asm.hpp"
using namespace static_asm::x86::registers;
using namespace static_asm::x86::instructions;
// Build machine code at compile time
constexpr auto code = core::assemble(
mov(rax, 0x12345678), // mov rax, imm32
add(rax, rcx), // add rax, rcx
xor_(r8, r8), // xor r8, r8
call(rax), // call rax
ret() // ret
);
// code is std::array<uint8_t, N> - fully constexpr!
// Register to register
add(rax, rbx); // 48 01 D8
sub(ecx, edx); // 29 D1
and_(r8, r9); // 4D 21 C8
or_(rsi, rdi); // 48 09 FE
xor_(eax, eax); // 31 C0 (common idiom to zero a register)
cmp(rax, rcx); // 48 39 C8
// Register with immediate
add(rax, 0x10); // 48 83 C0 10 (sign-extended imm8)
add(rax, 0x10000); // 48 05 00 00 01 00 (imm32)
sub(ecx, 100); // 83 E9 64
and_(rdx, 0xFF); // 48 83 E2 FF
// With memory operands
add(eax, dword_ptr(rbx)); // 03 03
add(rax, qword_ptr(rcx + 0x10)); // 48 03 41 10
sub(dword_ptr(rsp + 0x20), eax); // 29 44 24 20
// Register to register
mov(rax, rbx); // 48 89 D8
mov(eax, ecx); // 89 C8
mov(r8, r9); // 4D 89 C8
// Immediate to register
mov(rax, 0x12345678); // 48 C7 C0 78 56 34 12
mov(eax, 0xDEADBEEF); // B8 EF BE AD DE
// Memory operations
mov(rax, qword_ptr(rbx)); // 48 8B 03
mov(eax, dword_ptr(rcx + 0x10)); // 8B 41 10
mov(qword_ptr(rsp + 0x8), rax); // 48 89 44 24 08
mov(dword_ptr(rbp - 0x20), 0x100); // C7 45 E0 00 01 00 00
// Zero/sign extension
movzx(eax, bl); // 0F B6 C3 (zero-extend byte to dword)
movzx(rax, bx); // 48 0F B7 C3 (zero-extend word to qword)
movsx(eax, cl); // 0F BE C1 (sign-extend byte to dword)
movsx(rax, dx); // 48 0F BF C2 (sign-extend word to qword)
movsxd(rax, ecx); // 48 63 C1 (sign-extend dword to qword)
// Load effective address
lea(rax, qword_ptr(rbx + rcx * s4)); // 48 8D 04 8B
lea(rax, qword_ptr(rbx + rcx * s8 + 0x10)); // 48 8D 44 CB 10
// Exchange
xchg(rax, rbx); // 48 87 D8
// [base + index*scale]
mov(eax, dword_ptr(rbx + rcx * s1)); // 8B 04 0B
mov(eax, dword_ptr(rbx + rcx * s2)); // 8B 04 4B
mov(eax, dword_ptr(rbx + rcx * s4)); // 8B 04 8B
mov(eax, dword_ptr(rbx + rcx * s8)); // 8B 04 CB
// [base + index*scale + displacement]
mov(rax, qword_ptr(rbx + rcx * s4 + 0x10)); // 48 8B 44 8B 10
mov(rax, qword_ptr(r12 + r13 * s8 + 0x1000)); // 4B 8B 84 EC 00 10 00 00
// Store to SIB address
mov(dword_ptr(rax + rdx * s4), ecx); // 89 0C 90
mov(qword_ptr(rbx + rsi * s8 + 0x20), rax); // 48 89 44 F3 20
// LEA with SIB (useful for address calculations)
lea(rax, qword_ptr(rbx + rcx * s4)); // 48 8D 04 8B
lea(rax, qword_ptr(rdi + rsi * s8 + 0x100)); // 48 8D 84 F7 00 01 00 00
// Shift by 1
shl(eax, 1); // D1 E0
shr(rax, 1); // 48 D1 E8
sar(ecx, 1); // D1 F9
// Shift by immediate
shl(eax, 4); // C1 E0 04
shr(rax, 8); // 48 C1 E8 08
sar(rdx, 16); // 48 C1 FA 10
// Shift by CL register
shl(eax, cl); // D3 E0
shr(rax, cl); // 48 D3 E8
// Rotate
rol(eax, 1); // D1 C0
ror(rax, 8); // 48 C1 C8 08
rcl(ecx, cl); // D3 D1
rcr(rdx, 1); // 48 D1 DA
// Single operand (result in rdx:rax)
mul(rbx); // 48 F7 E3 (unsigned: rdx:rax = rax * rbx)
imul(rcx); // 48 F7 E9 (signed: rdx:rax = rax * rcx)
div(rbx); // 48 F7 F3 (unsigned: rax = rdx:rax / rbx, rdx = remainder)
idiv(rcx); // 48 F7 F9 (signed division)
// Two-operand IMUL (dest = dest * src)
imul(rax, rbx); // 48 0F AF C3
imul(ecx, edx); // 0F AF CA
// Three-operand IMUL (dest = src * imm)
imul(rax, rbx, 10); // 48 6B C3 0A
imul(ecx, edx, 1000); // 69 CA E8 03 00 00
// Unconditional jumps
jmp(0x10); // EB 10 (short, 8-bit offset)
jmp(0x1000); // E9 00 10 00 00 (near, 32-bit offset)
jmp(rax); // FF E0 (indirect)
jmp(here); // EB FE (jmp $, infinite loop)
// Conditional jumps (8-bit offset)
jz(0x10); // 74 10
jnz(0x20); // 75 20
jb(0x08); // 72 08 (below/carry)
jae(0x08); // 73 08 (above or equal/no carry)
jl(0x10); // 7C 10 (less than, signed)
jge(0x10); // 7D 10 (greater or equal, signed)
// Conditional jumps (32-bit offset for longer branches)
jz_near(0x10000); // 0F 84 00 00 01 00
jnz_near(0x20000); // 0F 85 00 00 02 00
// Call and return
call(rax); // FF D0 (indirect call)
call(0x100); // E8 00 01 00 00 (relative call)
ret(); // C3
ret(0x10); // C2 10 00 (return and pop 16 bytes)
// Move if condition is true (no branch penalty!)
cmovz(rax, rbx); // 48 0F 44 C3 (move if zero)
cmovnz(eax, ecx); // 0F 45 C1 (move if not zero)
cmovl(rax, rdx); // 48 0F 4C C2 (move if less, signed)
cmovge(ecx, esi); // 0F 4D CE (move if greater or equal, signed)
cmovb(rax, rbx); // 48 0F 42 C3 (move if below, unsigned)
cmovae(edx, edi); // 0F 43 D7 (move if above or equal, unsigned)
// With memory source
cmovz(rax, qword_ptr(rbx)); // 48 0F 44 03
cmovnz(eax, dword_ptr(rcx + 0x10)); // 0F 45 41 10
// Bit test
bt(eax, 5); // 0F BA E0 05
bt(rax, rbx); // 48 0F A3 D8
// Bit test and set/reset/complement
bts(eax, 10); // 0F BA E8 0A (test and set)
btr(rax, rcx); // 48 0F B3 C8 (test and reset)
btc(edx, 3); // 0F BA FA 03 (test and complement)
// Bit scan
bsf(eax, ecx); // 0F BC C1 (scan forward for first 1)
bsr(rax, rbx); // 48 0F BD C3 (scan reverse for first 1)
// Population count and leading/trailing zeros
popcnt(eax, ecx); // F3 0F B8 C1
lzcnt(rax, rbx); // F3 48 0F BD C3
tzcnt(eax, edx); // F3 0F BC C2
// Byte swap
bswap(eax); // 0F C8 (reverse byte order)
bswap(rax); // 48 0F C8
// Basic string ops (operate on [rsi] and/or [rdi])
movsb(); // A4 (move byte [rsi] -> [rdi])
movsw(); // 66 A5
movsd(); // A5
movsq(); // 48 A5
cmpsb(); // A6 (compare [rsi] with [rdi])
stosb(); // AA (store al -> [rdi])
lodsb(); // AC (load [rsi] -> al)
scasb(); // AE (compare al with [rdi])
// With REP prefix (repeat rcx times)
rep_movsb(); // F3 A4 (memcpy)
rep_movsq(); // F3 48 A5 (fast memcpy, 8 bytes at a time)
rep_stosb(); // F3 AA (memset)
rep_stosq(); // F3 48 AB
// With REPE/REPNE (repeat while equal/not equal)
repe_cmpsb(); // F3 A6 (compare strings until mismatch)
repne_scasb(); // F2 AE (scan for byte in string)
push(rax); // 50
push(rbx); // 53
push(r8); // 41 50
push(0x10); // 6A 10 (push imm8)
push(0x1000); // 68 00 10 00 00 (push imm32)
pop(rax); // 58
pop(rbx); // 5B
pop(r15); // 41 5F
// System calls
syscall_(); // 0F 05 (64-bit syscall)
sysenter(); // 0F 34
sysexit(); // 0F 35
// Interrupts
int3(); // CC (breakpoint)
int_(0x80); // CD 80 (Linux 32-bit syscall)
int_(0x21); // CD 21 (DOS interrupt)
// CPU info
cpuid(); // 0F A2
rdtsc(); // 0F 31
rdtscp(); // 0F 01 F9
// Privilege
cli(); // FA (clear interrupts)
sti(); // FB (set interrupts)
hlt(); // F4 (halt)
// Interrupt return
iret(); // CF (16-bit)
iretd(); // CF (32-bit)
iretq(); // 48 CF (64-bit)
Utilisez core::assemble() pour concaténer les tableaux d'octets d'instructions :
constexpr auto prologue = core::assemble(
push(rbp),
mov(rbp, rsp),
sub(rsp, 0x20)
);
constexpr auto epilogue = core::assemble(
add(rsp, 0x20),
pop(rbp),
ret()
);
// Combine them
constexpr auto full_function = core::assemble(prologue, epilogue);
Ajoutez à votre CMakeLists.txt :
include(FetchContent)
FetchContent_Declare(
static_asm
GIT_REPOSITORY https://github.com/mahmoudimus/static_asm.git
GIT_TAG v1.0.0 # or specific commit
)
FetchContent_MakeAvailable(static_asm)
target_link_libraries(your_target PRIVATE static_asm::static_asm)
Clonez ou ajoutez en tant que sous-module git :
git submodule add https://github.com/mahmoudimus/static_asm.git external/static_asm
Puis dans votre CMakeLists.txt :
add_subdirectory(external/static_asm)
target_link_libraries(your_target PRIVATE static_asm::static_asm)
Lorsqu'il est inclus via add_subdirectory ou FetchContent, seule la cible de bibliothèque d'interface static_asm::static_asm est ajoutée à votre projet. Les tests et exemples ne sont pas construits sauf s'ils sont explicitement activés avec -DSTATIC_ASM_BUILD_TESTS=ON.
Téléchargez static_asm.hpp depuis la page des releases et incluez-le directement :
#include "static_asm.hpp"
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --install build --prefix /usr/local
Utilisez ensuite find_package :
find_package(static_asm REQUIRED)
target_link_libraries(your_target PRIVATE static_asm::static_asm)
# Configuration
cmake -B build -DCMAKE_BUILD_TYPE=Release
# Construction
cmake --build build
# Exécution des tests
ctest --test-dir build --output-on-failure
# Construction avec exemples (Clang uniquement, utilise l'assembleur inline)
cmake -B build -DCMAKE_BUILD_TYPE=Release -DSTATIC_ASM_BUILD_EXAMPLES=ON
Support de plate-forme :
Note : La fonctionnalité core::emit() d'assembleur inline nécessite Clang avec l'optimisation -O2.
| Catégorie | Instructions |
|---|---|
| ALU | ADD, ADC, SUB, SBB, AND, OR, XOR, CMP, TEST |
| Unary | INC, DEC, NEG, NOT |
| Multiply/Divide | MUL, IMUL (formes à 1/2/3 opérandes), DIV, IDIV |
| Data Movement | MOV, MOVABS, MOVZX, MOVSX, MOVSXD, LEA, XCHG, PUSH, POP |
| Shift/Rotate | SHL, SHR, SAL, SAR, ROL, ROR, RCL, RCR |
| Control Flow | JMP, CALL, RET, RETF |
| Conditional Jumps | JZ/JE, JNZ/JNE, JB/JC, JNB/JNC, JBE/JNA, JNBE/JA, JL, JNL, JLE, JNLE, JO, JNO, JS, JNS, JP, JNP (décalages 8 et 32 bits) |
| Conditional Moves | CMOVA, CMOVAE, CMOVB, CMOVBE, CMOVE, CMOVG, CMOVGE, CMOVL, CMOVLE, CMOVNE, CMOVNO, CMOVNP, CMOVNS, CMOVO, CMOVP, CMOVS |
| Bit Operations | BT, BTC, BTR, BTS |
| Bit Scan/Count | BSF, BSR, POPCNT, LZCNT, TZCNT, BSWAP |
| String Operations | MOVSB/W/D/Q, CMPSB/W/D/Q, LODSB/W/D/Q, STOSB/W/D/Q, SCASB/W/D/Q (avec préfixes REP/REPE/REPNE) |
| System | SYSCALL, SYSENTER, SYSEXIT, INT, INT3, IRET/D/Q, CLI, STI, HLT, CPUID, RDTSC, RDTSCP |
| Misc | NOP, UD2 |
Support des opérandes :
[base + index*échelle + déplacement] avec facteurs d'échelle 1, 2, 4, 8Note : Pas encore d'extensions SIMD/AVX.
Ce projet utilise uv pour les outils Python (génération de code, amalgamation en header unique).
# Installer uv (si ce n'est pas déjà fait)
curl -LsSf https://astral.sh/uv/install.sh | sh
# Vérifier l'installation
uv --version
# Tous les scripts Python peuvent être exécutés directement avec uv (les dépendances sont gérées automatiquement)
uv run scripts/gen_from_x86ref.py --help
uv run scripts/amalgamate.sh
Outils requis :
Outils optionnels pour le développement :
| Outil | Objectif | Installation |
|---|---|---|
| clang-format | Mise en forme du code | Via LLVM ou le gestionnaire de paquets système |
| clang-tidy | Analyse statique | Via LLVM ou le gestionnaire de paquets système |
Le projet inclut un générateur qui analyse la base de données XML x86reference :
# Afficher le résumé de la base de données d'instructions
uv run scripts/gen_from_x86ref.py
# Afficher les détails d'une instruction spécifique
uv run scripts/gen_from_x86ref.py -i lea
uv run scripts/gen_from_x86ref.py -i imul
# Générer les fichiers de la base de données d'instructions
uv run scripts/gen_from_x86ref.py --generate-db
# Générer un fichier de test exhaustif
uv run scripts/gen_from_x86ref.py --generate-tests
instdb, prefix_db, prefix_0fdb)encoder.hpp ou étendre un encodeur existantAucun outil automatique ne peut convertir de manière fiable une bibliothèque C++ multi-fichiers arbitraire en une version header-only propre sans une certaine préparation manuelle. Les techniques suivantes aident à garantir que votre bibliothèque peut être amalgamée avec succès en un seul fichier header tout en restant correcte, maintenable et conforme aux normes.
Note : Ce projet utilise quom pour l'amalgamation et clang-tidy avec la vérification
google-build-using-namespacepour automatiser l'application de ces règles. quom gère également partiellement la technique #4 (marqueurs inline) via ses capacités de traitement.
using namespace dans les fichiers sourcesusing namespace à la portée du fichier dans les fichiers .cpp est dangereux lorsque ces fichiers sont ensuite inclus dans un header — cela pollue l'espace de noms global pour chaque unité de traduction qui inclut votre header.
Modèles recommandés à la place :
// Préféré : envelopper l'implémentation dans un espace de noms
namespace MyLib {
Foo::Foo() {
// ...
}
}
ou
// Qualification explicite (plus verbeuse mais très claire)
MyLib::Foo::Foo() {
// ...
}
Les API publiques doivent vivre dans l'espace de noms principal. Tout ce qui n'est pas destiné aux utilisateurs finaux doit être caché dans un espace de noms imbriqué tel que detail ou impl.
Conventions courantes :
namespace MyLib {
namespace detail { // très utilisé
// classes internes, fonctions, etc.
}
}
ou
namespace MyLib::impl { // plus court, également courant
// détails d'implémentation internes
}
C++17 et versions ultérieures prennent en charge les définitions d'espaces de noms imbriqués en ligne, ce qui est plus propre :
namespace MyLib::detail {
class InternalHelper { /* ... */ };
}
static inlineLes variables static à portée de fichier définies dans des fichiers .cpp deviennent problématiques dans un monde header-only (définitions multiples, violations ODR).
Solution moderne (C++17+) :
// Avant (dans .cpp)
namespace MyLib {
static int s_counter = 0;
int next_id() {
return ++s_counter;
}
}
// Après (sûr pour un header)
namespace MyLib::detail {
struct Globals {
static inline int counter = 0;
};
}
inline int MyLib::next_id() {
return ++detail::Globals::counter;
}
La variable static inline est garantie d'avoir une seule définition même lorsqu'elle est incluse plusieurs fois.
inlineToute fonction, fonction membre, constructeur ou destructeur dont le corps apparaît dans le header (mais pas à l'intérieur de la définition de classe) doit être marqué inline pour éviter les violations de la règle de définition unique (ODR).
Étant donné que de nombreux scripts d'amalgamation sont purement textuels et ne parsent pas la sémantique C++, une convention courante est d'utiliser une macro d'espace réservé (par exemple inline_t) pendant le développement :
// MyLib.h (ou header commun)
#define inline_t /* vide pendant les constructions normales */
// MyLib.cpp (pendant le développement)
namespace MyLib::detail {
inline_t void Helper::do_work() {
// implémentation
}
}
Pendant l'amalgamation, l'outil remplace inline_t par inline :
// Après amalgamation/transformation
inline void MyLib::detail::Helper::do_work() {
// ...
}
Vous pouvez choisir n'importe quel nom de macro que vous préférez (par exemple MYLIB_INLINE, INLINE_IMP, etc.) et configurer votre script d'amalgamation en conséquence.
Note : Des outils comme quom peuvent partiellement automatiser cela en comprenant les sémantiques d'inclusion C++ et en gérant correctement les définitions de fonctions pendant l'amalgamation, réduisant ainsi le besoin de marqueurs
inlinemanuels dans de nombreux cas.
using namespace … à la portée d'espace de noms/fichier dans les fichiers d'implémentation.detail / impl).static à portée de fichier par des membres static inline d'une structure/classe.inline (remplacée pendant l'amalgamation).Suivre ces quatre pratiques rend la transition vers une distribution header unique beaucoup plus fluide et bien moins sujette aux erreurs — même avec des outils d'amalgamation purement textuels.
Ce projet est basé sur cx_assembler par Midi12. La bibliothèque originale a fourni les fondations pour l'encodage d'instructions assembleur x86 à la compilation en C++.