Как работает движок микрокода процессора 8086

от автора

Микропроцессор 8086 — знаковое устройство, представленное компанией Intel в 1978 году. Именно он положил начало архитектуре x86, которая до сих пор доминирует в настольных и серверных системах. Внутри чипа 8086 для реализации набора инструкций используется микрокод. Я занимался реверс-инжинирингом 8086 по фотографиям кристалла, и в этой статье разбирается, как устроен движок микрокода этого процессора. Здесь я не буду рассматривать содержимое микрокода или то, как именно он управляет остальными частями процессора. 

Вместо этого речь пойдёт о том, как 8086 определяет, какой микрокод запускать, как он последовательно исполняется, как обрабатываются переходы и вызовы подпрограмм внутри микрокода, а также как микрокод физически хранится. С учётом технологических ограничений 1978 года размещение микрокода на кристалле было нетривиальной задачей, поэтому в Intel применили множество приёмов оптимизации, чтобы уменьшить его объём.

Вкратце, микрокод в 8086 состоит из 512 микроинструкций, каждая шириной 21 бит. Движок микрокода использует 13-битный регистр для последовательного перебора микроинструкций, а также отдельный 13-битный регистр подпрограмм для хранения адреса возврата при вызовах микрокодовых подпрограмм. Дополнительно используются два вспомогательных ПЗУ: «Group Decode ROM» — для классификации машинных инструкций, и «Translation ROM» — для переходов к подпрограммам микрокода, например при вычислении адресов. 

Физически микрокод хранится в виде массива размером 128×84. Для оптимизации хранения применяется специальный декодер адреса. Схемы микрокода можно увидеть на фотографии кристалла ниже.

The 8086 die under a microscope, with main functional blocks labeled. This photo shows the chip's single metal layer; the polysilicon and silicon are underneath. Click on this image (or any other) for a larger version.

Кристалл 8086 под микроскопом с обозначенными основными функциональными блоками. На этом снимке виден единственный металлический слой чипа; под ним находятся поликремний и кремний. 

Что такое микрокод?

Машинные инструкции обычно рассматриваются как базовые шаги, которые выполняет компьютер. Однако каждая инструкция, как правило, требует выполнения нескольких операций внутри процессора. Например, инструкция ADD может включать вычисление адреса в памяти, получение значения, передачу этого значения в арифметико-логическое устройство (АЛУ, англ. ALU — Arithmetic Logic Unit), выполнение сложения и сохранение результата в регистр. Одной из самых сложных задач при проектировании компьютера является создание управляющей логики, которая на каждом шаге инструкции подаёт сигналы соответствующим частям процессора. 

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

В 1951 году Maurice Wilkes предложил идею микрокода: вместо построения управляющей логики из сложных логических схем её можно заменить ещё одним уровнем кода (то есть микрокодом), хранящимся в специальной памяти, называемой управляющим хранилищем. При выполнении машинной инструкции компьютер фактически исполняет несколько более простых микроинструкций, заданных микрокодом. 

Иными словами, микрокод образует дополнительный слой между машинными инструкциями и аппаратной частью. Основное преимущество микрокода в том, что он превращает задачу проектирования управляющей логики в задачу программирования, а не сложной схемотехники. Кроме того, микрокод позволяет реализовывать сложные инструкции и большой набор команд без существенного усложнения процессора (за исключением увеличения объёма самого микрокода). Наконец, исправлять ошибки в микрокоде обычно проще, чем в логических схемах.

Ранние компьютеры не использовали микрокод — в первую очередь из-за отсутствия подходящих технологий хранения. Ситуация изменилась в 1960-х годах: например, IBM активно применяла микрокод в системе IBM System/360 (1964). Однако первые микропроцессоры вновь вернулись к жёстко заданной управляющей логике на логических элементах. (примеч.3) Такая логика обычно занимала меньше места и работала быстрее, поскольку могла быть оптимизирована на уровне схем. 

Учитывая жёсткие ограничения по площади кристалла и сравнительно простой набор инструкций ранних микропроцессоров, этот компромисс был оправдан. Но по мере усложнения наборов инструкций и удешевления транзисторов микрокод стал более привлекательным решением. Это привело к его использованию, например, в Intel 8086 (1978), Intel 8088 (1979) и Motorola 68000 (1979). (примеч.2)

