TL;DR:

8086 выполняетмногие инструкции через микрокод, а для ряда арифметико‑логических команд микрокод задаёт «псевдо»‑операцию XI, которую аппаратная часть подменяет на операцию из опкода. АЛУ работает с задержкой в один шаг: одна микроинструкция настраивает операцию и вход (временный регистр), следующая забирает результат – поэтому выбор хранится в защёлках/триггерах.

Само АЛУ – 16 одинаковых битовых узлов с двумя LUT‑таблицами и логикой переноса; управляющие сигналы формируются через PLA и подаются через бутстрэп‑драйверы. Из‑за множества особых случаев CISC (сравнение, перенос/заём, инкременты, сдвиги, BCD/ASCII‑коррекции) схема управления заметно сложнее, чем кажется по перечню инструкций.

В 1978 году компания Intel представила процессор 8086, революционный чип, который положил начало современной архитектуре x86. Однако в отличие от современных 64-битных процессоров, 8086 является 16-битным чипом. Его арифметико-логическое устройство (АЛУ) работает с 16-битными значениями, выполняя арифметические операции, такие как сложение и вычитание, а также логические операции, включая побитовые AND, OR и XOR. АЛУ 8086 является сложной частью чипа и в общей сложности поддерживает 28 операций.(примеч.1)

В этой статье я рассказываю о схемотехнике, которая управляет АЛУ, формируя нужные управляющие сигналы для конкретной операции. Процесс сложнее, чем можно ожидать. Во-первых, одна инструкция машинного кода приводит к выполнению нескольких микрокоманд. Работа с АЛУ устроена в два шага: одна микрокоманда (микроинструкция) настраивает АЛУ на требуемую операцию, а вторая микроинструкция получает результат работы АЛУ. Более того, опираясь и на микроинструкцию, и на инструкцию машинного кода, управляющая логика отправляет в АЛУ управляющие сигналы, перенастраивая его под нужную операцию. Таким образом, эта логика служит «клеем» между микроинструкциями и АЛУ.

На фотографии кристалла ниже показан процессор 8086 под микроскопом. Я подписал ключевые функциональные блоки. На уровне архитектуры чип разделён на блок интерфейса шины (Bus Interface Unit, BIU) в верхней части и исполнительный блок (Execution Unit, EU) в нижней. BIU отвечает за работу с шиной и памятью, а также за предвыборку инструкций, тогда как EU выполняет инструкции. В правом нижнем углу ПЗУ микрокода хранит микроинструкции. АЛУ находится в левом нижнем углу: биты 7–0 расположены выше, а биты 15–8 ниже, «обрамляя» схемы флагов состояния. Схемы управления АЛУ, выделенные красным в нижней части чипа, и являются основной темой этой статьи.

The die of the 8086 with the metal layer removed to show the silicon and polysilicon underneath. Click this image (or any other) for a larger version.
Кристалл 8086. Кликните, чтобы открыть в полном размере.

Микрокод

Процессор 8086 реализует большинство машинных инструкций с помощью микрокода, выполняя по одной микроинструкции на каждый шаг машинной инструкции. В 8086 используется любопытная архитектура микрокода: каждая микроинструкция выполняет две несвязанные между собой операции. Первая операция перемещает данные между источником и назначением. Вторая операция может быть чем угодно: от перехода или вызова подпрограммы до чтения/записи памяти или операции АЛУ. Операция АЛУ содержит пятибитное поле, задающее конкретную операцию, и двухбитное поле, указывающее, какой временный регистр задаёт вход АЛУ. Как вы увидите ниже, эти два поля играют важную роль в схемотехнике АЛУ.

Во многих случаях микроинструкция 8086 не задаёт операцию АЛУ напрямую, оставляя детали для подстановки из опкода машинной инструкции. Например, машинные инструкции ADD, SUB, ADC, SBB, AND, OR, XOR и CMP используют один и тот же микрокод, а аппаратная часть выбирает операцию АЛУ по опкоду инструкции. Аналогично, инструкции инкремента и декремента используют общий микрокод, как и инструкции десятичной коррекции DAA и DAS, а также ASCII-коррекции AAA и AAS. Внутри микроинструкции все эти операции выполняются через «псевдо»-операцию АЛУ под названием XI (почему-то). Если микрокод задаёт операцию АЛУ XI, аппаратная часть заменяет её на операцию АЛУ, указанную в инструкции.

