AsmX G3: Переосмысление взаимодействия с кремнием с нуля.

Мы не просто создаем еще один компилятор. Мы переосмысливаем, как программное обеспечение взаимодействует с кремнием, исходя из первых принципов. Старые методы, основанные на громоздких, монолитных бэкендах, устарели. Они медленные, сложные в поддержке и непрозрачные. AsmX G3, с его компилятором ZGEN, меняет это.

В этой статье мы погрузимся в ядро нашего подхода: как наш hwm (Hardware Machine Factory) — компонент, который является, по сути, автономным модулем, — транслирует человекочитаемый ассемблер в чистый машинный код x86_64, который исполняет процессор. Это не магия. Это инженерия.

Модульность как основа. Философия компилятора ZGEN

Ключевая философия AsmX G3 — радикальная модульность. hwm является идеальным примером.

  • HWM (Hardware Machine Factory): Это не часть запутанной логики компилятора. Это независимый, изолированный модуль со своим четким API. Вы можете изменять синтаксический анализ, оптимизацию или управление памятью в ZGEN, и hwm будет продолжать работать, пока вы предоставляете ему данные в оговоренном формате. Это позволяет нам итерировать быстрее и поддерживать систему с минимальными усилиями.

  • IDT (AssemblyInstructionDescriptorTable): Сердце hwm. Вместо того чтобы жестко кодировать тысячи вариаций инструкций в гигантских if-else или switch конструкциях — что является рецептом для катастрофы — мы определили полный интерфейс для каждой инструкции x86_64 в декларативном виде. IDT — это наша конституция, единый источник правды для набора инструкций. Если мы хотим добавить поддержку новой инструкции, мы просто обновляем этот интерфейс. Компилятор мгновенно "узнает" о ней.

    Фрагмент IDT (Instruction Descriptor Table). Вместо жесткого кода — декларативное описание набора инструкций. Это наш единый источник правды.
    Фрагмент IDT (Instruction Descriptor Table). Вместо жесткого кода — декларативное описание набора инструкций. Это наш единый источник правды.
  • RDB (RegisterDB): Подобно тому, как IDT определяет инструкции (действия), RDB определяет инструменты для этих действий — регистры. Это простая, но мощная база данных, которая сопоставляет имена регистров (raxr15cl) с их машинными кодами, размерами (8, 16, 32, 64 бит) и особыми требованиями, например, необходимостью использования префикса REX. Это устраняет "магические числа" из логики кодирования и делает процесс прозрачным и менее подверженным ошибкам.

Кратко об архитектурах: CISC — это наш холст

Две философии проектирования кремния. RISC (снизу) делает ставку на простоту: инструкции фиксированной длины, которые легко распараллеливать (конвейеризировать). CISC (вверху), наш холст, выбирает мощь: инструкции переменной длины, которые могут выполнять сложные задачи, но требуют внутреннего декодирования в микрооперации для эффективного исполнения. Мы принимаем эту сложность и строим инструменты, чтобы ей управлять.
Две философии проектирования кремния. RISC (снизу) делает ставку на простоту: инструкции фиксированной длины, которые легко распараллеливать (конвейеризировать). CISC (вверху), наш холст, выбирает мощь: инструкции переменной длины, которые могут выполнять сложные задачи, но требуют внутреннего декодирования в микрооперации для эффективного исполнения. Мы принимаем эту сложность и строим инструменты, чтобы ей управлять.

Прежде чем мы углубимся в детали, важно понимать, с чем мы работаем.

  • RISC (Reduced Instruction Set Computer): Философия простоты. Команды фиксированной длины, простые операции, которые выполняются очень быстро. Элегантно, но требует от компилятора большей работы по разбиению сложных задач на примитивы.

  • CISC (Complex Instruction Set Computer): Архитектура, к которой принадлежит x86_64. Инструкции переменной длины, способные выполнять сложные операции за один шаг (например, прочитать из памяти, выполнить вычисление и записать обратно). Это мощно, но плата за эту мощь — сложность кодирования.

Мы работаем в мире CISC. Это сложный, но невероятно мощный холст. Наша задача — укротить эту сложность с помощью превосходного программного инжиниринга.

Холст: Понимание архитектуры набора инструкций (ISA) x86-64

Прежде чем мы углубимся в детали, важно понимать, с чем мы работаем.

Что такое ISA?
Архитектура набора инструкций (Instruction Set Architecture) — это фундаментальный контракт между аппаратным и программным обеспечением. Это язык, на котором говорит процессор. Он определяет, какие операции может выполнять процессор и как эти операции кодируются в двоичном виде.