Микрокод 8086

Микрокод в 8086 значительно проще, чем в большинстве процессоров, но всё же остаётся достаточно сложным. Ниже приведён пример микрокодовой процедуры 8086 под названием «CORD» — это часть алгоритма целочисленного деления, состоящая из 16 микроинструкций. 

Я не буду подробно разбирать, как именно работает этот микрокод, а лишь покажу общий принцип. В каждой строке слева (синим) указан адрес, а справа (жёлтым) — микроинструкция, задающая низкоуровневые действия за один такт (то есть за один тактовый цикл). Каждая микроинструкция выполняет операцию пересылки данных — переносит значение из регистра-источника (S) в регистр-приёмник (D). (Источник Σ обозначает выход АЛУ). 

Для обеспечения параллелизма микроинструкция может одновременно с пересылкой выполнять одну или две дополнительные операции. Эти операции задаются полями «a» и «b», причём их интерпретация зависит от поля типа. Например, тип 1 соответствует операциям АЛУ, таким как вычитание (SUBT) или циклический сдвиг влево через флаг переноса (LRCY). 

Тип 4 задаёт две общие операции, например «RTN» — возврат из микрокодовой подпрограммы. Тип 0 означает операцию перехода: «UNC 10» — безусловный переход на строку 10, а «CY 13» — переход на строку 13, если установлен флаг переноса. Наконец, поле «F» указывает, нужно ли обновлять флаги условий. 

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

An example of a microcode routine. The CORD routine implements integer division with subtracts and left rotates. This is from patent 4,449,184.

Пример микрокодовой процедуры. Процедура CORD реализует целочисленное деление с использованием вычитаний и циклических сдвигов влево. Источник — патент 4,449,184.

Каждая инструкция хранится по 13-битному адресу (синим), который состоит из 9 явно заданных бит и 4-битного счётчика последовательности «CR». Восемь пронумерованных бит адреса обычно соответствуют коду операции машинной инструкции. Бит «X» — дополнительный, он расширяет адресное пространство для кода, не привязанного напрямую к машинным инструкциям, например для обработки сброса, прерываний, вычисления адресов и алгоритмов умножения/деления.

Микроинструкция кодируется в 21 бит, как показано ниже. Каждая микроинструкция обязательно содержит операцию пересылки данных между регистрами, причём источник и приёмник задаются по 5 бит. Назначение остальных бит неочевидно, поскольку зависит от поля типа (длиной 2 или 3 бита). «Короткий переход» (тип 0) — это условный переход внутри текущего блока из 16 микроинструкций. Операция АЛУ (тип 1) настраивает арифметико-логическое устройство для выполнения конкретной операции. Служебные операции (тип 4) включают действия от очистки очереди предварительной выборки (prefetch queue) до завершения текущей инструкции. Чтение или запись в память — это тип 6. «Длинный переход» (тип 5) — условный переход к одной из 16 фиксированных точек микрокода (заданных во внешней таблице). Наконец, «длинный вызов» (тип 7) — это условный вызов подпрограммы по одному из 16 адресов (отличных от адресов переходов).

The encoding of a micro-instruction into 21 bits. Based on NEC v. Intel: Will Hardware Be Drawn into the Black Hole of Copyright?

Кодирование микроинструкции в 21 бит. По материалам дела NEC против Intel: «Будет ли аппаратное обеспечение втянуто в чёрную дыру авторского права?»

Такой «вертикальный» формат микрокода уменьшает объём памяти, необходимый для его хранения, за счёт кодирования управляющих сигналов в отдельных полях. Однако это требует дополнительной логики декодирования, которая обрабатывает эти поля и формирует низкоуровневые управляющие сигналы. Примечательно, что отдельной схемы «декодера микрокода» не существует. 

Вместо этого логика распределена по всему кристаллу: различные участки схемы «отслеживают» определённые битовые шаблоны микрокода и формируют управляющие сигналы там, где это необходимо.

Как инструкции отображаются в ПЗУ

Интересный вопрос — как именно микроинструкции организованы в ПЗУ и каким образом выбираются нужные микроинструкции для конкретной машинной команды. В 8086 используется продуманное отображение машинной инструкции в адрес микрокода, которое позволяет разным инструкциям использовать общий микрокод.