Ещё одна важная особенность микрокода в том, что одну микроинструкцию АЛУ нужно выполнить, чтобы настроить работу АЛУ, но результат становится доступен только в последующей микроинструкции, которая переносит результат в назначение. Из этого следует, что аппаратной части приходится «помнить» выбранную операцию АЛУ.

Чтобы стало нагляднее, вот микрокод, который реализует типичную арифметико-логическую инструкцию, например ADD AL, BL или XOR [BX+DI], CX. Этот микрокод состоит из трёх микроинструкций. Левая половина каждой микроинструкции задаёт перемещение данных: сначала два аргумента переносятся во временные регистры АЛУ, а затем сохраняется результат АЛУ (обозначаемый Σ). Правая половина каждой микроинструкции выполняет вторую задачу. Сначала АЛУ настраивается на выполнение операции XI с использованием временного регистра A.

Напомню, XI означает, что операция АЛУ подставляется из машинной инструкции; именно так один и тот же микрокод обслуживает восемь разных типов машинных инструкций. Во второй микроинструкции запускается следующая машинная инструкция, если только не требуется запись результата обратно в память (WB). Последняя микроинструкция — RNI (Run Next Instruction), то есть переход к выполнению следующей машинной инструкции. Она также указывает, что флаги состояния процессора (F) должны быть обновлены, чтобы отразить, равен ли результат АЛУ нулю, положителен ли он, произошло ли переполнение и так далее.(примеч.2)

M → tmpa   XI   tmpa  Загрузить первый аргумент, настроить АЛУ.
R → tmpb   WB,NXT     Загрузить второй аргумент, начать следующую инструкцию, если не требуется обратная запись в память
Σ → M      RNI  F     Store ALU result, Run Next Instruction, update status Flags

Схема АЛУ

АЛУ — сердце процессора, выполняющее арифметические и логические операции. Микропроцессоры 1970-х обычно поддерживали сложение и вычитание, логические AND, OR и XOR, а также различные операции побитового сдвига. (Хотя у 8086 были инструкции умножения и деления, они реализованы микрокодом, а не в самом АЛУ.) Поскольку АЛУ одновременно велико по площади и критично для производительности, архитекторы стремятся оптимизировать его конструкцию.

Поэтому у разных микропроцессоров встречаются весьма разные реализации АЛУ. Например, у микропроцессора 6502 есть отдельные схемы для сложения и для каждой логической операции, а нужный результат выбирается мультиплексором. Intel 8085, напротив, использует оптимизированный «комок» логических элементов, который выполняет требуемую операцию в зависимости от управляющих сигналов, а 4-битное АЛУ Z80 использует другой «комок» логики.

В 8086 выбран иной подход: для каждого бита АЛУ он использует две таблицы поиска (lookup table) вместе с дополнительной логикой, чтобы формировать сигнал переноса и выходной сигнал. Если правильно настроить эти таблицы, АЛУ можно сконфигурировать для выполнения нужной операции. (Это похоже на то, как в FPGA произвольные функции реализуются через таблицы поиска)

На схеме ниже показан узел, реализующий один бит АЛУ. Нас здесь интересуют шесть управляющих сигналов слева. Два мультиплексора (трапециевидные символы) реализуют таблицы поиска: они используют два входных бита аргументов, чтобы по управляющим сигналам выбирать выходы, отвечающие за генерацию переноса и его распространение. Таким образом, подавая в АЛУ нужные управляющие сигналы, 8086 может перенастраивать АЛУ на требуемую операцию. Например, при одном наборе управляющих сигналов эта схема будет выполнять сложение. Другие наборы заставят её выполнять вычитание или вычислять логическую операцию, такую как AND или XOR. В 8086 есть 16 копий этой схемы, поэтому он работает с 16-битными значениями.