Философия CISC
x86-64 — это яркий представитель архитектуры CISC (Complex Instruction Set Computer). В отличие от RISC (Reduced Instruction Set Computer), которая стремится к простым инструкциям фиксированной длины, CISC использует инструкции переменной длины, способные выполнять сложные многошаговые операции. Например, одна инструкция ADD в x86-64 может прочитать значение из памяти, сложить его со значением из регистра и записать результат обратно в регистр.

Эта сложность — наследие десятилетий обратной совместимости, но она же и дает огромную мощь. Наша задача — не жаловаться на сложность, а укротить ее с помощью превосходного программного инжиниринга.

От байтов к действию: жизненный цикл инструкции в современном процессоре x86-64

Представление о том, что процессор последовательно выполняет одну инструкцию за другой, безнадежно устарело. Современный процессор x86-64 — это не просто исполнитель, это сложнейшая система прогнозирования, параллелизации и переупорядочивания, спроектированная для одной цели: максимальная производительность. Цель — не просто выполнять инструкции, а выполнять как можно больше инструкций за один тактовый цикл (Instructions Per Clock, IPC).

Этот процесс можно условно разделить на две большие части: Front-end, который отвечает за поставку и подготовку инструкций, и Back-end, который занимается их фактическим исполнением, часто в совершенно ином порядке, чем в программе.

The Front-End: Загрузка и подготовка инструкций

  1. Выборка (Fetch): Все начинается с регистра RIP (Instruction Pointer), который указывает на адрес следующей инструкции в памяти. Блок выборки не просто считывает одну инструкцию; он агрессивно предзагружает (pre-fetches) непрерывный поток байт из кэша инструкций первого уровня (L1 cache) в свой внутренний буфер. Скорость этого этапа критична — промах в кэше (cache miss) и необходимость обращения к более медленным уровням памяти (L2, L3, RAM) — это катастрофа для производительности.

  2. Декодирование (Decode): Это "узкое горлышко" и самая сложная часть для CISC-архитектуры. Декодер анализирует поток байт, полученный от блока выборки. Поскольку инструкции x86-64 имеют переменную длину (от 1 до 15 байт), декодер должен сначала определить границы каждой инструкции. Затем он выполняет свою главную задачу: разбивает каждую сложную CISC-инструкцию на последовательность простых, RISC-подобных микроопераций (μops). Современные процессоры Intel и AMD имеют несколько декодеров, работающих параллельно: "простые" декодеры быстро обрабатывают инструкции, которые преобразуются в одну микрооперацию, в то время как "сложные" декодеры обрабатывают команды, требующие нескольких микроопераций.

  3. Прогнозирование ветвлений (Branch Prediction): Инструкции условных (JNEJZ) и безусловных (JMPCALL) переходов — это яд для конвейера. Когда процессор встречает переход, он не знает, какой код выполнять дальше, пока результат условия не будет вычислен. Ожидание привело бы к простою конвейера на многие такты. Чтобы этого избежать, в игру вступает блок предсказания ветвлений (Branch Prediction Unit, BPU). Это, по сути, нейронная сеть, обученная на истории предыдущих переходов. BPU делает предположение, по какой ветке пойдет выполнение, и процессор начинает спекулятивно выполнять инструкции из предсказанного пути, не дожидаясь фактического результата. Если предсказание верно (в современных CPU точность > 95%), выигрыш в производительности огромен. Если нет, все спекулятивно выполненные операции отбрасываются, и конвейер перезагружается с правильного адреса, что приводит к значительным штрафам по тактам.

The Back-End: Выполнение вне очереди (Out-of-Order Execution)

Порядок программы — это предложение, а не приказ. Это Back-End, ядро Out-of-Order исполнения. Здесь CISC-инструкции, уже разбитые на простые микрооперации (μOPs), освобождаются от оков своей первоначальной последовательности. Scheduler действует как центр управления полетами, отправляя готовые к выполнению операции в рой параллельных исполнительных блоков (Ports 0-7). Ключевая технология — Register Renaming (обратите внимание: 16 архитектурных целочисленных регистров против 180 физических), которая устраняет ложные зависимости и открывает уровень параллелизма, невозможный в последовательной модели. Цель — максимальная пропускная способность. Мы создаем инструменты, чтобы эффективно кормить этого зверя.
Порядок программы — это предложение, а не приказ. Это Back-End, ядро Out-of-Order исполнения. Здесь CISC-инструкции, уже разбитые на простые микрооперации (μOPs), освобождаются от оков своей первоначальной последовательности. Scheduler действует как центр управления полетами, отправляя готовые к выполнению операции в рой параллельных исполнительных блоков (Ports 0-7). Ключевая технология — Register Renaming (обратите внимание: 16 архитектурных целочисленных регистров против 180 физических), которая устраняет ложные зависимости и открывает уровень параллелизма, невозможный в последовательной модели. Цель — максимальная пропускная способность. Мы создаем инструменты, чтобы эффективно кормить этого зверя.