В различных процессорах применяются разные подходы к организации микрокода. Один из вариантов — включать в каждую микроинструкцию поле с адресом следующей микроинструкции. Это даёт полную гибкость в размещении микрокода, но требует дополнительных бит для хранения адреса, увеличивая размер каждой микроинструкции. Более распространённый подход — последовательное выполнение микроинструкций с помощью счётчика микропрограммы, который проходит по адресам, если не встречается явный переход. Такой подход позволяет обойтись без поля адреса в каждой инструкции, но требует наличия счётчика с инкрементором, усложняя аппаратную часть.

В 8086 используется гибридный подход. 4-битный счётчик проходит по младшим 4 битам адреса, позволяя выполнять до 16 микроинструкций подряд без перехода. Преимущество в том, что достаточно небольшого 4-битного инкрементора, а не полного 13-битного. Кроме того, движок микрокода поддерживает операцию «короткого перехода», которая позволяет легко переходить внутри блока из 16 инструкций, используя 4-битный адрес перехода вместо полного 13-битного.

Ещё одно важное архитектурное решение — как определить начальный адрес микрокода для каждой машинной инструкции. Иными словами, если нужно выполнить ADD, с какого адреса начинается соответствующий микрокод? Один из вариантов — таблица начальных адресов: система ищет адрес старта для ADD в таблице. Однако это требует хранения большой таблицы из 256 записей. Второй вариант — использовать значение кода операции как начальный адрес. 

Например, инструкция ADD с кодом 0x05 начиналась бы с микроадреса 5. Но у такого подхода есть два недостатка. Во-первых, невозможно последовательное выполнение микрокода, так как соседние микроинструкции будут относиться к разным машинным инструкциям. Во-вторых, становится невозможным совместное использование микрокода, поскольку каждая инструкция привязана к своему уникальному адресу в ПЗУ микрокода.

8086 решает эти проблемы двумя способами. Во-первых, машинные инструкции в микрокоде размещаются с шагом в шестнадцать позиций. Иными словами, код операции умножается на 16 (к нему добавляются четыре нулевых бита), формируя начальный адрес в ПЗУ микрокода, благодаря чему для реализации каждой инструкции остаётся достаточно пространства. Во-вторых, адресация ПЗУ реализована с частичным декодированием, а не полным, поэтому нескольким микроадресам может соответствовать одна и та же физическая область хранения.(примеч.4)

Чтобы сделать это более наглядным, рассмотрим арифметико-логические инструкции 8086: сложение байта из регистра с памятью, сложение байта из памяти с регистром, вычитание слова из памяти из регистра, XOR слова из регистра с памятью и так далее. Всего существует 8 операций АЛУ, каждая из которых может работать либо с байтом, либо со словом, а также использовать память как источник или приёмник. В итоге получается 32 различных машинных кода операций. Эти коды были подобраны таким образом, что все они имеют формат 00xxx0xx. 

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

Физическая организация ПЗУ микрокода

ПЗУ микрокода хранит 512 слов по 21 биту, поэтому на первый взгляд логичной была бы организация в виде 512 столбцов и 21 строки. Однако такая форма непрактична для реализации — структура получилась бы слишком вытянутой. Вместо этого ПЗУ строится так, что в каждом столбце размещаются сразу четыре слова, в результате чего получается матрица 128×84, гораздо более близкая к квадратной. Это не только упрощает физическую компоновку, но и уменьшает число декодеров столбцов с 512 до 128, снижая сложность схемы. 

Хотя теперь требуется 21 мультиплексор для выбора одной из четырёх строк для каждого выходного бита, итоговая схема всё равно оказывается значительно компактнее. При этом возникает компромисс: снижается гибкость объединения адресов за счёт игнорирования отдельных битов. Теперь каждый декодер выбирает столбец из четырёх слов, а не одно слово, поэтому каждая четвёрка должна содержать последовательные адреса.

The main components of the microcode engine. The metal layer has been removed to show the silicon and polysilicon underneath. If you zoom in, the bit pattern is visible in the silicon doping pattern.

Основные компоненты движка микрокода. Металлический слой удалён, чтобы показать кремний и поликремний под ним. При увеличении можно различить битовые шаблоны в распределении легирования кремния.