The circuit that implements one bit in the 8086's ALU.
Схема, реализующая один бит в АЛУ 8086.

8086 — сложный процессор, а его инструкции содержат множество особых случаев, поэтому управление АЛУ оказывается сложнее, чем описано выше. Например, операция сравнения эквивалентна вычитанию, но числовой результат сравнения отбрасывается, обновляются только флаги состояния. Инструкции сложения и сложения с переносом требуют разных значений переноса, подаваемого в бит 0, тогда как при вычитании требуется инвертировать флаг переноса, поскольку он трактуется как заём. АЛУ 8086 поддерживает операции инкремента и декремента, а также инкремент и декремент на 2, что требует сигнала инкремента в бит 1 вместо бита 0. Все операции побитовых сдвигов требуют особой обработки. Например, при ротации можно учитывать бит переноса или исключать его, а арифметический сдвиг вправо требует дублирования старшего бита.

В результате, помимо шести управляющих сигналов таблиц поиска (LUT), АЛУ требуется множество дополнительных управляющих сигналов, чтобы корректировать его поведение для конкретных инструкций. В следующем разделе я объясню, как формируются эти управляющие сигналы.

Схемы управления АЛУ на кристалле

Диаграмма ниже показывает компоненты логики управления АЛУ в том виде, как они выглядят на кристалле. Информация из микроинструкции поступает справа и сохраняется в защёлках. PLA (Programmable Logic Array, программируемые логические матрицы) декодируют инструкцию и формируют управляющие сигналы. Эти сигналы идут влево, где они управляют АЛУ.

The ALU control logic as it appears on the die. I removed the metal layer to show the underlying polysilicon and silicon. The reddish lines are remnants of the metal.
Логика управления АЛУ в том виде, как она выглядит на кристалле. Я снял металлический слой, чтобы показать нижележащие слои поликремния и кремния. Красноватые линии — это остатки металлизации.

Как объяснялось ранее, если микрокод задаёт операцию XI, поле операции заменяется значением, вычисляемым на основе опкода машинной инструкции. Эта подстановка выполняется мультиплексором XI до того, как значение будет сохранено в защёлке поля операции. Из-за сложности системы команд 8086 операция XI устроена не так прямолинейно, как можно ожидать. Этот мультиплексор получает три бита машинной инструкции из специального регистра под названием «X-регистр», ещё один бит инструкции — из регистра инструкции, а последний бит — из схемы декодирования, называемой Group Decode ROM.(примеч.4)

Напомню: одна микроинструкция задаёт операцию АЛУ, а более поздняя микроинструкция обращается к результату. Следовательно, схемы управления АЛУ должны помнить заданную операцию, чтобы использовать её позже. В частности, управляющая логика должна отслеживать, какую операцию АЛУ выполнить и какой временный регистр выбран. Для отслеживания выбранного временного регистра используются три триггера, по одному триггеру на каждый регистр. Микроинструкция содержит двухбитное поле, задающее временный регистр. Управляющая логика декодирует это поле и активирует соответствующий триггер. Выходы этих триггеров идут в АЛУ и разрешают выбранный временный регистр. В начале каждой машинной инструкции(примеч.5) триггеры сбрасываются, поэтому по умолчанию выбирается временный регистр A.

Для хранения пятибитного поля операции из микроинструкции управляющая логика использует пять триггеров. В начале каждой машинной инструкции триггеры сбрасываются так, что по умолчанию выбирается операция 0 (ADD). Важно, что из этого следует: операцию сложения потенциально можно выполнить без микроинструкции, которая настраивает АЛУ, сократив микрокод на одну микроинструкцию и тем самым уменьшив время выполнения инструкции на один такт.