После того как инструкции преобразованы в микрооперации, они попадают в "сердце хаоса" — исполнительное ядро, работающее по принципу "out-of-order" (OoO). Цель OoO — найти в потоке микроопераций те, которые не зависят друг от друга, и выполнить их параллельно, даже если в исходной программе они стояли далеко друг от друга.

  1. Переименование регистров (Register Renaming): Это ключевая технология, позволяющая реализовать OoO. В архитектуре x86-64 всего 16 целочисленных регистров общего назначения. Это создает массу ложных зависимостей по данным (например, когда одна инструкция хочет записать в RAX, а следующая, не связанная с ней, тоже хочет записать в RAX). Чтобы решить эту проблему, процессор имеет гораздо больше физических регистров (сотни), чем архитектурных. На этом этапе каждая микрооперация, использующая архитектурный регистр (RAXRCX и т.д.), получает взамен уникальный физический регистр из пула. Это разрывает ложные зависимости и дает исполнительному ядру свободу действий.

  2. Диспетчеризация (Dispatch/Issue): Микрооперации помещаются в буфер, называемый планировщиком или станцией резервирования (Reservation Station). Здесь они ожидают, пока их операнды (которые теперь являются физическими регистрами) станут доступны. Как только все операнды для конкретной микрооперации готовы, она помечается как готовая к исполнению.

  3. Исполнение (Execute): Планировщик отправляет готовые микрооперации на свободные исполнительные блоки (Execution Units). Современный процессор является суперскалярным, то есть у него есть несколько таких блоков, работающих параллельно: несколько ALU (арифметико-логических устройств), блоки для загрузки/сохранения в память (AGU - Address Generation Unit), блоки для операций с плавающей запятой (FPU) и т.д. В один такт могут исполняться 4, 6 и более микроопераций одновременно.

  4. Завершение и отставка (Completion and Retirement): Результаты выполненных (возможно, спекулятивно) микроопераций хранятся во временном буфере, который называется буфером переупорядочения (Re-Order Buffer, ROB). Наконец, блок отставки (Retirement Unit) восстанавливает порядок. Он просматривает ROB и "утверждает" результаты микроопераций строго в том порядке, в котором они шли в исходной программе. Только на этом этапе результаты записываются в архитектурные регистры или память, становясь видимыми для остальной программы. Если выясняется, что предсказание ветвления было неверным, блок отставки просто отбрасывает все спекулятивные результаты из ROB, и состояние процессора откатывается к моменту неверного предсказания.

Этот сложный механизм гарантирует, что, несмотря на весь внутренний параллелизм и переупорядочивание, с точки зрения программиста программа выполняется так, как будто все инструкции шли строго одна за другой.

Анатомия Инструкции x86_64: Декодирование Кремния

Забудьте о простых командах фиксированной длины. Это CISC. Инструкции здесь — это составные существа, длиной от 1 до 15 байт. Каждая инструкция — это последовательность полей, большинство из которых опциональны. Понимание этой структуры — ключ к пониманию того, как наш hwm превращает абстракции в биты.

Вот чертеж стандартной инструкции x86_64:

Давайте разберем каждый блок.

1. Префиксы (Prefixes) — Модификаторы Поведения

Это опциональные байты (от 1 до 4), которые изменяют поведение основной инструкции. Они как флаги в командной строке. Самые важные для нас:

  • Префикс REX (0x40 - 0x4F): Это король 64-битного режима. Без него мы застряли бы в 32-битном мире. Он состоит из 4 битов-флагов:

    • W (Width): Если установлен (1), операция работает с 64-битными операндами (RAXRBX). Если сброшен (0), операция 32-битная (EAXEBX). Это фундаментальное различие.

    • R (Register): Расширяет поле Reg в байте ModR/M, давая доступ к регистрам r8-r15.

    • X (Index): Расширяет поле Index в байте SIB для доступа к r8-r15 в качестве индексного регистра.

    • B (Base): Расширяет поле R/M в ModR/M (или Base в SIB) для доступа к r8-r15.

  • Префикс размера операнда (0x66): В 64-битном режиме он переключает операцию с 32-битной (по умолчанию) на 16-битную.

  • Префикс размера адреса (0x67): Позволяет использовать 32-битную адресацию в 64-битном режиме.