На изображении выше показано, как микрокод хранится и извлекается. В верхней части находится 13-битный регистр адреса микрокода, который будет подробно рассмотрен ниже. Схема выбора столбца декодирует 11 из 13 бит адреса, чтобы выбрать один столбец массива хранения. Слева мультиплексоры выбирают по одному биту из каждой группы из четырёх строк, используя оставшиеся два бита адреса (а именно два младших бита счётчика последовательности). Выбранные 21 бит микрокода фиксируются в защёлках и передаются в остальные части процессора, где они декодируются, как описано ранее, и управляют действиями процессора.

Оптимизация микрокода

В 1978 году объём памяти, доступной для хранения микрокода в ПЗУ, был сильно ограничен. В частности, в 8086 помещается всего 512 микроинструкций. При этом процессор имеет около 256 машинных инструкций в однобайтовом коде операции, каждая из которых может использовать различные режимы адресации и требует выполнения нескольких микроинструкций. Чтобы уместить всё это в доступный объём, потребовались сжатие и оптимизация.(примеч.5) Основная идея заключалась в том, чтобы по возможности выносить функциональность из микрокода в отдельную аппаратную логику. Ниже рассмотрены некоторые из этих приёмов.

В 8086 есть АЛУ, выполняющее операции сложения, вычитания, а также логические операции вроде AND и XOR. Рассмотрим инструкцию ADD: она реализуется с помощью нескольких микроопераций — вычисление адреса в памяти, загрузка данных, выполнение сложения и запись результата. Инструкции вычитания, AND или XOR требуют тех же самых шагов, за исключением самой операции в АЛУ. Всего в 8086 есть восемь операций, основанных на АЛУ, которые различаются только типом выполняемой операции. (примеч.6) В 8086 используется приём, при котором все эти восемь инструкций используют общий микрокод. Конкретно, микрокод задаёт специальную операцию XI, которая означает, что АЛУ должна проанализировать соответствующие биты инструкции и выполнить нужную операцию.(примеч.7) Это сокращает объём микрокода для таких инструкций в восемь раз, ценой добавления дополнительной логики в АЛУ. В частности, схема управления АЛУ содержит регистр для хранения нужных битов инструкции и программируемую логическую матрицу (PLA — Programmable Logic Array), которая преобразует эти биты в низкоуровневые управляющие сигналы.

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

Ещё одна оптимизация связана с тем, что многие инструкции в 8086 существуют в двух вариантах: 8-битном и 16-битном. Можно было бы реализовать для них отдельный микрокод — один для байта и один для слова. Однако вместо этого используется общий микрокод. Сложность переносится в схему передачи данных по шине: она анализирует младший бит инструкции, чтобы определить, нужно ли обрабатывать байт или слово. Это позволяет сократить объём микрокода вдвое для множества инструкций.

Наконец, простые инструкции, выполняемые за один такт, реализованы напрямую через логические элементы, без использования микрокода. Например, инструкция CLC (сброс флага переноса) изменяет флаг напрямую. Аналогично, префиксы выбора сегмента, блокировки инструкции или повторения обрабатываются аппаратной логикой. Эти инструкции вообще не используют микрокод, что будет важно далее.

Благодаря таким приёмам в микрокоде реализовано около 75 типов инструкций (вместо примерно 256), что существенно уменьшает его объём. Цена за это — увеличение сложности аппаратной логики, однако разработчики сочли такой компромисс оправданным.

Байт ModR/M

Однако в микрокоде 8086 есть ещё одно усложнение. Большинство инструкций 8086 содержит второй байт — байт ModR/M, который задаёт режим адресации инструкции достаточно сложным образом (см. ниже). Этот байт придаёт инструкциям 8086 большую гибкость: можно использовать два регистра, регистр и ячейку памяти или регистр и «непосредственное значение» (immediate value), заданное в самой инструкции. Адрес ячейки памяти может определяться через 8 комбинаций индексных регистров с возможным добавлением смещения размером в один или два байта. (Это, например, удобно для доступа к элементам массива или структуры.) Несмотря на всю мощь таких режимов адресации, они создают проблемы для микрокода.

A summary of the ModR/M byte, from MCS-86 Assembly Language Reference Guide.

Краткое описание байта ModR/M из руководства MCS-86 Assembly Language Reference Guide.

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

