8. ISA. Интерфейс между железом и софтом

В предыдущей главе 7. Цикл исполнения инструкции. Fetch, Decode, Execute мы проследили за тем, как процессор читает байты из памяти, дешифрует их и запускает вычисления. Но здесь неизбежно возникает фундаментальный вопрос:

Откуда процессор знает, что байт 0x01 означает сложение, а не прыжок? Кто решает, какие именно байты обязан сгенерировать компилятор Go? И почему бинарник, скомпилированный на ноутбуке с процессором Intel, без единого изменения запускается на сервере с процессором AMD, но откажется работать на сервере с процессором AWS Graviton?

Ответ на все эти вопросы дает величайшее изобретение в истории вычислительной техники — ISA (Instruction Set Architecture — Архитектура системы команд).

Если выразить суть ISA языком бэкенд-разработчика: ISA — это официальный публичный API процессора.

Все аппаратные узлы, которые мы кропотливо разбирали в 6. Анатомия CPU. Datapath, Control Unit и Register File (размеры кэшей, ширина шин, каскады сумматоров Carry-Lookahead, мультиплексоры), — это скрытые детали приватной реализации (микроархитектура). Вы, как программист, не имеете к ним прямого доступа. Вы общаетесь с кремниевым кристаллом исключительно через публичный контракт — ISA.


Интерфейс и Реализация

В 1964 году легендарная команда инженеров IBM под руководством Фреда Брукса (архитекторами системы были также Джин Амдал и Геррит Блау) представила миру мейнфрейм IBM System/360. До этого момента каждая новая модель компьютера проектировалась с чистого листа: менялись наборы команд, форматы чисел, разрядность регистров. Программы, написанные для старой машины, приходилось полностью переписывать под новую.

System/360 впервые разделила компьютер на два независимых слоя:

  1. Архитектуру (Architecture / ISA): функциональное поведение процессора глазами программиста и компилятора;
  2. Микроархитектуру (Implementation): физическое воплощение этого поведения в транзисторах, проводниках и тактовой частоте.
flowchart TD
    subgraph Software ["Мир программного обеспечения (Go, Linux)"]
        GoProg["Go-программа / Скомпилированный бинарник"]
    end

    subgraph API ["ISA: Публичный контракт (API)"]
        Contract["Набор инструкций (Opcodes)<br>Регистры (RAX, RBX...)<br>Способы адресации памяти<br>Модель памяти (Memory Model)"]
    end

    subgraph Hardware ["Микроархитектура: Приватная реализация (Транзисторы)"]
        Intel["Intel Core (например, Golden Cove)<br>Кэш L3: 30 МБ (в Core i9-12900K), 6 декодеров"]
        AMD["AMD Ryzen / EPYC (Zen 4/Zen 5)<br>Чиплетная компоновка, 4.5 ГГц"]
    end

    GoProg --> Contract
    Contract --> Intel
    Contract --> AMD

    classDef soft fill:#131d33,stroke:#38bdf8,stroke-width:1px,color:#f8fafc;
    classDef api fill:#1e293b,stroke:#10b981,stroke-width:1px,color:#f8fafc;
    classDef hard fill:#0f172a,stroke:#f59e0b,stroke-width:1px,color:#f8fafc;
    class Software,GoProg soft;
    class API,Contract api;
    class Hardware,Intel,AMD hard;
  

ISA строго регламентирует:

  • Словарь операций (Opcodes): какие математические, логические и управляющие команды умеет выполнять чип (ADD, SUB, MOV, JMP);
  • Регистровую модель: сколько регистров доступно коду, какова их разрядность и как они называются (например, 64-битные RAX в x86-64 или X0 в ARM64);
  • Адресацию памяти: как процессор обращается к байтам RAM (базовые смещения, сегментация, выравнивание);
  • Модель памяти (Memory Model): разрешено ли аппаратуре переставлять местами чтение и запись данных между ядрами;
  • Обработку исключений и прерываний: что происходит при делении на ноль или нарушении прав доступа к памяти (Page Fault).

Два лагеря: CISC vs RISC

За полувековую историю микропроцессоров в схемотехнике сформировались две главные философские школы, с противостоянием которых сегодня сталкивается любой бэкенд-инженер.

flowchart TD
    subgraph Philosophies ["CISC vs RISC"]
        direction LR
        subgraph CISC ["CISC (x86-64)"]
            C1["Сложные, многозадачные команды"]
            C2["Переменная длина (1–15 байт)"]
            C3["Прямая работа с памятью в ALU"]
            C4["Компактный размер бинарника"]
        end
        subgraph RISC ["RISC (ARM64, RISC-V)"]
            R1["Простые элементарные команды"]
            R2["ARM64: строго 4 байта; RISC-V: базовые 4 байта, RVC — 2 байта"]
            R3["Архитектура Load/Store"]
            R4["Высокая энергоэффективность"]
        end
    end

    classDef cisc fill:#1e293b,stroke:#6366f1,stroke-width:1px,color:#f8fafc;
    classDef risc fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc;
    class CISC,C1,C2,C3,C4 cisc;
    class RISC,R1,R2,R3,R4 risc;
  