Наш hwm автоматически вычисляет и добавляет необходимый REX префикс на основе регистров и размера операндов, которые вы ему передаете. Никакой ручной работы.

2. Опкод (Opcode) — Глагол

Это сердце инструкции, её "глагол". Он говорит процессору, что делать: ADDMOVCMP. Опкод может занимать от 1 до 3 байт. В CISC-архитектуре опкод не всегда полностью определяет операцию. Иногда часть информации "зашита" в других байтах, например, в ModR/M.

3. Байт ModR/M — Швейцарский Нож Адресации

Это, пожалуй, самый важный байт после опкода. Он определяет, откуда брать операнды и куда класть результат. Это настоящий швейцарский нож, состоящий из трех полей:

  • Mod (Mode) [2 бита]: Определяет режим адресации.

    • 00: Адресация через регистр ([RAX]). Есть исключения.

    • 01: Адресация через регистр плюс 8-битное смещение ([RAX + 0x10]).

    • 10: Адресация через регистр плюс 32-битное смещение ([RAX + 0x12345678]).

    • 11: Операнд — это другой регистр, а не адрес в памяти (RAX).

  • Reg/Opcode [3 бита]: Его назначение зависит от опкода. Чаще всего он указывает на регистр-операнд. Но для некоторых инструкций (например, ADD r/m, imm) эти 3 бита служат расширением опкода, уточняя, что именно нужно сделать. Наш IDT хранит эту информацию в поле modrmRegExtension.

  • R/M (Register/Memory) [3 бита]: Указывает на второй операнд. Если Mod не 11, это регистр, используемый для вычисления адреса в памяти. Если Mod равен 11, это просто другой регистр.

4. Байт SIB (Scale, Index, Base) — Для Сложных Адресов

Если ModR/M — это швейцарский нож, то SIB — это мультитул. Он нужен, когда адресация становится сложнее, чем просто [регистр + смещение]. Он кодирует адреса вида [Base + Index * Scale].

  • Scale [2 бита]: Множитель для индексного регистра (1, 2, 4 или 8).

  • Index [3 бита]: Индексный регистр.

  • Base [3 бита]: Базовый регистр.

hwm автоматически определяет, нужен ли SIB-байт, и генерирует его, если вы используете сложную адресацию.

5. Смещение (Displacement) и Непосредственное значение (Immediate)

Это необязательные "данные" инструкции.

  • Displacement: Смещение адреса в памяти. Может быть 1, 2 или 4 байта.

  • Immediate: Константное значение, используемое в операции (например, ADD RAX, 10). Может быть 1, 2, 4 или 8 байт.

Арсенал: Регистры x86_64

Регистры — это сверхбыстрая память прямо на кристалле процессора. Это рабочие столы, на которых происходят все вычисления. Наша RegisterDB знает о них всё.

Регистры общего назначения (GPRs)

Это основные регистры общего назначения. Ключевая особенность x86-64 — историческая преемственность в именах, которая отражает их эволюцию:

64-бит

32-бит

16-бит

8-бит (High/Low)

Описание

RAX

EAX

AX

AH/AL

Аккумулятор, часто используется для возвращаемых значений

RBX

EBX

BX

BH/BL

Базовый регистр, часто используется для указателей

RCX

ECX

CX

CH/CL

Счетчик, используется в циклах и сдвигах

RDX

EDX

DX

DH/DL

Регистр данных, используется в операциях умножения/деления

RSP

ESP

SP

SPL

Указатель стека

RBP

EBP

BP

BPL

Указатель базы стека, для доступа к локальным переменным

RSI

ESI

SI

SIL

Индекс источника (Source Index)

RDI

EDI

DI

DIL

Индекс назначения (Destination Index)

R8-R15

R8D-R15D

R8W-R15W

R8B-R15B

Новые 8 регистров, доступные только в 64-битном режиме (требуют REX)