Пятибитный выход триггеров операции поступает в операционную PLA (примеч.7), которая декодирует операцию в 27 управляющих сигналов.(примеч.6) Многие из этих сигналов идут в АЛУ, где управляют его поведением для особых случаев. Около 15 из этих сигналов поступают в PLA таблиц поиска (Lookup Table, LUT), которая формирует шесть сигналов таблиц поиска для АЛУ. С левой стороны LUT PLA специальные мощные драйверы повышенного тока усиливают управляющие сигналы перед отправкой в АЛУ. Подробности об этих драйверах приведены в сносках.(примеч.8)

Выводы

Каждый раз, когда я смотрю на схемотехнику процессора 8086, я вижу различия между RISC- и CISC-процессорами. В RISC-процессоре, таком как ARM, декодирование инструкций устроено прямолинейно, как и сама схемотехника процессора. Но в 8086, CISC-процессоре, повсюду встречаются пограничные случаи и усложнения. Например, в 8086 машинная инструкция иногда задаёт операцию АЛУ в первом байте, иногда во втором, а иногда и где-то ещё, поэтому требуются защёлка X-регистра, мультиплексор XI и ПЗУ декодирования групп (Group Decode ROM). АЛУ 8086 включает редкие операции, в том числе четыре вида BCD-коррекции и семь типов сдвигов, что делает АЛУ более сложным. Разумеется, продолжающийся успех x86 показывает, что у этой сложности есть и преимущества.

Эта статья была глубоким погружением в детали АЛУ 8086, и я надеюсь, что она оказалась для вас интересной. 

Примечания и ссылки (осторожно, много текста)
  1. Операции, реализованные в АЛУ:

Код

Мнемоника

Описание

00

ADD

Сложение

01

OR

Логическое OR

02

ADC

Сложение с входным переносом

03

SBB

Вычитание с входным заёмом

04

AND

Логическое AND

05

SUBT

Вычитание

06

XOR

Логическое XOR

07

CMP

Сравнение

08

ROL

Циклический сдвиг влево

09

ROR

Циклический сдвиг вправо

0a

LRCY

Циклический сдвиг влево через перенос

0b

RRCY

Циклический сдвиг вправо через перенос

0c

SHL

Сдвиг влево

0d

SHR

Сдвиг вправо

0e

SETMO

Установить в минус единицу (сомнительно)

0f

SAR

Арифметический сдвиг вправо

10

PASS

Передать аргумент без изменений

11

XI

Инструкция задаёт операцию АЛУ

14

DAA

Десятичная коррекция после сложения

15

DAS

Десятичная коррекция после вычитания

16

AAA

ASCII-коррекция после сложения

17

AAS

ASCII-коррекция после вычитания

18

INC

Инкремент

19

DEC

Декремент

1a

COM1

Единичное дополнение

1b

NEG

Смена знака

1c

INC2

Инкремент на 2

1d

DEC2

Декремент на 2

Также см. код Andrew Jenner.

2. Возможно, вам интересно, как этот микрокод справляется со сложными режимами адресации 8086, такими как [BX+DI]. Фокус в том, что режимы адресации реализованы подпрограммами микрокода. 

3. У АЛУ 8086 есть отдельная схема для реализации сдвига вправо. Проблема в том, что в АЛУ данные обычно «текут» справа налево, поскольку переносы идут от младших битов к старшим. Сдвиг данных вправо идёт против этого направления, поэтому требуется отдельный путь. (Сдвиг влево устроен проще: можно просто прибавить число само к себе.)

Операции коррекции (DAA, DAS, AAA, AAS) также используют полностью отдельную схемотехнику. Эти операции формируют корректирующие величины для BCD (двоично-десятичной) арифметики на основе значения и флагов. Схемы для этих операций расположены рядом со схемами флагов и отделены от остальной части схем АЛУ. 