Решение — использовать подпрограммы в микрокоде для вычисления адреса в памяти. Благодаря этому разные инструкции могут использовать общий микрокод для одного и того же режима адресации. Однако это заметно усложняет движок микрокода, поскольку ему необходимо сохранять микроадрес при вызове подпрограммы, чтобы затем корректно вернуться. Для этого в движке микрокода предусмотрен отдельный регистр для хранения адреса возврата. (Полноценного стека нет, поэтому вложенные вызовы подпрограмм невозможны, но это не является серьёзным ограничением.)

В итоге в микрокоде используется около 10 подпрограмм для различных режимов адресации, а также четыре процедуры для разных размеров смещения. (Восемь вариантов регистров-источников обрабатываются логикой выбора регистров, а не микрокодом.) Таким образом, микрокод способен обработать 256 различных режимов адресации с помощью примерно 14 коротких процедур, которые складывают соответствующие адресные регистры и смещение для получения эффективного адреса.

Ещё одно усложнение заключается в том, что машинные инструкции могут менять местами источник и приёмник, заданные байтом ModR/M, в зависимости от кода операции. Например, одна инструкция вычитания вычитает значение из памяти из регистра, а другая — значение регистра из памяти. Эти варианты различаются по второму биту инструкции — так называемому «биту направления». Такие случаи обрабатываются аппаратной управляющей логикой, поэтому микрокод может их игнорировать. В частности, перед передачей информации о источнике и приёмнике в схему управления регистрами специальная перекрёстная схема при необходимости меняет их местами в зависимости от значения бита направления.

ПЗУ трансляции (Translation ROM)

Как было сказано выше, начальный адрес для машинной инструкции напрямую определяется её кодом операции. Однако движку микрокода нужен механизм, который будет задавать адреса для операций перехода и вызова подпрограмм. В 8086 эти адреса жёстко заданы в ПЗУ трансляции (Translation ROM), которое формирует 13-битный адрес.(примеч.8) В нём хранится десять адресов назначения для операций перехода и ещё десять (других) адресов для вызовов подпрограмм.

Вторая функция ПЗУ трансляции — хранение адресов для каждого режима адресации ModR/M, указывающих на код вычисления эффективного адреса. Дополнительная сложность в том, что две записи в таблице переходов реализованы с использованием условной логики — в зависимости от того, включает ли вычисление адреса смещение. За счёт того, что это условие «зашито» прямо в ПЗУ трансляции, микрокоду не требуется выполнять отдельную проверку.

На изображении ниже показано, как выглядит ПЗУ трансляции на кристалле. Оно реализовано как частично декодированное ПЗУ с мультиплексированными входами.(примеч.9) Входы находятся в нижней левой части. Для операций перехода или вызова ПЗУ использует 4 входных бита из выхода микрокода, поскольку именно микрокод выбирает цель перехода. Для вычисления адреса используются 5 бит из байта ModR/M инструкции, и соответствующая процедура выбирается самой инструкцией. В ПЗУ также есть дополнительные входные биты для выбора режима (переход, вызов или вычисление адреса), а также для условных переходов. Логика декодирования (левая часть) активирует одну из строк в правой части, формируя выходной адрес. Этот адрес выводится снизу и загружается в регистр микроадреса, расположенный под ПЗУ трансляции.

The Translation ROM holds addresses of routines in the microcode.

ПЗУ трансляции хранит адреса процедур в микрокоде.

ПЗУ группового декодирования

Ранее обсуждалось, как оптимизируются различные категории инструкций. Например, у многих инструкций есть бит, определяющий, работает ли операция с байтом или со словом. У многих также есть бит, меняющий направление обмена между памятью и регистрами. Эти особенности реализованы аппаратной логикой, а не микрокодом. Некоторые инструкции вообще выполняются без участия микрокода. Как же 8086 определяет, каким образом обрабатывать ту или иную инструкцию?

ПЗУ группового декодирования (Group Decode ROM) принимает код операции инструкции и формирует 15 сигналов, указывающих на различные категории инструкций, которые обрабатываются по-разному. (примеч.10) Выходы этого ПЗУ используются различными логическими схемами для выбора способа обработки инструкции. В одних случаях это влияет на микрокод — например, инициируется вызов подпрограммы вычисления адреса, если инструкция содержит байт ModR/M. В других случаях сигналы используются «после» микрокода, например для определения, должна ли операция работать с байтом или со словом. Некоторые сигналы полностью обходят микрокод, приводя к выполнению инструкции исключительно средствами аппаратной логики.