Специальные Регистры

  • RIP (Instruction Pointer): Указывает на следующую инструкцию для выполнения. Вы не можете напрямую изменять его, но инструкции вроде JMPCALLRET делают это за вас.

  • RFLAGS: Хранит флаги состояния после операций. ZF (Zero Flag), CF (Carry Flag), SF (Sign Flag) — это то, что делает условные переходы (JNEJC) возможными.

SIMD Регистры (MMX, SSE, AVX)

Это широкие регистры (64, 128, 256, 512 бит) для параллельной обработки данных (Single Instruction, Multiple Data). Идеально подходят для графики, научных вычислений и криптографии.

HWM изнутри: Фабрика по сборке машинного кода

Мы не строим черный ящик. Мы строим прозрачную, детерминированную сборочную линию. Традиционные компиляторы смешивают парсинг, семантический анализ и генерацию кода в один сложный, хрупкий монолит. Мы отвергли этот подход.

hwm работает как высокотехнологичная фабрика. У нее есть четко определенные отделы, каждый из которых выполняет одну задачу и делает ее безупречно. Разделение ответственности — не опция, а закон физики для качественного программного обеспечения. Это как у нас в SpaceX: инженеры по двигателям занимаются двигателями, а команда по авионике — авионикой. Если бы мы заставили инженеров по двигателям писать прошивки для навигации, это было бы, эээ... неэффективно, хаха.

Процесс кодирования одной инструкции — это сборочная линия. Можно думать об этом как о пятитактном двигателе, цикле, который преобразует абстрактную идею в физическое действие. Это очень похоже на наш итерационный подход к постройке Starship.

Этап 1: Поступление Заказа (compile метод)

Это входная точка. Фабрика получает заказ: мнемонику (например, add) и массив операндов (например, RAX[RDI + 8]). Наш API прост и понятен. Он принимает стандартизированные, разобранные операнды, а не сырой текст. Никакой магии, никаких скрытых состояний. Он запускает сборочную линию.

Этап 2: Поиск Чертежа (findMatchingVariant)

Для человека ADD — это одна операция. Для процессора — это целое семейство инструкций, каждая со своей уникальной кодировкой. Наша задача — просеять этот комбинаторный хаос и детерминированно выбрать единственно верный, наиболее компактный чертеж. Это инженерия, а не перебор.
Для человека ADD — это одна операция. Для процессора — это целое семейство инструкций, каждая со своей уникальной кодировкой. Наша задача — просеять этот комбинаторный хаос и детерминированно выбрать единственно верный, наиболее компактный чертеж. Это инженерия, а не перебор.

Это мозг операции. Инструкция ADD в x86_64 — это не одна команда, а более дюжины различных вариантов (вариантов кодирования). ADD reg, regADD mem, regADD reg, imm8ADD RAX, imm32 — все это разные "чертежи" с разными опкодами и правилами.

findMatchingVariant — это наш интеллектуальный фильтр. Он перебирает все варианты, определенные в IDT для данной мнемоники, и выполняет строгую проверку:

  1. Количество операндов: Соответствует ли количество предоставленных операндов чертежу?

  2. Типы операндов: Совпадают ли типы (Reg, Mem, Imm)?

  3. Размеры операндов: Удовлетворяют ли размеры операндов ограничениям варианта? Например, вариант ADD r/m8, imm8 не примет 32-битный регистр.

  4. Разрешение неоднозначностей: Что если несколько чертежей подходят? Например, ADD RAX, 10. Это можно закодировать как ADD r/m64, imm32 (опкод 0x81) или как ADD r/m64, imm8 (опкод 0x83). hwm запрограммирован на выбор наиболее оптимального и компактного варианта — в данном случае, с 8-битным непосредственным значением.

В результате этого этапа мы получаем один, и только один, InstructionVariant — точный чертеж для нашей инструкции.

Этап 3: Инженерный Расчет (HardwareMachineFactoryComputation)

Мы разбили монолитную проблему "вычислить префиксы" на две логические подзадачи.

  • getOperationSize: Определяет "доминирующий" размер операции. Для ADD EAX, EBX это 32 бита. Для ADD RAX, 10 — 64 бита. Эта информация критична для установки флага REX.W.

  • calculateRex: Этот метод — чистая логика. Он берет операнды, выбранный вариант и размер операции и пошагово вычисляет, какие биты префикса REX необходимы.

    • Операция 64-битная? Установить REX.W.

    • Используется регистр r9 в поле Reg? Установить REX.R.

    • Используется r10 как база в адресе памяти? Установить REX.B.

    • Используется r11 как индекс? Установить REX.X.

