
Ensamblador de solo cabecera en tiempo de compilación en C++2x para la codificación de instrucciones x86/x86-64
Una biblioteca C++20 de solo encabezado para codificación de instrucciones x86/x86-64 en tiempo de compilación.
Este proyecto tiene doble licencia bajo la Boost Software License 1.0 y la MIT License. Puedes elegir cualquiera de las dos.
#include "static_asm.hpp"
using namespace static_asm::x86::registers;
using namespace static_asm::x86::instructions;
// Construir código máquina en tiempo de compilación
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 es std::array<uint8_t, N> - ¡completamente constexpr!
// Registro a registro
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 (idioma común para poner a cero un registro)
cmp(rax, rcx); // 48 39 C8
// Registro con inmediato
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
// Con operandos de memoria
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
// Registro a registro
mov(rax, rbx); // 48 89 D8
mov(eax, ecx); // 89 C8
mov(r8, r9); // 4D 89 C8
// Inmediato a registro
mov(rax, 0x12345678); // 48 C7 C0 78 56 34 12
mov(eax, 0xDEADBEEF); // B8 EF BE AD DE
// Operaciones de memoria
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
// Extensión de cero/signo
movzx(eax, bl); // 0F B6 C3 (extensión cero de byte a dword)
movzx(rax, bx); // 48 0F B7 C3 (extensión cero de word a qword)
movsx(eax, cl); // 0F BE C1 (extensión de signo de byte a dword)
movsx(rax, dx); // 48 0F BF C2 (extensión de signo de word a qword)
movsxd(rax, ecx); // 48 63 C1 (extensión de signo de dword a qword)
// Cargar dirección efectiva
lea(rax, qword_ptr(rbx + rcx * s4)); // 48 8D 04 8B
lea(rax, qword_ptr(rbx + rcx * s8 + 0x10)); // 48 8D 44 CB 10
// Intercambio
xchg(rax, rbx); // 48 87 D8
// [base + índice*escala]
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 + índice*escala + desplazamiento]
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
// Almacenar en dirección SIB
mov(dword_ptr(rax + rdx * s4), ecx); // 89 0C 90
mov(qword_ptr(rbx + rsi * s8 + 0x20), rax); // 48 89 44 F3 20
// LEA con SIB (útil para cálculos de direcciones)
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
// Desplazar por 1
shl(eax, 1); // D1 E0
shr(rax, 1); // 48 D1 E8
sar(ecx, 1); // D1 F9
// Desplazar por inmediato
shl(eax, 4); // C1 E0 04
shr(rax, 8); // 48 C1 E8 08
sar(rdx, 16); // 48 C1 FA 10
// Desplazar por registro CL
shl(eax, cl); // D3 E0
shr(rax, cl); // 48 D3 E8
// Rotar
rol(eax, 1); // D1 C0
ror(rax, 8); // 48 C1 C8 08
rcl(ecx, cl); // D3 D1
rcr(rdx, 1); // 48 D1 DA
// Un solo operando (resultado en rdx:rax)
mul(rbx); // 48 F7 E3 (sin signo: rdx:rax = rax * rbx)
imul(rcx); // 48 F7 E9 (con signo: rdx:rax = rax * rcx)
div(rbx); // 48 F7 F3 (sin signo: rax = rdx:rax / rbx, rdx = resto)
idiv(rcx); // 48 F7 F9 (división con signo)
// IMUL de dos operandos (dest = dest * src)
imul(rax, rbx); // 48 0F AF C3
imul(ecx, edx); // 0F AF CA
// IMUL de tres operandos (dest = src * imm)
imul(rax, rbx, 10); // 48 6B C3 0A
imul(ecx, edx, 1000); // 69 CA E8 03 00 00
// Saltos incondicionales
jmp(0x10); // EB 10 (corto, offset de 8 bits)
jmp(0x1000); // E9 00 10 00 00 (cercano, offset de 32 bits)
jmp(rax); // FF E0 (indirecto)
jmp(here); // EB FE (jmp $, bucle infinito)
// Saltos condicionales (offset de 8 bits)
jz(0x10); // 74 10
jnz(0x20); // 75 20
jb(0x08); // 72 08 (por debajo/acarreo)
jae(0x08); // 73 08 (por encima o igual/sin acarreo)
jl(0x10); // 7C 10 (menor que, con signo)
jge(0x10); // 7D 10 (mayor o igual, con signo)
// Saltos condicionales (offset de 32 bits para ramas largas)
jz_near(0x10000); // 0F 84 00 00 01 00
jnz_near(0x20000); // 0F 85 00 00 02 00
// Llamada y retorno
call(rax); // FF D0 (llamada indirecta)
call(0x100); // E8 00 01 00 00 (llamada relativa)
ret(); // C3
ret(0x10); // C2 10 00 (retorno y extrae 16 bytes)
// Mover si la condición es verdadera (¡sin penalización de bifurcación!)
cmovz(rax, rbx); // 48 0F 44 C3 (mover si cero)
cmovnz(eax, ecx); // 0F 45 C1 (mover si no cero)
cmovl(rax, rdx); // 48 0F 4C C2 (mover si menor, con signo)
cmovge(ecx, esi); // 0F 4D CE (mover si mayor o igual, con signo)
cmovb(rax, rbx); // 48 0F 42 C3 (mover si por debajo, sin signo)
cmovae(edx, edi); // 0F 43 D7 (mover si por encima o igual, sin signo)
// Con fuente de memoria
cmovz(rax, qword_ptr(rbx)); // 48 0F 44 03
cmovnz(eax, dword_ptr(rcx + 0x10)); // 0F 45 41 10
// Prueba de bit
bt(eax, 5); // 0F BA E0 05
bt(rax, rbx); // 48 0F A3 D8
// Prueba de bit y establecer/restablecer/complementar
bts(eax, 10); // 0F BA E8 0A (prueba y establece)
btr(rax, rcx); // 48 0F B3 C8 (prueba y restablece)
btc(edx, 3); // 0F BA FA 03 (prueba y complementa)
// Escaneo de bits
bsf(eax, ecx); // 0F BC C1 (escanear hacia adelante el primer 1)
bsr(rax, rbx); // 48 0F BD C3 (escanear hacia atrás el primer 1)
// Conteo de población y ceros iniciales/finales
popcnt(eax, ecx); // F3 0F B8 C1
lzcnt(rax, rbx); // F3 48 0F BD C3
tzcnt(eax, edx); // F3 0F BC C2
// Intercambio de bytes
bswap(eax); // 0F C8 (invierte el orden de bytes)
bswap(rax); // 48 0F C8
// Operaciones básicas de cadena (operan sobre [rsi] y/o [rdi])
movsb(); // A4 (mover byte [rsi] -> [rdi])
movsw(); // 66 A5
movsd(); // A5
movsq(); // 48 A5
cmpsb(); // A6 (comparar [rsi] con [rdi])
stosb(); // AA (almacenar al -> [rdi])
lodsb(); // AC (cargar [rsi] -> al)
scasb(); // AE (comparar al con [rdi])
// Con prefijo REP (repetir rcx veces)
rep_movsb(); // F3 A4 (memcpy)
rep_movsq(); // F3 48 A5 (memcpy rápida, 8 bytes a la vez)
rep_stosb(); // F3 AA (memset)
rep_stosq(); // F3 48 AB
// Con prefijo REPE/REPNE (repetir mientras igual/no igual)
repe_cmpsb(); // F3 A6 (comparar cadenas hasta discrepancia)
repne_scasb(); // F2 AE (escanear byte en cadena)
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
// Llamadas al sistema
syscall_(); // 0F 05 (syscall de 64 bits)
sysenter(); // 0F 34
sysexit(); // 0F 35
// Interrupciones
int3(); // CC (punto de interrupción)
int_(0x80); // CD 80 (syscall de Linux de 32 bits)
int_(0x21); // CD 21 (interrupción de DOS)
// Información de CPU
cpuid(); // 0F A2
rdtsc(); // 0F 31
rdtscp(); // 0F 01 F9
// Privilegios
cli(); // FA (deshabilitar interrupciones)
sti(); // FB (habilitar interrupciones)
hlt(); // F4 (detener)
// Retorno de interrupción
iret(); // CF (16 bits)
iretd(); // CF (32 bits)
iretq(); // 48 CF (64 bits)
Usa core::assemble() para concatenar arrays de bytes de instrucciones:
constexpr auto prologue = core::assemble(
push(rbp),
mov(rbp, rsp),
sub(rsp, 0x20)
);
constexpr auto epilogue = core::assemble(
add(rsp, 0x20),
pop(rbp),
ret()
);
// Combinarlos
constexpr auto full_function = core::assemble(prologue, epilogue);
Agrega a tu CMakeLists.txt:
include(FetchContent)
FetchContent_Declare(
static_asm
GIT_REPOSITORY https://github.com/mahmoudimus/static_asm.git
GIT_TAG v1.0.0 # o commit específico
)
FetchContent_MakeAvailable(static_asm)
target_link_libraries(your_target PRIVATE static_asm::static_asm)
Clona o agrega como submódulo git:
git submodule add https://github.com/mahmoudimus/static_asm.git external/static_asm
Luego en tu CMakeLists.txt:
add_subdirectory(external/static_asm)
target_link_libraries(your_target PRIVATE static_asm::static_asm)
Al incluirse mediante add_subdirectory o FetchContent, solo se agrega a tu proyecto el objetivo de biblioteca de interfaz static_asm::static_asm. Las pruebas y ejemplos no se construyen a menos que se habilite explícitamente con -DSTATIC_ASM_BUILD_TESTS=ON.
Descarga static_asm.hpp desde la página de versiones e inclúyelo directamente:
#include "static_asm.hpp"
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --install build --prefix /usr/local
Luego usa find_package:
find_package(static_asm REQUIRED)
target_link_libraries(your_target PRIVATE static_asm::static_asm)
# Configurar
cmake -B build -DCMAKE_BUILD_TYPE=Release
# Compilar
cmake --build build
# Ejecutar pruebas
ctest --test-dir build --output-on-failure
# Compilar con ejemplos (solo Clang, usa ensamblado en línea)
cmake -B build -DCMAKE_BUILD_TYPE=Release -DSTATIC_ASM_BUILD_EXAMPLES=ON
Soporte de plataformas:
Nota: La función core::emit() de ensamblado en línea requiere Clang con optimización -O2.
Soporte de operandos:
[base + índice*escala + desplazamiento] con factores de escala 1, 2, 4, 8Nota: Aún no hay extensiones SIMD/AVX.
Este proyecto usa uv para herramientas Python (generación de código, amalgamación de encabezado único).
# Instalar uv (si aún no está instalado)
curl -LsSf https://astral.sh/uv/install.sh | sh
# Verificar instalación
uv --version
# Todos los scripts Python se pueden ejecutar directamente con uv (las dependencias se gestionan automáticamente)
uv run scripts/gen_from_x86ref.py --help
uv run scripts/amalgamate.sh
Herramientas requeridas:
Herramientas opcionales para desarrollo:
| Herramienta | Propósito | Instalación |
|---|---|---|
| clang-format | Formateo de código | Vía LLVM o paquete del sistema |
| clang-tidy | Análisis estático | Vía LLVM o paquete del sistema |
El proyecto incluye un generador que analiza la base de datos XML de x86reference:
# Mostrar resumen de la base de datos de instrucciones
uv run scripts/gen_from_x86ref.py
# Mostrar detalles de una instrucción específica
uv run scripts/gen_from_x86ref.py -i lea
uv run scripts/gen_from_x86ref.py -i imul
# Generar archivos de base de datos de instrucciones
uv run scripts/gen_from_x86ref.py --generate-db
# Generar archivo de pruebas exhaustivas
uv run scripts/gen_from_x86ref.py --generate-tests
instdb, prefix_db, prefix_0fdb)encoder.hpp o extender un codificador existenteNinguna herramienta automática puede convertir de manera confiable una biblioteca C++ arbitraria de varios archivos en una versión limpia y de solo encabezado sin cierta preparación manual. Las siguientes técnicas ayudan a garantizar que tu biblioteca pueda amalgamarse exitosamente en un solo archivo de encabezado manteniendo corrección, mantenibilidad y conformidad con los estándares.
Nota: Este proyecto usa quom para amalgamación y clang-tidy con la verificación
google-build-using-namespacepara automatizar la aplicación de estas reglas. quom también maneja parcialmente la técnica #4 (marcadores inline) a través de sus capacidades de procesamiento.
using namespace en archivos fuenteusing namespace a nivel de archivo en archivos .cpp es peligroso cuando esos archivos se incluyen posteriormente en un encabezado: contamina el espacio de nombres global para cada unidad de traducción que incluya tu encabezado.
Patrones recomendados en su lugar:
// Preferido: envolver la implementación en un espacio de nombres
namespace MyLib {
Foo::Foo() {
// ...
}
}
o
// Calificación explícita (más verbosa pero muy clara)
MyLib::Foo::Foo() {
// ...
}
Las API públicas deben residir en el espacio de nombres principal. Todo lo que no esté destinado a usuarios finales debe ocultarse en un espacio de nombres anidado como detail o impl.
Convenciones comunes:
namespace MyLib {
namespace detail { // muy utilizado
// clases internas, funciones, etc.
}
}
o
namespace MyLib::impl { // más corto, también común
// detalles internos de implementación
}
C++17 y posteriores admiten definiciones de espacios de nombres anidados en línea, que son más limpias:
namespace MyLib::detail {
class InternalHelper { /* ... */ };
}
static inlineLas variables static a nivel de archivo definidas en archivos .cpp se vuelven problemáticas en un mundo de solo encabezado (definiciones múltiples, violaciones de ODR).
Solución moderna (C++17+):
// Antes (en .cpp)
namespace MyLib {
static int s_counter = 0;
int next_id() {
return ++s_counter;
}
}
// Después (seguro para encabezado)
namespace MyLib::detail {
struct Globals {
static inline int counter = 0;
};
}
inline int MyLib::next_id() {
return ++detail::Globals::counter;
}
La variable static inline tiene garantizada una única definición incluso cuando se incluye múltiples veces.
inlineCualquier función, función miembro, constructor o destructor cuyo cuerpo aparezca en el encabezado (pero no dentro de la definición de la clase) debe marcarse como inline para evitar violaciones de la Regla de Definición Única (ODR).
Debido a que muchos scripts de amalgamación son puramente textuales y no analizan la semántica de C++, una convención común es usar una macro de marcador de posición (por ejemplo, inline_t) durante el desarrollo:
// MyLib.h (o encabezado común)
#define inline_t /* vacío durante compilaciones normales */
// MyLib.cpp (durante desarrollo)
namespace MyLib::detail {
inline_t void Helper::do_work() {
// implementación
}
}
Durante la amalgamación, la herramienta reemplaza inline_t con inline:
// Después de amalgamación / transformación
inline void MyLib::detail::Helper::do_work() {
// ...
}
Puedes elegir cualquier nombre de macro que prefieras (ej. MYLIB_INLINE, INLINE_IMP, etc.) y configurar tu script de amalgamación en consecuencia.
Nota: Herramientas como quom pueden automatizar parcialmente esto al comprender las semánticas de inclusión de C++ y manejar adecuadamente las definiciones de funciones durante la amalgamación, reduciendo la necesidad de marcadores
inlinemanuales en muchos casos.
using namespace … a nivel de espacio de nombres/archivo en archivos de implementación.detail / impl).static a nivel de archivo con miembros static inline de una estructura/clase.inline (reemplazada durante la amalgamación).Seguir estas cuatro prácticas hace que la transición a una distribución de solo encabezado sea mucho más suave y menos propensa a errores, incluso cuando se utilizan herramientas de amalgamación puramente basadas en texto.
Este proyecto está basado en cx_assembler de Midi12. La biblioteca original proporcionó la base para la codificación de ensamblador x86 en tiempo de compilación en C++.
| Categoría | Instrucciones |
|---|
| ALU | ADD, ADC, SUB, SBB, AND, OR, XOR, CMP, TEST |
| Unario | INC, DEC, NEG, NOT |
| Multiplicación/División | MUL, IMUL (formas de 1/2/3 operandos), DIV, IDIV |
| Movimiento de datos | MOV, MOVABS, MOVZX, MOVSX, MOVSXD, LEA, XCHG, PUSH, POP |
| Desplazamiento/Rotación | SHL, SHR, SAL, SAR, ROL, ROR, RCL, RCR |
| Flujo de control | JMP, CALL, RET, RETF |
| Saltos condicionales | JZ/JE, JNZ/JNE, JB/JC, JNB/JNC, JBE/JNA, JNBE/JA, JL, JNL, JLE, JNLE, JO, JNO, JS, JNS, JP, JNP (offsets de 8 bits y 32 bits) |
| Movimientos condicionales | CMOVA, CMOVAE, CMOVB, CMOVBE, CMOVE, CMOVG, CMOVGE, CMOVL, CMOVLE, CMOVNE, CMOVNO, CMOVNP, CMOVNS, CMOVO, CMOVP, CMOVS |
| Operaciones de bits | BT, BTC, BTR, BTS |
| Escaneo/Conteo de bits | BSF, BSR, POPCNT, LZCNT, TZCNT, BSWAP |
| Operaciones de cadenas | MOVSB/W/D/Q, CMPSB/W/D/Q, LODSB/W/D/Q, STOSB/W/D/Q, SCASB/W/D/Q (con prefijos REP/REPE/REPNE) |
| Sistema | SYSCALL, SYSENTER, SYSEXIT, INT, INT3, IRET/D/Q, CLI, STI, HLT, CPUID, RDTSC, RDTSCP |
| Miscelánea | NOP, UD2 |
| Herramienta | Propósito | Instalación |
|---|
| uv | Gestor de paquetes/proyectos Python | curl -LsSf https://astral.sh/uv/install.sh | sh |
| quom | Amalgamación de encabezado único | uv tool install quom |
| CMake 3.19+ | Sistema de compilación | cmake.org |
| Compilador C++20 | GCC 11+, Clang 14+, MSVC 2022+ | - |