A closeup of the Group Decode ROM. The circuit uses two layers of NOR gates to generate the output signals from the opcode inputs. This image shows a composite of the metal, polysilicon, and silicon layers.

Увеличенный фрагмент ПЗУ группового декодирования. Схема использует два уровня элементов NOR для формирования выходных сигналов на основе входных битов кода операции. На изображении показана совмещённая визуализация металлического слоя, поликремния и кремния.

Инструкции со специальным кодированием

Для большинства инструкций 8086 первый байт однозначно определяет инструкцию. Однако в 8086 есть ряд инструкций, у которых байт ModR/M полностью меняет смысл первого байта. Например, код операции 0xF6 (группа 1 ниже) может означать TEST, NOT, NEG, MUL, IMUL, DIV или IDIV — в зависимости от значения байта ModR/M. Аналогично, код 0xFE (группа 2) может означать INC, DEC, CALL, JMP или PUSH.(примеч.11)

The 8086 instruction map for opcodes 0xF0 to 0xFF. Based on MCS-86 Assembly Language Reference Guide.

Карта инструкций 8086 для кодов операций от 0xF0 до 0xFF. По материалам MCS-86 Assembly Language Reference Guide.

На первый взгляд такое кодирование может показаться произвольным, но за ним стоит логика. Большинство инструкций работают с источником и приёмником. Однако некоторые, например INC (инкремент), используют один и тот же регистр или ячейку памяти и как источник, и как приёмник. Другие, такие как CALL или JMP, используют только один адрес. В результате поле «reg» в байте ModR/M оказывается избыточным. Чтобы не «терять» эти биты, их используют для кодирования разных инструкций. (Поскольку доступно всего 256 однобайтовых кодов операций, важно использовать их максимально эффективно.)

Реализация этих инструкций в микрокоде устроена довольно интересно. Поскольку у них одинаковый первый байт, стандартное отображение микрокода привело бы к одному и тому же адресу. Однако для этих инструкций применяется специальная обработка: поле «reg» из байта ModR/M копируется в младшие биты адреса микрокода. По сути, такие инструкции рассматриваются как коды операций в диапазоне 0xF0–0xFF, благодаря чему разные варианты выполняются по разным микроадресам. Можно было бы ожидать конфликт с реальными инструкциями в диапазоне 0xF0–0xFF, но коды операций в 8086 подобраны так, что ни одна из этих инструкций не использует микрокод. Как видно выше, это префиксы (LOCK, REP, REPZ), останов (HLT) и операции с флагами (CMC, CLC, STC, CLI, STI, CLD, STD), все они реализованы аппаратной логикой. Таким образом, диапазон 0xF0–0xFF фактически освобождён для «расширенных» инструкций.

Аппаратная реализация этого механизма не слишком сложна. ПЗУ группового декодирования формирует сигнал для таких специальных инструкций. Это заставляет регистр адреса микрокода загружать соответствующие биты из байта ModR/M, в результате чего запускается нужная микрокодовая процедура.

Регистр адреса микрокода

Центральным элементом движка микрокода является регистр адреса микрокода, который определяет, какой микроадрес будет выполнен. Как уже упоминалось, адрес микрокода имеет длину 13 бит: 8 бит обычно соответствуют коду операции инструкции, один бит — дополнительный бит «X», а ещё 4 бита последовательно инкрементируются. На схеме ниже показано, как организована логика этих битов. Девять бит, относящихся к инструкции, имеют почти одинаковую схему. Биты последовательности устроены сложнее и отличаются друг от друга, поскольку схема инкремента для каждого бита реализована по-разному.

Layout of the microcode address register. Each bit has a roughly vertical block of circuitry.

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

На схеме ниже показана реализация одного бита регистра адреса микрокода. В ней используются два триггера: один хранит текущий бит адреса, а второй — предыдущий адрес при выполнении вызова подпрограммы. Мультиплексор (mux) выбирает вход для каждого триггера. Например, если микрокод ожидает завершения обращения к памяти, вход «hold» заставляет текущий адрес замкнуться на себя и повторно загрузиться в триггер. При вызове подпрограммы вход «call» сохраняет текущий адрес во вспомогательном триггере. Соответственно, при возврате из подпрограммы вход «return» загружает сохранённый адрес обратно. Кроме того, триггер адреса может получать значение адреса напрямую из инструкции, из ПЗУ трансляции или из обработчика прерывания. Схема передаёт бит адреса (и его инверсию) в декодер адреса ПЗУ микрокода.