Этот отдел выдает готовый объект rex, который будет использован на сборочной линии.

Этап 4: Производственный цех (HardwareMachineFactoryEncoder)

Здесь абстракции превращаются в биты. Главный станок здесь — encode_modrm_sib_disp. Он получает операнды и вычисленный rex и выполняет сложную, но строго регламентированную работу по кодированию байтов ModR/M и SIB. Он знает все правила и исключения:

  • Если операнд — регистр, Mod будет 11.

  • Если операнд — [RAX]Mod будет 00.

  • Если операнд — [RBP + 16]Mod будет 01, и потребуется 8-битное смещение.

  • Если используется RSP в качестве базы, обязательно нужен SIB.

  • Если адресация относительно RIPMod будет 00, а R/M — 101b.

На выходе мы получаем три компонента: modrmsib (может быть null) и displacement (может быть null).

Этап 5: Финальная Сборка

Метод compile собирает все воедино в правильном порядке:

// Псевдокод финальной сборки в hwm.compile()
function compile(mnemonic, operands) {
  // Этап 2: Найти чертеж
  variant = findMatchingVariant(mnemonic, operands);

  // Этап 3: Инженерные расчеты
  opSize = Computation.getOperationSize(operands, variant);
  rex = Computation.calculateRex(operands, variant, opSize);

  // Этап 4: Производство компонентов
  { modrm, sib, displacement } = Encoder.encode_modrm_sib_disp(...);

  // Этап 5: Сборка
  final_bytes = [];

  // Добавляем префиксы (если нужны)
  if (opSize === 16-bit) final_bytes.push(0x66);
  if (rex.needed) final_bytes.push(calculateRexValue(rex));

  // Добавляем опкод
  final_bytes.push(variant.opcode);

  // Добавляем ModR/M и SIB (если нужны)
  if (variant_requires_modrm) {
    final_bytes.push(encode_modrm(modrm));
    if (sib) final_bytes.push(encode_sib(sib));
  }

  // Добавляем смещение и непосредственное значение
  if (displacement) final_bytes.push(displacement);
  if (immediate) final_bytes.push(immediate);

  // Готовый продукт
  return Buffer.from(final_bytes);
}

Эта модульная, пошаговая архитектура — наш ответ на сложность CISC. Мы не пытаемся решить все проблемы одной гигантской функцией. Мы строим систему, где каждая часть выполняет свою работу предсказуемо и эффективно. hwm не просто кодирует инструкции; он является воплощением нашей философии — создавать сложные системы из простых, надежных и тестируемых компонентов.

Заключение

Мы не просто прошлись по верхам. Мы вскрыли процессор, препарировали его язык и спроектировали сборочную линию для его производства. hwm — это не просто кодировщик. Это заявление. Заявление о том, что со сложностью можно и нужно бороться с помощью чистого инжиниринга, модульности и следования первым принципам.

Традиционный подход к созданию компиляторов — это путь к созданию монолитных, хрупких систем. Мы выбрали другой путь. AsmX G3 — это система, построенная из независимых, тестируемых и понятных компонентов. hwm — лишь первый из них.

Следующий шаг? Масштабирование. Мы будем расширять IDT, добавлять поддержку новых инструкций, интегрировать более сложные стратегии оптимизации. Но фундаментальный принцип останется неизменным: строить сложные системы из простых, надежных частей.

Мы не просто пишем компилятор. Мы строим будущее того, как создается программное обеспечение.

Справочник: Инженерные Принципы Кремния

