
Заголовочный ассемблер времени компиляции на C++2x для кодирования инструкций x86/x86-64
Библиотека только с заголовками для C++20, предназначенная для кодирования инструкций x86/x86-64 на этапе компиляции.
Этот проект распространяется под двойной лицензией: Boost Software License 1.0 и MIT License. Вы можете выбрать любую из них.
#include "static_asm.hpp"
using namespace static_asm::x86::registers;
using namespace static_asm::x86::instructions;
// Сборка машинного кода на этапе компиляции
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> - полностью constexpr!
// Регистр-регистр
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 (распространённый способ обнуления регистра)
cmp(rax, rcx); // 48 39 C8
// Регистр с непосредственным значением
add(rax, 0x10); // 48 83 C0 10 (знаковое расширение imm8)
add(rax, 0x10000); // 48 05 00 00 01 00 (imm32)
sub(ecx, 100); // 83 E9 64
and_(rdx, 0xFF); // 48 83 E2 FF
// С операндами в памяти
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
// Регистр-регистр
mov(rax, rbx); // 48 89 D8
mov(eax, ecx); // 89 C8
mov(r8, r9); // 4D 89 C8
// Непосредственное значение в регистр
mov(rax, 0x12345678); // 48 C7 C0 78 56 34 12
mov(eax, 0xDEADBEEF); // B8 EF BE AD DE
// Операции с памятью
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
// Расширение нулём / знаком
movzx(eax, bl); // 0F B6 C3 (расширение байта нулём до двойного слова)
movzx(rax, bx); // 48 0F B7 C3 (расширение слова нулём до учётверённого слова)
movsx(eax, cl); // 0F BE C1 (расширение байта знаком до двойного слова)
movsx(rax, dx); // 48 0F BF C2 (расширение слова знаком до учётверённого слова)
movsxd(rax, ecx); // 48 63 C1 (расширение двойного слова знаком до учётверённого слова)
// Загрузка эффективного адреса
lea(rax, qword_ptr(rbx + rcx * s4)); // 48 8D 04 8B
lea(rax, qword_ptr(rbx + rcx * s8 + 0x10)); // 48 8D 44 CB 10
// Обмен
xchg(rax, rbx); // 48 87 D8
// [база + индекс*масштаб]
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
// [база + индекс*масштаб + смещение]
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
// Запись по 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 с SIB (полезно для вычисления адресов)
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
// Сдвиг на 1
shl(eax, 1); // D1 E0
shr(rax, 1); // 48 D1 E8
sar(ecx, 1); // D1 F9
// Сдвиг на непосредственное значение
shl(eax, 4); // C1 E0 04
shr(rax, 8); // 48 C1 E8 08
sar(rdx, 16); // 48 C1 FA 10
// Сдвиг на регистр CL
shl(eax, cl); // D3 E0
shr(rax, cl); // 48 D3 E8
// Вращение
rol(eax, 1); // D1 C0
ror(rax, 8); // 48 C1 C8 08
rcl(ecx, cl); // D3 D1
rcr(rdx, 1); // 48 D1 DA
// Однооперандные (результат в rdx:rax)
mul(rbx); // 48 F7 E3 (беззнаковое: rdx:rax = rax * rbx)
imul(rcx); // 48 F7 E9 (знаковое: rdx:rax = rax * rcx)
div(rbx); // 48 F7 F3 (беззнаковое: rax = rdx:rax / rbx, rdx = остаток)
idiv(rcx); // 48 F7 F9 (знаковое деление)
// Двухоперандный IMUL (результат = результат * источник)
imul(rax, rbx); // 48 0F AF C3
imul(ecx, edx); // 0F AF CA
// Трёхоперандный IMUL (результат = источник * непосредственное)
imul(rax, rbx, 10); // 48 6B C3 0A
imul(ecx, edx, 1000); // 69 CA E8 03 00 00
// Безусловные переходы
jmp(0x10); // EB 10 (короткий, 8-битное смещение)
jmp(0x1000); // E9 00 10 00 00 (близкий, 32-битное смещение)
jmp(rax); // FF E0 (косвенный)
jmp(here); // EB FE (jmp $, бесконечный цикл)
// Условные переходы (8-битное смещение)
jz(0x10); // 74 10
jnz(0x20); // 75 20
jb(0x08); // 72 08 (ниже/перенос)
jae(0x08); // 73 08 (выше или равно/нет переноса)
jl(0x10); // 7C 10 (меньше, знаковое)
jge(0x10); // 7D 10 (больше или равно, знаковое)
// Условные переходы (32-битное смещение для длинных ветвей)
jz_near(0x10000); // 0F 84 00 00 01 00
jnz_near(0x20000); // 0F 85 00 00 02 00
// Вызов и возврат
call(rax); // FF D0 (косвенный вызов)
call(0x100); // E8 00 01 00 00 (относительный вызов)
ret(); // C3
ret(0x10); // C2 10 00 (возврат и извлечение 16 байт)
// Пересылка, если условие истинно (без штрафа за ветвление!)
cmovz(rax, rbx); // 48 0F 44 C3 (переслать, если ноль)
cmovnz(eax, ecx); // 0F 45 C1 (переслать, если не ноль)
cmovl(rax, rdx); // 48 0F 4C C2 (переслать, если меньше, знаковое)
cmovge(ecx, esi); // 0F 4D CE (переслать, если больше или равно, знаковое)
cmovb(rax, rbx); // 48 0F 42 C3 (переслать, если ниже, беззнаковое)
cmovae(edx, edi); // 0F 43 D7 (переслать, если выше или равно, беззнаковое)
// С источником в памяти
cmovz(rax, qword_ptr(rbx)); // 48 0F 44 03
cmovnz(eax, dword_ptr(rcx + 0x10)); // 0F 45 41 10
// Тест бита
bt(eax, 5); // 0F BA E0 05
bt(rax, rbx); // 48 0F A3 D8
// Тест бита и установка/сброс/дополнение
bts(eax, 10); // 0F BA E8 0A (тест и установка)
btr(rax, rcx); // 48 0F B3 C8 (тест и сброс)
btc(edx, 3); // 0F BA FA 03 (тест и дополнение)
// Сканирование бита
bsf(eax, ecx); // 0F BC C1 (сканирование вперёд для первого 1)
bsr(rax, rbx); // 48 0F BD C3 (сканирование назад для первого 1)
// Подсчёт единиц и ведущие/конечные нули
popcnt(eax, ecx); // F3 0F B8 C1
lzcnt(rax, rbx); // F3 48 0F BD C3
tzcnt(eax, edx); // F3 0F BC C2
// Перестановка байт
bswap(eax); // 0F C8 (обратный порядок байт)
bswap(rax); // 48 0F C8
// Базовые строковые операции (работают с [rsi] и/или [rdi])
movsb(); // A4 (переместить байт [rsi] -> [rdi])
movsw(); // 66 A5
movsd(); // A5
movsq(); // 48 A5
cmpsb(); // A6 (сравнить [rsi] с [rdi])
stosb(); // AA (записать al -> [rdi])
lodsb(); // AC (загрузить [rsi] -> al)
scasb(); // AE (сравнить al с [rdi])
// С префиксом REP (повторить rcx раз)
rep_movsb(); // F3 A4 (memcpy)
rep_movsq(); // F3 48 A5 (быстрый memcpy, по 8 байт за раз)
rep_stosb(); // F3 AA (memset)
rep_stosq(); // F3 48 AB
// С префиксами REPE/REPNE (повторять, пока равно/не равно)
repe_cmpsb(); // F3 A6 (сравнение строк до несовпадения)
repne_scasb(); // F2 AE (поиск байта в строке)
push(rax); // 50
push(rbx); // 53
push(r8); // 41 50
push(0x10); // 6A 10 (поместить imm8)
push(0x1000); // 68 00 10 00 00 (поместить imm32)
pop(rax); // 58
pop(rbx); // 5B
pop(r15); // 41 5F
// Системные вызовы
syscall_(); // 0F 05 (64-битный syscall)
sysenter(); // 0F 34
sysexit(); // 0F 35
// Прерывания
int3(); // CC (точка останова)
int_(0x80); // CD 80 (Linux 32-битный syscall)
int_(0x21); // CD 21 (прерывание DOS)
// Информация о CPU
cpuid(); // 0F A2
rdtsc(); // 0F 31
rdtscp(); // 0F 01 F9
// Привилегированные
cli(); // FA (запрет прерываний)
sti(); // FB (разрешение прерываний)
hlt(); // F4 (останов)
// Возврат из прерывания
iret(); // CF (16-битный)
iretd(); // CF (32-битный)
iretq(); // 48 CF (64-битный)
Используйте core::assemble() для объединения массивов байт инструкций:
constexpr auto prologue = core::assemble(
push(rbp),
mov(rbp, rsp),
sub(rsp, 0x20)
);
constexpr auto epilogue = core::assemble(
add(rsp, 0x20),
pop(rbp),
ret()
);
// Объединение
constexpr auto full_function = core::assemble(prologue, epilogue);
Добавьте в ваш CMakeLists.txt:
include(FetchContent)
FetchContent_Declare(
static_asm
GIT_REPOSITORY https://github.com/mahmoudimus/static_asm.git
GIT_TAG v1.0.0 # или конкретный коммит
)
FetchContent_MakeAvailable(static_asm)
target_link_libraries(your_target PRIVATE static_asm::static_asm)
Клонируйте или добавьте как git-подмодуль:
git submodule add https://github.com/mahmoudimus/static_asm.git external/static_asm
Затем в вашем CMakeLists.txt:
add_subdirectory(external/static_asm)
target_link_libraries(your_target PRIVATE static_asm::static_asm)
При включении через add_subdirectory или FetchContent в проект добавляется только цель интерфейсной библиотеки static_asm::static_asm. Тесты и примеры не собираются, если явно не включены с помощью -DSTATIC_ASM_BUILD_TESTS=ON.
Скачайте static_asm.hpp со страницы релизов и включите его напрямую:
#include "static_asm.hpp"
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --install build --prefix /usr/local
Затем используйте find_package:
find_package(static_asm REQUIRED)
target_link_libraries(your_target PRIVATE static_asm::static_asm)
# Конфигурация
cmake -B build -DCMAKE_BUILD_TYPE=Release
# Сборка
cmake --build build
# Запуск тестов
ctest --test-dir build --output-on-failure
# Сборка с примерами (только Clang, использует встроенный ассемблер)
cmake -B build -DCMAKE_BUILD_TYPE=Release -DSTATIC_ASM_BUILD_EXAMPLES=ON
Поддерживаемые платформы:
Примечание: функция core::emit() со встроенным ассемблером требует Clang с оптимизацией -O2.
| Категория | Инструкции |
|---|---|
| ALU | ADD, ADC, SUB, SBB, AND, OR, XOR, CMP, TEST |
| Унарные | INC, DEC, NEG, NOT |
| Умножение/Деление | MUL, IMUL (формы с 1/2/3 операндами), DIV, IDIV |
| Перемещение данных | MOV, MOVABS, MOVZX, MOVSX, MOVSXD, LEA, XCHG, PUSH, POP |
| Сдвиг/Вращение | SHL, SHR, SAL, SAR, ROL, ROR, RCL, RCR |
| Управление потоком | JMP, CALL, RET, RETF |
| Условные переходы | JZ/JE, JNZ/JNE, JB/JC, JNB/JNC, JBE/JNA, JNBE/JA, JL, JNL, JLE, JNLE, JO, JNO, JS, JNS, JP, JNP (8-битные и 32-битные смещения) |
| Условные пересылки | CMOVA, CMOVAE, CMOVB, CMOVBE, CMOVE, CMOVG, CMOVGE, CMOVL, CMOVLE, CMOVNE, CMOVNO, CMOVNP, CMOVNS, CMOVO, CMOVP, CMOVS |
| Битовые операции | BT, BTC, BTR, BTS |
| Сканирование/Подсчёт бит | BSF, BSR, POPCNT, LZCNT, TZCNT, BSWAP |
| Строковые операции | MOVSB/W/D/Q, CMPSB/W/D/Q, LODSB/W/D/Q, STOSB/W/D/Q, SCASB/W/D/Q (с префиксами REP/REPE/REPNE) |
| Системные | SYSCALL, SYSENTER, SYSEXIT, INT, INT3, IRET/D/Q, CLI, STI, HLT, CPUID, RDTSC, RDTSCP |
| Разное | NOP, UD2 |
Поддержка операндов:
[база + индекс*масштаб + смещение] с коэффициентами масштаба 1, 2, 4, 8Примечание: Расширения SIMD/AVX пока не поддерживаются.
В этом проекте используется uv для инструментов Python (генерация кода, объединение в один заголовок).
# Установка uv (если ещё не установлен)
curl -LsSf https://astral.sh/uv/install.sh | sh
# Проверка установки
uv --version
# Все Python-скрипты можно запускать напрямую через uv (зависимости управляются автоматически)
uv run scripts/gen_from_x86ref.py --help
uv run scripts/amalgamate.sh
Необходимые инструменты:
Дополнительные инструменты для разработки:
| Инструмент | Назначение | Установка |
|---|---|---|
| clang-format | Форматирование кода | Через LLVM или системный пакет |
| clang-tidy | Статический анализ | Через LLVM или системный пакет |
Проект включает генератор, который разбирает базу данных XML x86reference:
# Показать сводку базы инструкций
uv run scripts/gen_from_x86ref.py
# Показать подробности для конкретной инструкции
uv run scripts/gen_from_x86ref.py -i lea
uv run scripts/gen_from_x86ref.py -i imul
# Сгенерировать файлы базы инструкций
uv run scripts/gen_from_x86ref.py --generate-db
# Сгенерировать исчерпывающий тестовый файл
uv run scripts/gen_from_x86ref.py --generate-tests
instdb, prefix_db, prefix_0fdb)encoder.hpp или расширьте существующийНикакой автоматический инструмент не может надёжно преобразовать произвольную многофайловую библиотеку C++ в чистую версию только с заголовками без некоторой ручной подготовки. Следующие методы помогут убедиться, что ваша библиотека может быть успешно объединена в один файл заголовка, оставаясь при этом корректной, поддерживаемой и соответствующей стандартам.
Примечание: Этот проект использует quom для объединения и clang-tidy с проверкой
google-build-using-namespaceдля автоматического соблюдения этих правил. quom также частично обрабатывает метод #4 (маркеры встраивания) через свои возможности обработки.
using namespace в исходных файлахusing namespace на уровне файла в файлах .cpp опасен, если эти файлы позже включаются в заголовок — это загрязняет глобальное пространство имён для каждой единицы трансляции, которая включает ваш заголовок.
Рекомендуемые альтернативные шаблоны:
// Предпочтительно: обернуть реализацию в пространство имён
namespace MyLib {
Foo::Foo() {
// ...
}
}
или
// Явная квалификация (более многословно, но очень понятно)
MyLib::Foo::Foo() {
// ...
}
Публичные API должны находиться в основном пространстве имён. Всё, что не предназначено для конечных пользователей, должно быть скрыто во вложенном пространстве имён, таком как detail или impl.
Распространённые соглашения:
namespace MyLib {
namespace detail { // очень широко используется
// внутренние классы, функции и т.д.
}
}
или
namespace MyLib::impl { // короче, также распространено
// детали внутренней реализации
}
C++17 и новее поддерживают встроенные вложенные определения пространств имён, которые чище:
namespace MyLib::detail {
class InternalHelper { /* ... */ };
}
static inlinestatic-переменные на уровне файла, определённые в файлах .cpp, становятся проблематичными в мире только заголовков (множественные определения, нарушения ODR).
Современное (C++17+) решение:
// До (в .cpp)
namespace MyLib {
static int s_counter = 0;
int next_id() {
return ++s_counter;
}
}
// После (безопасно для заголовка)
namespace MyLib::detail {
struct Globals {
static inline int counter = 0;
};
}
inline int MyLib::next_id() {
return ++detail::Globals::counter;
}
Переменная static inline гарантированно имеет единственное определение, даже если включается несколько раз.
inlineЛюбая функция, функция-член, конструктор или деструктор, чьё тело появляется в заголовке (но не внутри определения класса) должна быть помечена как inline, чтобы избежать нарушений Правила Одного Определения (ODR).
Поскольку многие скрипты объединения являются чисто текстовыми и не разбирают семантику C++, распространённым соглашением является использование макроса-заполнителя (например, inline_t) во время разработки:
// MyLib.h (или общий заголовок)
#define inline_t /* пусто при обычной сборке */
// MyLib.cpp (во время разработки)
namespace MyLib::detail {
inline_t void Helper::do_work() {
// реализация
}
}
Во время объединения инструмент заменяет inline_t на inline:
// После объединения / преобразования
inline void MyLib::detail::Helper::do_work() {
// ...
}
Вы можете выбрать любое имя макроса по своему усмотрению (например, MYLIB_INLINE, INLINE_IMP и т.д.) и настроить соответствующий скрипт объединения.
Примечание: Инструменты, такие как quom, могут частично автоматизировать этот процесс, понимая семантику включения C++ и правильно обрабатывая определения функций во время объединения, что во многих случаях снижает необходимость в ручных маркерах
inline.
using namespace … на уровне пространства имён/файла в файлах реализации.detail / impl).static inline члены структуры/класса.inline (заменяемым во время объединения).Следование этим четырём практикам делает переход к распространению в виде одного заголовка гораздо более плавным и менее подверженным ошибкам — даже при использовании чисто текстовых инструментов объединения.
Этот проект основан на cx_assembler от Midi12. Оригинальная библиотека предоставила основу для кодирования ассемблера x86 на этапе компиляции на C++.