4. Если подробнее, 8086 сохраняет биты 5–3 машинной инструкции в «X-регистре». Для операции XI биты X-регистра становятся битами 2–0 спецификатора операции АЛУ, при этом бит 3 берётся из бита 6 инструкции, а бит 4 для некоторых инструкций берётся из ПЗУ декодирования групп. Смысл в том, что система команд спроектирована так, чтобы биты инструкции соответствовали битам спецификатора операции АЛУ, но отображение оказывается сложнее, чем можно ожидать. Восьмёрка базовых ари��метико-логических операций (ADD, SUB, OR и т. д.) имеет прямолинейное соответствие, которое видно по таблице опкодов 8086, но соответствие для других инструкций не столь очевидно. Более того, иногда операция задаётся в первом байте машинной инструкции, а иногда во втором, поэтому X-регистру и нужно хранить нужные биты. 

5. Триггеры сбрасываются сигналом 8086 под названием «Second Clock». Когда начинается новая машинная инструкция, сигнал «First Clock» формируется на первом байте инструкции, а «Second Clock» — на втором байте. (Заметьте, что эти сигналы не обязательно приходятся на соседние такты, поскольку может потребоваться выборка из памяти, если очередь инструкций пуста.) Почему триггеры сбрасываются по Second Clock, а не по First Clock? У 8086 есть небольшая конвейеризация, поэтому предыдущая микроинструкция может ещё завершаться во время First Clock следующей инструкции. К моменту Second Clock уже безопасно сбрасывать состояние АЛУ.

6. Для справки: 27 выходов PLA активируются следующими микрооперациями АЛУ:

Output 0: RRCY (циклический сдвиг вправо через перенос)
Output 1: ROR (циклический сдвиг вправо)
Output 2: BCD-коррекции: DAA (десятичная коррекция после сложения), DAS (десятичная коррекция после вычитания), AAA (ASCII-коррекция после сложения) или AAS (ASCII-коррекция после вычитания)
Output 3: SAR (арифметический сдвиг вправо)
Output 4: Сдвиг влево: ROL (циклический сдвиг влево), RCL (циклический сдвиг влево через перенос), SHL (сдвиг влево) или SETMO (установить в минус единицу)
Output 5: Сдвиг вправо: ROR (циклический сдвиг вправо), RCR (циклический сдвиг вправо через перенос), SHR (сдвиг вправо) или SAR (арифметический сдвиг вправо)
Output 6: INC2 (инкремент на 2)
Output 7: ROL (циклический сдвиг влево)
Output 8: RCL (циклический сдвиг влево через перенос)
Output 9: ADC (сложение с переносом)
Output 10: DEC2 (декремент на 2)
Output 11: INC (инкремент)
Output 12: NEG (смена знака)
Output 13: операция АЛУ 12 (не используется?)
Output 14: SUB (вычитание), CMP (сравнение), DAS (десятичная коррекция после вычитания), AAS (ASCII-коррекция после вычитания)
Output 15: SBB (вычитание с заёмом)
Output 16: ROL (циклический сдвиг влево) или RCL (циклический сдвиг влево через перенос)
Output 17: ADD или ADC (сложение или сложение с переносом)
Output 18: DEC или DEC2 (декремент на 1 или 2)
Output 19: PASS (передача без изменений) или INC (инкремент)
Output 20: COM1 (единичное дополнение, побитовая инверсия) или NEG (смена знака)
Output 21: XOR
Output 22: OR
Output 23: AND
Output 24: SHL (сдвиг влево)
Output 25: DAA или AAA (десятичная/ASCII-коррекция после сложения)
Output 26: CMP (сравнение)

7. Программируемая логическая матрица (PLA, Programmable Logic Array) — это способ реализации логических элементов в виде структурированной решётки. PLA часто используют в микропроцессорах, потому что они позволяют очень компактно реализовывать логику. Обычно PLA состоит из двух слоёв: слоя «OR» и слоя «AND». Вместе эти слои формируют выходы в виде «суммы произведений» (sum-of-products), то есть нескольких термов, объединённых по OR. PLA в составе АЛУ немного необычна: многие выходы снимаются напрямую со слоя OR, а во второй слой поступает лишь около 15 выходов из первого слоя. 