Это не просто словарь. Это разбор фундаментальных концепций, первых принципов, на которых построен современный процессор. Понимание этих принципов — это то, что отличает простого кодера от инженера, способного создавать инструменты, которые работают на уровне физических законов вычислений.

  • HWM (Hardware Machine Factory): Наш модульный компонент для трансляции ассемблерных инструкций в машинный код.

  • IDT (AssemblyInstructionDescriptorTable): Декларативная база данных, описывающая все поддерживаемые варианты инструкций, их опкоды и правила кодирования. "Конституция" hwm.

  • RDB (RegisterDB): База данных, хранящая информацию о регистрах процессора: их коды, размеры и специальные требования.

  • ISA (Instruction Set Architecture / Архитектура набора инструкций)

    • Концепция: Это не просто "словарь" процессора. Это фундаментальный общественный договор между аппаратным и программным обеспечением. ISA определяет не только инструкции, но и видимые программисту регистры, типы данных, режимы адресации, модель памяти и архитектуру прерываний. Это API для кремния.

    • Технически: Для x86_64, ISA — это результат десятилетий эволюции, включающий в себя 16-, 32- и 64-битные режимы работы. Она определяет наличие регистров GPR (RAX, RBX, etc), SIMD (XMM, YMM, ZMM), управляющих регистров (CR0-CR15), а также правила работы с памятью, включая сегментацию (в основном, унаследованную) и страничную организацию (ключевую для современных ОС).

    • В AsmX G3: Наша цель — предоставить чистый, мощный и безопасный интерфейс к этой сложной и, местами, перегруженной архитектуре. Мы абстрагируем опасные и устаревшие части, давая разработчику прямой доступ к мощи 64-битного режима.

  • CISC (Complex Instruction Set Computer / Компьютер со сложным набором инструкций)

    • Концепция: Философия "одна инструкция — одна сложная задача". Вместо того чтобы заставлять компилятор собирать сложные операции из десятков примитивов, CISC предоставляет мощные, многошаговые инструкции прямо в "железе".

    • Технически: Характеризуется инструкциями переменной длины, большим количеством режимов адресации и наличием инструкций, работающих напрямую с памятью (memory-to-memory или register-to-memory). Современные CISC-процессоры, такие как x86_64, на самом деле являются гибридами: они транслируют сложные CISC-инструкции в последовательность более простых, RISC-подобных микроопераций (μops) для внутреннего исполнения.

    • В AsmX G3: Мы принимаем эту сложность как данность. Наш hwm спроектирован так, чтобы мастерски справляться с вариативностью и сложностью кодирования CISC, предоставляя разработчику простой и предсказуемый интерфейс.

  • Опкод (Opcode / Код операции)

    • Концепция: Глагол машины. Основная часть инструкции, которая говорит процессору, что нужно сделать.

    • Технически: В x86_64 опкод может иметь длину 1, 2 или 3 байта. Он редко бывает самодостаточным. Часто его точное значение модифицируется префиксами или уточняется полями в байте ModR/M. Например, опкод 0x81 может означать ADD, OR, ADC, SUB и т.д., в зависимости от 3-битного поля Reg/Opcode в байте ModR/M.

    • В AsmX G3: Наш IDT сопоставляет мнемоники (add, mov) с их базовыми опкодами и правилами, которые определяют, как другие байты (например, ModR/M) влияют на финальную операцию.

  • Байт ModR/M (Mod, Register/Memory)

    • Концепция: Этот байт, следующий сразу за опкодом, — это компьютер наведения для операции. Он не просто указывает направление; он точно определяет, что является операндом (регистр или ячейка памяти) и как до него добраться (режим адресации).

    • Технически: Этот 8-битный байт разделен на три поля:

      • Mod [биты 7-6]: Режим адресации. Определяет, является ли операнд регистром или адресом в памяти, и если это адрес, то как он формируется.

        • 00: Адресация через регистр ([RAX]), со специальными случаями для [RIP + смещение] и [только смещение].

        • 01: Адресация через регистр + 8-битное смещение со знаком ([RAX + 12h]).

        • 10: Адресация через регистр + 32-битное смещение со знаком ([RAX + 12345678h]).

        • 11: Операнд является регистром, а не ячейкой памяти.

      • Reg/Opcode [биты 5-3]: Обычно указывает на регистр, который является одним из операндов (источником или назначением). В некоторых случаях эти биты служат расширением опкода, выбирая конкретную операцию из группы (например, ADD вместо OR для опкода 0x81).

      • R/M [биты 2-0]: Указывает на второй операнд. В зависимости от поля Mod, это может быть либо регистр, либо регистр, используемый для вычисления адреса памяти (возможно, в сочетании с SIB).

    • В AsmX G3: Модуль HardwareMachineFactoryEncoder — это наш эксперт по навигации. Он анализирует предоставленные операнды и безупречно вычисляет биты ModR/M.

  • Байт SIB (Scale, Index, Base)

    • Концепция: Усовершенствованная система навигации для сложных траекторий в памяти. Используется, когда простая адресация недостаточна.

    • Технически: Этот необязательный байт следует за ModR/M и позволяет формировать адреса по формуле [Base + Index * Scale]. Он необходим для эффективной работы с массивами и сложными структурами данных.

      • Scale [биты 7-6]: Множитель для индексного регистра. Значения 00, 01, 10, 11 кодируют множители 1, 2, 4, 8 соответственно.

      • Index [биты 5-3]: Индексный регистр (например, RCX).

      • Base [биты 2-0]: Базовый регистр (например, RAX).

    • В AsmX G3: Вам не нужно думать о SIB. Если вы пишете [rax + rcx*4], hwm автоматически определит необходимость SIB и сгенерирует его с правильными параметрами.

  • Префикс REX

    • Концепция: "Стартовый ключ" для 64-битного режима. Без него процессор не знает, что вы хотите использовать всю его мощь.

    • Технически: Один байт в диапазоне 0x40 - 0x4F, используемый только в 64-битном режиме. Он не меняет операцию, а расширяет ее возможности. Содержит 4 ключевых флага:

      • W (bit 3): 0 = 32-битный размер операнда (по умолчанию), 1 = 64-битный размер операнда. Это самый важный флаг.

      • R (bit 2): Расширяет поле Reg в ModR/M, позволяя ему ссылаться на регистры R8-R15 в дополнение к RAX-RDI.

      • X (bit 1): Расширяет поле Index в байте SIB, позволяя использовать R8-R15 в качестве индексных регистров.

      • B (bit 0): Расширяет поле R/M в ModR/M (или поле Base в SIB), позволяя использовать R8-R15.

    • В AsmX G3: Модуль HardwareMachineFactoryComputation автоматически анализирует ваши операнды. Видит R15? Включает нужные биты REX. Видит qword ptr? Включает бит REX.W. Все происходит автоматически. Мы устраняем человеческий фактор.