1. CISC (Complex Instruction Set Computer)

  • Флагманские представители: Архитектура x86 и x86-64 (Intel, AMD).
  • Исторический контекст: Появилась на заре микроэлектроники (процессор Intel 8086 в 1978 году). В те времена оперативная память стоила баснословных денег, а компиляторы были примитивными. Инженеры стремились сделать инструкции как можно мощнее, чтобы одна строка на ассемблере выполняла целую последовательность действий.
  • Особенности:
    • Богатейший словарный запас: Более 1 500 инструкций. Одна команда может одновременно загрузить число из RAM, сложить его с регистром и отправить результат обратно в память (ADD [RBP-8], RAX). Впрочем, внутри ядро всё равно разбивает такую команду на несколько микроопераций (load + ALU + store).
    • Переменная длина инструкций: Команды x86 могут занимать от 1 до 15 байт. Это обеспечивает высочайшую плотность машинного кода (бинарники получаются компактными), но превращает блок аппаратного декодирования инструкций в сложнейший и энергоемкий узел чипа.

2. RISC (Reduced Instruction Set Computer)

  • Флагманские представители: ARM64 (AArch64 — чипы Apple Silicon M-серии, AWS Graviton, Ampere Altra) и открытый стандарт RISC-V.
  • Исторический контекст: В 1980-х годах исследователи (во главе с Джоном Хеннесси из Стэнфорда и Дэвидом Паттерсоном из Беркли) доказали: 80% времени программы используют лишь 20% самых простых инструкций. Сложные инструкции только замедляют базовый тактовый цикл.
  • Особенности:
    • Архитектура Load/Store: Никаких сложений прямо в оперативной памяти! Арифметика (ADD) выполняется исключительно между регистрами. Чтобы изменить число в памяти, вы обязаны явно выполнить три операции:
      1. Загрузить байты из памяти в регистр (LOAD / LDR);
      2. Выполнить математику в регистрах (ADD);
      3. Сохранить результат из регистра обратно в память (STORE / STR).
    • Фиксированная длина команд: В ARM64 каждая инструкция занимает ровно 4 байта (32 бита). Декодеру не нужно анализировать префиксы — следующий опкод гарантированно начинается через каждые 4 байта. Это позволяет делать ядра простыми, холодными и масштабируемыми до сотен ядер на один сокет в облачных ЦОД. В RISC-V базовые инструкции также 32-битны, однако стандартное расширение «C» (Compressed) вводит 16-битные компактные команды: они обязательны для Linux-профилей RISC-V (RVA20/RVA22) и применяются повсеместно в embedded. Тем самым RISC-V поддерживает инструкции переменной длины с шагом 16 бит, что отличает его от строго фиксированного ARM64.

Go и кросс-компиляция (GOARCH)

Многие современные языки программирования абстрагируются от ISA:

  • Python, Ruby и PHP исполняются интерпретатором, который берет на себя общение с ОС;
  • Java и C# компилируются в промежуточный байт-код (JVM / CLR), который транслируется в инструкции процессора уже на лету (JIT-компиляция).

Язык Go спроектирован по фундаментально иной модели.
Go — это компилируемый язык со статической типизацией и моделью Ahead-Of-Time (AOT). Утилита go build не создает виртуальных машин. Она генерирует чистейший машинный код, адресованный конкретной целевой ISA.

За выбор целевой архитектуры отвечает переменная окружения GOARCH:

flowchart TD
    GoSource["Исходный код Go (*.go)"] --> Front["Frontend компилятора Go<br>(Синтаксический анализ, AST, Typecheck)"]
    Front --> SSA["Оптимизатор (cmd/compile)<br>IR: Inlining, Escape Analysis → SSA: Dead Code Elimination и др."]

    SSA --> BackendAMD["Бэкенд: GOARCH=amd64<br>(Генерация инструкций x86-64)"]
    SSA --> BackendARM["Бэкенд: GOARCH=arm64<br>(Генерация инструкций ARM64)"]
    SSA --> BackendRISC["Бэкенд: GOARCH=riscv64<br>(Генерация инструкций RISC-V)"]

    BackendAMD --> ELF_AMD["Исполняемый файл ELF<br>(Серверы Intel / AMD)"]
    BackendARM --> ELF_ARM["Исполняемый файл<br>(ELF: AWS Graviton; Mach-O: Apple Silicon)"]
    BackendRISC --> ELF_RISC["Исполняемый файл ELF<br>(Встраиваемые системы RISC-V)"]

    classDef ssa fill:#1e293b,stroke:#6366f1,stroke-width:1px,color:#f8fafc;
    classDef arch fill:#0f172a,stroke:#38bdf8,stroke-width:1px,color:#f8fafc;
    class SSA ssa;
    class BackendAMD,BackendARM,BackendRISC,ELF_AMD,ELF_ARM,ELF_RISC arch;
  