8. Управляющие сигналы проходят через драйверную схему, показанную ниже. Работа этой схемы много лет ставила меня в тупик, поскольку транзистор с затвором на +5 В выглядит так, будто он постоянно открыт. Но однажды я листал книгу DRAM Circuit Design и заметил там точно такую же схему под названием «Bootstrap Wordline Driver». Назначение этой схемы — поднять выходное напряжение выше, чем может дать обычная NMOS-схема, чтобы повысить быстродействие. Проблема NMOS-схем в том, что NMOS-транзисторы плохо «тянут» сигнал вверх: из-за свойств транзистора выходное напряжение ниже напряжения на затворе на величину порогового напряжения VTH, на полвольта и более.

The drive signals to the ALU gates are generated with this dynamic circuit.
Управляющие сигналы, подаваемые на затворы транзисторов АЛУ, формируются этой динамической схемой.

Бутстрэп-схема использует ёмкость, чтобы «выжать» из схемы больше напряжения. В частности, предположим, что на входе +5 В, а тактовый сигнал высокий. Тогда в точке A будет примерно 4,5 В, то есть около полувольта потеряется на пороге. Теперь предположим, что тактовый сигнал падает в ноль, и инвертированный тактовый сигнал, управляющий верхним транзистором, становится высоким. Из-за ёмкости второго транзистора, когда его исток и сток поднимаются по напряжению, затвор второго транзистора подтягивается выше, возможно прибавляя пару вольт. Высокое напряжение на затворе обеспечивает «полноуровневый» выход, избегая просадки из-за VTH. Но зачем тогда транзистор с затвором на +5 В? Этот транзистор работает примерно как диод, не позволяя повышенному напряжению утекать назад через вход и рассеиваться.

Бутстрэп-схема применяется к управляющим сигналам LUT АЛУ по двум причинам. Во-первых, эти управляющие сигналы управляют проходными транзисторами (pass transistor). У проходного транзистора возникает падение напряжения из-за порогового напряжения, поэтому полезно иметь управляющий сигнал с как можно более высоким уровнем уже «на входе». Во-вторых, каждый управляющий сигнал подключён к 16 транзисторам (по одному на каждый бит). Это много транзисторов для управления одним сигналом, поскольку у каждого транзистора есть ёмкость затвора. Повышение напряжения помогает преодолеть R-C (резистивно-ёмкостную) задержку и улучшает быстродействие.

A close-up of the bootstrap drive circuits, in the left half of the LUT PLA.
Крупный план бутстрэп-драйверов в левой половине LUT PLA.

На схеме выше показаны шесть бутстрэп-драйверов на кристалле. Слева расположены транзисторы, которые притягивают сигналы к земле, когда тактовый сигнал высокий. Транзисторы на +5 В разбросаны по изображению; два из них подписаны. Шесть крупных транзисторов формируют выходной сигнал и управляются инвертированным тактовым сигналом (clock′). Обратите внимание, что эти транзисторы заметно больше остальных, потому что им нужно выдавать выход с большим током, тогда как остальные транзисторы играют скорее вспомогательную роль.

(Бутстрэп-схемы появились очень давно: Federico Faggin разработал бутстрэп-схему для Intel 8008 и утверждал, что она «оказалась критически важной для реализации микропроцессора») 

Если разбор 8086 напомнил, что «внутри» системы всегда интереснее, чем в спецификации, рекомендуем обратить внимание на курс «Обратная разработка» (Reverse engineering). На нем вы разберёте, как код выглядит после компиляции: распаковка, PE-формат, импорт и практический анализ вредоносов – чтобы увереннее искать уязвимости и чинить их. Готовы к обучению? Пройдите вступительный тест.

Для знакомства с форматом обучения и экспертами приходите на бесплатные уроки:

  • 9 февраля, 20:00. «Lock-free в C++: Без блокировок к высокой производительности». Записаться

  • 12 февраля, 20:00. «Безопасная многопоточность: пишем пул потоков». Записаться

  • 19 февраля, 20:00. «С++ под капотом – что стоит за кодом, который мы пишем». Записаться

Полный список бесплатных уроков от преподавателей курсов можно посмотреть в календаре мероприятий.