Schematic of a typical bit in the microcode address register.

Схема типичного бита регистра адреса микрокода.

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

Управляющие сигналы для мультиплексоров формируются из различных источников. Специальная схема, называемая загрузчиком (loader), запускает обработку инструкции, синхронизируясь с очередью предвыборки и выборкой инструкций из памяти. Операции вызова и возврата реализуются через микрокод. ПЗУ группового декодирования управляет частью входов, например при обработке байта ModR/M. В итоге используется умеренно сложная условная логика, которая определяет текущий микроадрес и, соответственно, выполняемый микрокод.

Выводы

Материал получился объёмным, поэтому спасибо, что дочитали до конца. Изучение движка микрокода 8086 позволяет сделать три основных вывода. Во-первых, реальная реализация микрокода значительно сложнее той «чистой» модели, которая обычно описывается в учебниках. Значительная часть функциональности реализована вне микрокода, на уровне аппаратной логики, поэтому это не «чистая» микрокодовая архитектура. Кроме того, присутствует множество оптимизаций и частных случаев. В системе используются два вспомогательных ПЗУ — ПЗУ трансляции и ПЗУ группового декодирования. Даже регистр адреса микрокода устроен нетривиально.

Во-вторых, необходимость всех этих оптимизаций показывает, что 8086 находился на грани технологических возможностей своего времени. Разработчикам пришлось приложить значительные усилия, чтобы уместить микрокод в доступный объём.

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

Примечания и ссылки (осторожно, много текста)

  1. Микрокод 8086 был дизассемблирован (ссылка) несколько лет назад Эндрю Дженнером путём извлечения битов из моих фотографий кристалла. Моя статья отличается тем, что рассматривает аппаратную часть, исполняющую микрокод, а не его содержимое. 

2. Согласно данным из Wikipedia, процессор Zilog Z8000 (1979) не использовал микрокод, что несколько неожиданно для того времени. Такое решение позволило сократить количество транзисторов, но привело к сложности исправления ошибок в логике декодирования инструкций. 

3. В качестве примера процессора без микрокода можно привести MOS 6502 (1975), который использовал программируемую логическую матрицу (PLA — Programmable Logic Array) для выполнения значительной части декодирования. PLA представляет собой структурированный способ реализации логических схем в достаточно плотной форме.

В некотором смысле PLA похожа на ПЗУ — и PLA, и ПЗУ могут реализовывать друг друга — поэтому различие между ними не всегда очевидно. Обычно считается, что в ПЗУ в каждый момент времени активна только одна строка (по текущему адресу), тогда как PLA более универсальна: в ней могут одновременно активироваться несколько строк, объединяясь для формирования выходных сигналов.

Процессор Zilog Z80 использовал несколько иной подход. В нём применялась небольшая PLA для базового декодирования инструкций по типам, а затем управляющие сигналы формировались с помощью большого объёма так называемой «произвольной логики» (random logic — термин связан с внешним видом схемы, а не с её случайностью). Эта логика объединяла типы инструкций и временные сигналы, чтобы сформировать нужные управляющие воздействия. 

4. В «обычном» ПЗУ адресные биты декодируются логическими элементами так, что каждому уникальному адресу соответствует свой столбец памяти. Однако часть схемы декодирования может «игнорировать» отдельные биты, из-за чего несколько адресов будут указывать на одну и ту же область памяти. Например, если декодируются 5-битные адреса, можно не учитывать последний бит — тогда адреса 11010 и 11011 будут обращаться к одним и тем же данным.

Или можно игнорировать три младших бита, и тогда все 8 адресов вида 11xxx будут указывать на одну и ту же ячейку (где x — любой бит). В таком случае ПЗУ становится похожим на программируемую логическую матрицу, хотя по-прежнему выбирается только одна строка за раз. 

5. Сопроцессор Intel 8087 выполнял операции с плавающей запятой для 8086. Для работы ему требовался большой объём микрокода — больше, чем могло поместиться в стандартное ПЗУ на кристалле. Чтобы решить эту проблему, в Intel разработали специальное ПЗУ, в котором один транзистор хранил не один, а два бита. Это достигалось за счёт использования транзисторов четырёх разных размеров, формирующих четыре различных уровня напряжения.

