
Header-only C++2x Kompilierzeit-Assembler für x86/x86-64 Instruktionscodierung
Eine Nur-Header-C++20-Bibliothek für die Kompilierzeitkodierung von x86/x86-64-Befehlen.
Dieses Projekt ist dual lizenziert unter der Boost Software License 1.0 und der MIT-Lizenz. Sie können eine der beiden Lizenzen wählen.
#include "static_asm.hpp"
using namespace static_asm::x86::registers;
using namespace static_asm::x86::instructions;
// Maschinencode zur Kompilierzeit erstellen
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 ist std::array<uint8_t, N> - vollständig constexpr!
// Register zu 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 (häufiger Ausdruck zum Nullsetzen eines Registers)
cmp(rax, rcx); // 48 39 C8
// Register mit sofortigem Wert
add(rax, 0x10); // 48 83 C0 10 (vorzeichenerweiterter imm8)
add(rax, 0x10000); // 48 05 00 00 01 00 (imm32)
sub(ecx, 100); // 83 E9 64
and_(rdx, 0xFF); // 48 83 E2 FF
// Mit Speicheroperanden
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 zu Register
mov(rax, rbx); // 48 89 D8
mov(eax, ecx); // 89 C8
mov(r8, r9); // 4D 89 C8
// Sofortiger Wert zu Register
mov(rax, 0x12345678); // 48 C7 C0 78 56 34 12
mov(eax, 0xDEADBEEF); // B8 EF BE AD DE
// Speicheroperationen
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
// Null-/Vorzeichenerweiterung
movzx(eax, bl); // 0F B6 C3 (Nullerweiterung Byte zu Dword)
movzx(rax, bx); // 48 0F B7 C3 (Nullerweiterung Word zu Qword)
movsx(eax, cl); // 0F BE C1 (Vorzeichenerweiterung Byte zu Dword)
movsx(rax, dx); // 48 0F BF C2 (Vorzeichenerweiterung Word zu Qword)
movsxd(rax, ecx); // 48 63 C1 (Vorzeichenerweiterung Dword zu Qword)
// Effektive Adresse laden
lea(rax, qword_ptr(rbx + rcx * s4)); // 48 8D 04 8B
lea(rax, qword_ptr(rbx + rcx * s8 + 0x10)); // 48 8D 44 CB 10
// Austausch
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
// Speichern auf SIB-Adresse
mov(dword_ptr(rax + rdx * s4), ecx); // 89 0C 90
mov(qword_ptr(rbx + rsi * s8 + 0x20), rax); // 48 89 44 F3 20
// LEA mit SIB (nützlich für Adressberechnungen)
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
// Verschiebung um 1
shl(eax, 1); // D1 E0
shr(rax, 1); // 48 D1 E8
sar(ecx, 1); // D1 F9
// Verschiebung um sofortigen Wert
shl(eax, 4); // C1 E0 04
shr(rax, 8); // 48 C1 E8 08
sar(rdx, 16); // 48 C1 FA 10
// Verschiebung um CL-Register
shl(eax, cl); // D3 E0
shr(rax, cl); // 48 D3 E8
// Rotation
rol(eax, 1); // D1 C0
ror(rax, 8); // 48 C1 C8 08
rcl(ecx, cl); // D3 D1
rcr(rdx, 1); // 48 D1 DA
// Einzeloperand (Ergebnis 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 = Rest)
idiv(rcx); // 48 F7 F9 (signed division)
// Zweiopranden-IMUL (dest = dest * src)
imul(rax, rbx); // 48 0F AF C3
imul(ecx, edx); // 0F AF CA
// Dreioperanden-IMUL (dest = src * imm)
imul(rax, rbx, 10); // 48 6B C3 0A
imul(ecx, edx, 1000); // 69 CA E8 03 00 00
// Unbedingte Sprünge
jmp(0x10); // EB 10 (kurz, 8-Bit-Offset)
jmp(0x1000); // E9 00 10 00 00 (nah, 32-Bit-Offset)
jmp(rax); // FF E0 (indirekt)
jmp(here); // EB FE (jmp $, Endlosschleife)
// Bedingte Sprünge (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)
// Bedingte Sprünge (32-Bit-Offset für längere Verzweigungen)
jz_near(0x10000); // 0F 84 00 00 01 00
jnz_near(0x20000); // 0F 85 00 00 02 00
// Aufruf und Rückkehr
call(rax); // FF D0 (indirekter Aufruf)
call(0x100); // E8 00 01 00 00 (relativer Aufruf)
ret(); // C3
ret(0x10); // C2 10 00 (Rückkehr und 16 Bytes vom Stack holen)
// Verschieben, wenn Bedingung wahr ist (keine Verzweigungsstrafe!)
cmovz(rax, rbx); // 48 0F 44 C3 (verschieben, wenn null)
cmovnz(eax, ecx); // 0F 45 C1 (verschieben, wenn nicht null)
cmovl(rax, rdx); // 48 0F 4C C2 (verschieben, wenn kleiner, signed)
cmovge(ecx, esi); // 0F 4D CE (verschieben, wenn größer oder gleich, signed)
cmovb(rax, rbx); // 48 0F 42 C3 (verschieben, wenn darunter, unsigned)
cmovae(edx, edi); // 0F 43 D7 (verschieben, wenn oben oder gleich, unsigned)
// Mit Speicherquelle
cmovz(rax, qword_ptr(rbx)); // 48 0F 44 03
cmovnz(eax, dword_ptr(rcx + 0x10)); // 0F 45 41 10
// Bit testen
bt(eax, 5); // 0F BA E0 05
bt(rax, rbx); // 48 0F A3 D8
// Bit testen und setzen/zurücksetzen/komplementieren
bts(eax, 10); // 0F BA E8 0A (testen und setzen)
btr(rax, rcx); // 48 0F B3 C8 (testen und zurücksetzen)
btc(edx, 3); // 0F BA FA 03 (testen und komplementieren)
// Bits scannen
bsf(eax, ecx); // 0F BC C1 (vorwärts scannen nach erster 1)
bsr(rax, rbx); // 48 0F BD C3 (rückwärts scannen nach erster 1)
// Populationszählung und führende/nachlaufende Nullen
popcnt(eax, ecx); // F3 0F B8 C1
lzcnt(rax, rbx); // F3 48 0F BD C3
tzcnt(eax, edx); // F3 0F BC C2
// Byte-Reihenfolge umkehren
bswap(eax); // 0F C8 (Bytes umkehren)
bswap(rax); // 48 0F C8
// Basis-String-Ops (arbeiten auf [rsi] und/oder [rdi])
movsb(); // A4 (Byte [rsi] -> [rdi])
movsw(); // 66 A5
movsd(); // A5
movsq(); // 48 A5
cmpsb(); // A6 ([rsi] mit [rdi] vergleichen)
stosb(); // AA (al -> [rdi])
lodsb(); // AC ([rsi] -> al)
scasb(); // AE (al mit [rdi] vergleichen)
// Mit REP-Präfix (rcx-mal wiederholen)
rep_movsb(); // F3 A4 (memcpy)
rep_movsq(); // F3 48 A5 (schnelles memcpy, 8 Bytes auf einmal)
rep_stosb(); // F3 AA (memset)
rep_stosq(); // F3 48 AB
// Mit REPE/REPNE (wiederholen, solange gleich/nicht gleich)
repe_cmpsb(); // F3 A6 (Strings vergleichen bis Ungleichheit)
repne_scasb(); // F2 AE (nach Byte in String suchen)
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
// Systemaufrufe
syscall_(); // 0F 05 (64-Bit-Syscall)
sysenter(); // 0F 34
sysexit(); // 0F 35
// Interrupts
int3(); // CC (Haltepunkt)
int_(0x80); // CD 80 (Linux 32-Bit-Syscall)
int_(0x21); // CD 21 (DOS-Interrupt)
// CPU-Informationen
cpuid(); // 0F A2
rdtsc(); // 0F 31
rdtscp(); // 0F 01 F9
// Privilegien
cli(); // FA (Interrupts sperren)
sti(); // FB (Interrupts freigeben)
hlt(); // F4 (anhalten)
// Interrupt-Rückkehr
iret(); // CF (16-Bit)
iretd(); // CF (32-Bit)
iretq(); // 48 CF (64-Bit)
Verwenden Sie core::assemble(), um Byte-Arrays von Befehlen zu verketten:
constexpr auto prologue = core::assemble(
push(rbp),
mov(rbp, rsp),
sub(rsp, 0x20)
);
constexpr auto epilogue = core::assemble(
add(rsp, 0x20),
pop(rbp),
ret()
);
// Kombinieren
constexpr auto full_function = core::assemble(prologue, epilogue);
Fügen Sie zu Ihrer CMakeLists.txt hinzu:
include(FetchContent)
FetchContent_Declare(
static_asm
GIT_REPOSITORY https://github.com/mahmoudimus/static_asm.git
GIT_TAG v1.0.0 # oder spezifischer Commit
)
FetchContent_MakeAvailable(static_asm)
target_link_libraries(your_target PRIVATE static_asm::static_asm)
Klonen oder als Git-Submodul hinzufügen:
git submodule add https://github.com/mahmoudimus/static_asm.git external/static_asm
Dann in Ihrer CMakeLists.txt:
add_subdirectory(external/static_asm)
target_link_libraries(your_target PRIVATE static_asm::static_asm)
Wenn via add_subdirectory oder FetchContent eingebunden, wird nur das Interface-Bibliotheksziel static_asm::static_asm zu Ihrem Projekt hinzugefügt. Tests und Beispiele werden nicht gebaut, es sei denn, sie werden explizit mit -DSTATIC_ASM_BUILD_TESTS=ON aktiviert.
Laden Sie static_asm.hpp von der Releases-Seite herunter und binden Sie es direkt ein:
#include "static_asm.hpp"
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --install build --prefix /usr/local
Dann find_package verwenden:
find_package(static_asm REQUIRED)
target_link_libraries(your_target PRIVATE static_asm::static_asm)
# Konfigurieren
cmake -B build -DCMAKE_BUILD_TYPE=Release
# Bauen
cmake --build build
# Tests ausführen
ctest --test-dir build --output-on-failure
# Mit Beispielen bauen (nur Clang, verwendet Inline-Assembly)
cmake -B build -DCMAKE_BUILD_TYPE=Release -DSTATIC_ASM_BUILD_EXAMPLES=ON
Plattformunterstützung:
Hinweis: Die core::emit()-Inline-Assembly-Funktion erfordert Clang mit -O2-Optimierung.
Operandenunterstützung:
[base + index*scale + disp] mit Skalierungsfaktoren 1, 2, 4, 8Hinweis: Noch keine SIMD/AVX-Erweiterungen.
Dieses Projekt verwendet uv für Python-Werkzeuge (Code-Generierung, Single-Header-Amalgamation).
# uv installieren (falls noch nicht geschehen)
curl -LsSf https://astral.sh/uv/install.sh | sh
# Installation überprüfen
uv --version
# Alle Python-Skripte können direkt mit uv ausgeführt werden (Abhängigkeiten werden automatisch verwaltet)
uv run scripts/gen_from_x86ref.py --help
uv run scripts/amalgamate.sh
Erforderliche Werkzeuge:
Optionale Werkzeuge für die Entwicklung:
| Werkzeug | Zweck | Installation |
|---|---|---|
| clang-format | Code-Formatierung | Über LLVM oder Systempaket |
| clang-tidy | Statische Analyse | Über LLVM oder Systempaket |
Das Projekt enthält einen Generator, der die x86reference-XML-Datenbank parst:
# Zusammenfassung der Befehlskatalog
uv run scripts/gen_from_x86ref.py
# Details für einen bestimmten Befehl anzeigen
uv run scripts/gen_from_x86ref.py -i lea
uv run scripts/gen_from_x86ref.py -i imul
# Befehlskatalog-Dateien generieren
uv run scripts/gen_from_x86ref.py --generate-db
# Erschöpfende Testdatei generieren
uv run scripts/gen_from_x86ref.py --generate-tests
instdb, prefix_db, prefix_0fdb)encoder.hpp oder erweitern Sie einen vorhandenen EncoderKein automatisches Werkzeug kann eine beliebige Multi-File-C++-Bibliothek zuverlässig in eine saubere, Header-Only-Version umwandeln, ohne manuelle Vorbereitung. Die folgenden Techniken helfen sicherzustellen, dass Ihre Bibliothek erfolgreich zu einer einzigen Header-Datei amalgamiert werden kann, während sie korrekt, wartbar und standardkonform bleibt.
Hinweis: Dieses Projekt verwendet quom für die Amalgamation und clang-tidy mit dem
google-build-using-namespace-Check, um die Einhaltung dieser Regeln zu automatisieren. quom behandelt auch teilweise Technik #4 (Inline-Markierungen) durch seine Verarbeitungsfähigkeiten.
using namespace in Quelldateienusing namespace auf Dateiebene in .cpp-Dateien ist gefährlich, wenn diese Dateien später in einen Header eingefügt werden – es verschmutzt den globalen Namensraum für jede Übersetzungseinheit, die Ihren Header einbindet.
Empfohlene Muster stattdessen:
// Bevorzugt: Implementierung in Namensraum einbetten
namespace MyLib {
Foo::Foo() {
// ...
}
}
oder
// Explizite Qualifikation (ausführlicher, aber sehr klar)
MyLib::Foo::Foo() {
// ...
}
Öffentliche APIs sollten im Hauptnamensraum leben. Alles, was nicht für Endbenutzer gedacht ist, sollte in einem verschachtelten Namensraum wie detail oder impl versteckt werden.
Übliche Konventionen:
namespace MyLib {
namespace detail { // weit verbreitet
// interne Klassen, Funktionen usw.
}
}
oder
namespace MyLib::impl { // kürzer, ebenfalls üblich
// interne Implementierungsdetails
}
C++17 und höher unterstützen verschachtelte Inline-Namensraumdefinitionen, die sauberer sind:
namespace MyLib::detail {
class InternalHelper { /* ... */ };
}
static inline-KlassenmemberDateibereichs-static-Variablen, die in .cpp-Dateien definiert sind, werden in einer Header-Only-Welt problematisch (mehrere Definitionen, ODR-Verletzungen).
Moderne (C++17+) Lösung:
// Vorher (in .cpp)
namespace MyLib {
static int s_counter = 0;
int next_id() {
return ++s_counter;
}
}
// Nachher (sicher für Header)
namespace MyLib::detail {
struct Globals {
static inline int counter = 0;
};
}
inline int MyLib::next_id() {
return ++detail::Globals::counter;
}
Die static inline-Variable hat garantiert eine einzige Definition, selbst wenn sie mehrfach eingebunden wird.
inlineJede Funktion, Memberfunktion, Konstruktor oder Destruktor, deren Rumpf im Header erscheint (aber nicht innerhalb der Klassendefinition), muss mit inline markiert werden, um Verletzungen der Eine-Definitions-Regel (ODR) zu vermeiden.
Da viele Amalgamationsskripte rein textuell sind und keine C++-Semantik parsen, ist eine übliche Konvention, während der Entwicklung ein Platzhalter-Makro (z.B. inline_t) zu verwenden:
// MyLib.h (oder gemeinsamer Header)
#define inline_t /* leer während normaler Builds */
// MyLib.cpp (während der Entwicklung)
namespace MyLib::detail {
inline_t void Helper::do_work() {
// Implementierung
}
}
Während der Amalgamation ersetzt das Werkzeug inline_t durch inline:
// Nach Amalgamation / Transformation
inline void MyLib::detail::Helper::do_work() {
// ...
}
Sie können jeden Makronamen wählen, den Sie bevorzugen (z.B. MYLIB_INLINE, INLINE_IMP usw.) und Ihr Amalgamationsskript entsprechend konfigurieren.
Hinweis: Werkzeuge wie quom können dies teilweise automatisieren, indem sie C++-Include-Semantik verstehen und Funktionsdefinitionen während der Amalgamation korrekt behandeln, was in vielen Fällen die Notwendigkeit manueller
inline-Markierungen reduziert.
using namespace … auf Namensraum-/Dateiebene in Implementierungsdateien schreiben.detail / impl) verschieben.static-Daten durch static inline-Member einer Struktur/Klasse ersetzen.inline-Markierungs-Makro versehen (ersetzt während der Amalgamation).Die Befolgung dieser vier Praktiken macht den Übergang zu einer Single-Header-Distribution viel reibungsloser und weitaus weniger fehleranfällig – selbst bei Verwendung rein textbasierter Amalgamationswerkzeuge.
Dieses Projekt basiert auf cx_assembler von Midi12. Die ursprüngliche Bibliothek lieferte die Grundlage für die Kompilierzeit-Kodierung von x86-Assembly in C++.
| Kategorie | Befehle |
|---|
| ALU | ADD, ADC, SUB, SBB, AND, OR, XOR, CMP, TEST |
| Unär | INC, DEC, NEG, NOT |
| Multiplikation/Division | MUL, IMUL (1/2/3 Operandenformen), DIV, IDIV |
| Datenbewegung | MOV, MOVABS, MOVZX, MOVSX, MOVSXD, LEA, XCHG, PUSH, POP |
| Schieben/Rotieren | SHL, SHR, SAL, SAR, ROL, ROR, RCL, RCR |
| Kontrollfluss | JMP, CALL, RET, RETF |
| Bedingte Sprünge | JZ/JE, JNZ/JNE, JB/JC, JNB/JNC, JBE/JNA, JNBE/JA, JL, JNL, JLE, JNLE, JO, JNO, JS, JNS, JP, JNP (8-Bit- und 32-Bit-Offsets) |
| Bedingte Verschiebungen | CMOVA, CMOVAE, CMOVB, CMOVBE, CMOVE, CMOVG, CMOVGE, CMOVL, CMOVLE, CMOVNE, CMOVNO, CMOVNP, CMOVNS, CMOVO, CMOVP, CMOVS |
| Bitoperationen | BT, BTC, BTR, BTS |
| Bitscannen/Zählen | BSF, BSR, POPCNT, LZCNT, TZCNT, BSWAP |
| Stringoperationen | MOVSB/W/D/Q, CMPSB/W/D/Q, LODSB/W/D/Q, STOSB/W/D/Q, SCASB/W/D/Q (mit REP/REPE/REPNE-Präfixen) |
| System | SYSCALL, SYSENTER, SYSEXIT, INT, INT3, IRET/D/Q, CLI, STI, HLT, CPUID, RDTSC, RDTSCP |
| Sonstiges | NOP, UD2 |
| Werkzeug | Zweck | Installation |
|---|
| uv | Python-Paket-/Projektmanager | curl -LsSf https://astral.sh/uv/install.sh | sh |
| quom | Single-Header-Amalgamation | uv tool install quom |
| CMake 3.19+ | Build-System | cmake.org |
| C++20-Compiler | GCC 11+, Clang 14+, MSVC 2022+ | - |