Команда разработчиков Go совершила технологический подвиг: компилятор Go содержит внутри собственные независимые генераторы машинного кода для десятка архитектур (amd64, arm64, riscv64, ppc64le, s390x).

Вы можете сидеть за ноутбуком MacBook с процессором Apple M3 (darwin/arm64) и одной строчкой в терминале собрать бинарник для серверов продакшна на Intel Xeon:

Bash
GOOS=linux GOARCH=amd64 go build main.go

Компилятору Go не требуется запущенный Linux или процессор x86-64! Он берет промежуточный граф SSA (Static Single Assignment; верхние проходы платформонезависимы, нижние — после этапа lower — уже зависят от архитектуры) и напрямую нарезает байты инструкций x86-64, бережно упаковывая их в стандартный формат исполняемых файлов Linux ELF (Executable and Linkable Format).



Mechanical Sympathy: Псевдо-ассемблер Plan 9

Несмотря на мощь компилятора, в критических местах рантайма Go (планировщик горутин, сборщик мусора, криптография AES-GCM, быстрый хэш runtime/map) инженеры пишут код на чистом ассемблере вручную.

Если вы откроете исходники стандартной библиотеки Go (например, src/runtime/sys_linux_amd64.s), вы заметите, что местный ассемблер выглядит необычно: он не соответствует ни синтаксису Intel, ни синтаксису AT&T.

Причина кроется в корнях языка. Двое из трёх создателей Go — Кен Томпсон и Роб Пайк (третий — Роберт Гризмер) — были ключевыми разработчиками операционной системы Plan 9 в Bell Labs. Для компилятора Go они взяли архитектуру псевдо-ассемблера Plan 9.

Вместо жесткой привязки к конкретным аппаратным регистрам Plan 9 вводит четыре абстрактных псевдо-регистра:

  1. FP (Frame Pointer): виртуальный указатель аргументов функции. Обращение x+0(FP) адресует первый параметр функции в стековой раскладке аргументов (ABI0), которую используют ассемблерные функции. Регистры ABIInternal из ассемблера напрямую недоступны: для этого компилятор генерирует обёртки.
  2. SB (Static Base): виртуальный глобальный указатель на статическую память. Через него адресуются имена функций и глобальные переменные: TEXT ·MyFunction(SB), $0-16.
  3. SP (Stack Pointer): виртуальный указатель вершины локального стека горутины.
  4. PC (Program Counter): псевдоним аппаратного счетчика команд.

Внутренний ассемблер Go (cmd/internal/obj) транслирует этот универсальный псевдокод в реальные машинные биты целевой архитектуры. Благодаря такой модульности добавление поддержки новых ISA (например, перспективной riscv64) в экосистему Go потребовало минимальных изменений в ядре компилятора.



Итог

  1. ISA (Архитектура системы команд) — это публичный контракт (API) между процессором и программным обеспечением. Она скрывает под собой микроархитектурные детали реализации (транзисторы, кэши, шины).
  2. CISC (x86-64): богатый словарь сложных инструкций переменной длины (1–15 байт). Обеспечивает высокую плотность кода, но требует сложного декодера.
  3. RISC (ARM64, RISC-V): минималистичный набор простых команд строго фиксированной длины (4 байта). Использует архитектуру Load/Store (память адресуется только через LOAD и STORE), обеспечивая непревзойденную энергоэффективность.
  4. Кросс-компиляция в Go (GOARCH): компилятор Go генерирует сырой машинный код под любую поддерживаемую ISA без внешних зависимостей. Единственное препятствие — использование CGO (go-sqlite3, confluent-kafka-go), требующее сторонний C-кросс-компилятор (clang / gcc).
  5. Псевдо-ассемблер Plan 9: фирменный промежуточный диалект ассемблера Go, использующий виртуальные регистры (FP, SB, SP), позволяющий писать переносимый низкоуровневый код.

Мы выяснили, что ISA — это словарь команд. Но как эти команды выглядят для человеческого глаза? Как транслируются наши лаконичные функции на Go в инструкции ассемблера, и как научиться читать этот код?

В следующей лекции мы освоим основы машинного языка: 9. Базовый Ассемблер. Как CPU видит код.