Аналоговая схема преобразовывала каждый уровень напряжения в два бита. Такая сложная техника (по крайней мере теоретически) удваивала плотность хранения и позволяла разместить микрокод на чипе. Я писал об этом небинарном ПЗУ 8087 отдельно

6. К объединённым операциям АЛУ относятся: сложение, сложение с переносом, вычитание, вычитание с заёмом, логическое AND, логическое XOR, логическое OR и сравнение. Операция сравнения может показаться лишней в этом списке, но фактически она реализуется как вычитание, при котором обновляются флаги условий без изменения значения.

Такие операции, как инкремент, декремент, отрицание и логическое NOT, могли бы показаться кандидатами на включение в этот список, но они работают с одним операндом, а не с двумя, поэтому реализуются иначе на уровне микрокода. При этом инкремент и декремент всё же объединены в микрокоде.

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

7. Само АЛУ не анализирует биты инструкции, чтобы определить, какую операцию выполнять. Рядом с ним находится управляющая схема, которая с помощью программируемой логической матрицы анализирует биты инструкции и управляющую команду АЛУ из микрокода, формируя низкоуровневые сигналы управления. Эти сигналы определяют, например, распространение переноса, инверсию операндов и выбор логической операции, заставляя АЛУ выполнять нужное действие.

8. ПЗУ трансляции имеет ещё один выход — сигнал, указывающий на режим адресации, использующий регистр BP. Этот сигнал поступает в схему выбора сегментного регистра и переключает используемый сегмент. Причина в том, что 8086 по умолчанию использует сегмент данных для вычисления эффективного адреса, если только в режиме адресации не задействован регистр BP в качестве базового. В этом случае используется сегмент стека. Это пример того, что архитектура 8086 не является ортогональной и содержит множество частных случаев. 

9. ПЗУ трансляции также можно рассматривать как программируемую логическую матрицу, построенную на двух уровнях элементов NOR. Наличие условных записей делает её ещё более похожей на PLA, чем на классическое ПЗУ. Формально это всё же ПЗУ, поскольку в каждый момент времени активируется только одна строка. Я использую термин «ПЗУ трансляции», поскольку именно так оно называется в патентах Intel. 

10. Хотя ПЗУ группового декодирования в патенте называется ПЗУ, его скорее можно рассматривать как PLA. Концептуально оно содержит 256 «слов» — по одному на каждую инструкцию, но реализовано как набор логических функций. 

11. В документации 8086 эти инструкции назывались «Group 1» и «Group 2». В более поздней документации Intel они были переименованы в «Unary Group 3», «INC/DEC Group 4» и «Indirect Group 5». Подробности можно найти на stackoverflow.

В 8086 также есть ещё две группы инструкций, где поле reg определяет конкретную операцию: это инструкции с непосредственным операндом (0x80–0x83) и инструкции сдвига (0xD0–0xD3). Для этих кодов операции выбор конкретной инструкции выполняется на уровне АЛУ. С точки зрения микрокода это «обычные» инструкции, поэтому здесь они не рассматриваются.

Стоит отметить, что хотя коды операций 8086 традиционно записываются в шестнадцатеричной системе, их структура становится гораздо более понятной при рассмотрении в восьмеричной системе счисления. Подробнее на github. Этот подход применим и к другим связанным процессорам, таким как 8008, 8080 и Z80. 

Другие выпуски от Ken Shirriff

Если разбираться в таких вещах, как микрокод и внутренняя логика процессора, почти неизбежно приходишь к более широкому интересу — как вообще устроены системы «под капотом»: от ядра и памяти до языков и рантаймов. Если хочется копнуть в эту сторону глубже и на практических примерах, можно посмотреть ближайшие открытые уроки:

  • 21 апреля 20:00. «Связанные списки в ядре Linux: от API до реального кода». Записаться

  • 23 апреля 20:00. «Многопоточность в C++: как писать быстрые и безопасные приложения». Записаться

  • 4 мая 20:00. «Интерфейсы в Golang изнутри». Записаться

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

ссылка на оригинал статьи https://habr.com/ru/articles/1024544/