Post Scriptum: Итерация — это всё. Обновление до rev 2.0

Разработка — это не статичный процесс. Это постоянная итерация. Пока эта статья готовилась, мы уже выпустили обновление rev 2.0, которое решает критическую проблему и расширяет наши возможности. Это наш подход: быстро выявлять проблемы, исправлять их и двигаться дальше.

Критический сбой при парсинге инструкций без операндов.

Надежность — это не характеристика. Это фундаментальное требование. Система давала сбой с Unhandled Rejection, когда наш парсер встречал простейшие инструкции, такие как nop или pushf. Это неприемлемо.

Сбой — это вариант. Если у вас не бывает сбоев, вы недостаточно инновационны. Мы нашли этот сбой и устранили его.
Сбой — это вариант. Если у вас не бывает сбоев, вы недостаточно инновационны. Мы нашли этот сбой и устранили его.

Проблема коренилась в ложном допущении, заложенном в методе zcc_build_generic_instruction: предположении, что у любой инструкции всегда есть операнды. Это нарушение первого принципа — всегда проверять входные данные. Парсер пытался получить доступ к ast[0].type, когда массив ast был пуст, что приводило к фатальному null reference.
Было (Хрупкий код):

static zcc_build_generic_instruction(ast) {
  // Падает, если ast[0] не существует
  if (ast[0].type == TypeOfAtomicExpression.ARGUMENTS) { 
    return ast[0].body.values;
  }
  return ast;
}

Стало (Антихрупкий код):

static zcc_build_generic_instruction(ast) {
  // Безопасная проверка с помощью optional chaining
  if (ast[0]?.type === TypeOfAtomicExpression.ARGUMENTS) { 
    return ast[0].body.values;
  }
  return ast;
}

Мы применили простое, но элегантное решение: опциональную цепочку (?.). Теперь код не предполагает, а проверяет.

Это не просто исправление бага. Это итерация в сторону более надежной, отказоустойчивой системы. Мы не латаем дыры; мы укрепляем фундамент.

Добавлено: Поддержка базовых инструкций amd64 без операндов.

Исправление бага открыло нам дорогу для расширения набора инструкций. Теперь hwm полностью поддерживает:

  • nop: No Operation

  • fwait: Check pending unmasked floating-point exceptions

  • pushf: Push rFLAGS Register onto the Stack

  • popf: Pop Stack into rFLAGS Register

  • sahf: Store AH into Flags

  • lahf: Load Status Flags into AH Register

Это не просто добавление команд. Это расширение инструментария для тех, кто хочет писать код максимально близко к "железу". Каждое такое добавление проверяется нашими юнит-тестами, гарантируя, что то, что мы обещаем, работает.

hwm: unit-tests
Итерация и проверка. Каждый компонент hwm покрыт тестами, гарантируя детерминированность и корректность кодирования. Мы не надеемся, мы проверяем.
Итерация и проверка. Каждый компонент hwm покрыт тестами, гарантируя детерминированность и корректность кодирования. Мы не надеемся, мы проверяем.

Комментарии (6)