Интерфейс и обмен регистрами в микрокоде сопроцессора Intel 8087

Прокомментировать Просмотры: 8

Intel 8087 выпускался в 40-выводном корпусе DIP (dual in-line package, корпус с двумя рядами выводов).

В 1980 году корпорация Intel презентовала 8087 — новаторский сопроцессор, который увеличивал скорость вычислений с плавающей запятой почти в сто раз. Этот чип заложил фундамент для всей современной архитектуры процессоров: подавляющее большинство актуальных CPU по сей день задействуют тот самый стандарт арифметики, впервые воплощенный в модели 8087.

Для безупречно точного расчета тригонометрических функций, экспонент и квадратных корней 8087 опирается на изощренные вычислительные алгоритмы. На кремниевом уровне они запрограммированы в виде микрокода. Будучи участником исследовательского объединения Opcode Collective, я занимаюсь обратной разработкой (реверс-инжинирингом) этого внутреннего кода. В настоящей публикации я детально препарирую микрокод конкретной инструкции 8087 — FXCH — и продемонстрирую ее изнанку. Команда Die of the Intel 8087 floating-point unit chip, with main functional blocks labeled. The die is 5mm×6mm.  Click for a larger image.

Кристалл сопроцессора Intel 8087 с обозначенными основными функциональными блоками. Размер кристалла – 5×6 мм. Нажмите на изображение, чтобы открыть его в большем размере.

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

Микрокод 8087

Исполнение сложной математической команды вроде арктангенса требует сотен внутрипроцессорных итераций. Они запрограммированы на уровне микрокода, где каждая элементарная инструкция задает конкретный шаг алгоритма. (Здесь критически важно разделять макроинструкции ассемблера, видимые разработчику, и скрытые низкоуровневые микрокоманды внутри кремния). В ПЗУ микрокода прошито 1648 инструкций, обеспечивающих богатый набор функций 8087. Каждая микрокоманда имеет 16-битную длину и выполняет примитивные действия: пересылает байты между внутренними буферами, производит сложение или сдвиг информации. Совместно с Opcode Collective мы проводим реверс-инжиниринг этих структур для глубокого понимания логики чипа (ссылка).

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

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

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

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

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

Structure of an 8087 micro-instruction.
Структура микроинструкции 8087.

Как значения хранятся внутри 8087

Сопроцессор поддерживает массу форматов: различные типы чисел с плавающей точкой, целые величины и упакованные десятичные числа. Тем не менее на аппаратном уровне абсолютно все данные приводятся к единому 80-битному формату с плавающей запятой. Подобное число состоит из знакового бита, 15-битного порядка и 64-битной мантиссы. Внутри кремния функционируют два независимых тракта: один для мантиссы, второй — для знака и порядка.

Для манипуляций с данными в процессе вычислений выделено восемь регистров, образующих верхний ряд на схеме. Их организация не совсем привычна — это стек, куда элементы заносятся последовательно и откуда извлекаются. Программа адресуется не к абстрактному «регистру №3», а к элементу ST(3), находящемуся на три позиции ниже вершины стека. При добавлении или извлечении данных индексы ячеек динамически смещаются. Изначально разработчики полагали, что такая стековая модель оптимизирует систему команд, облегчит написание компиляторов и повысит скорость вызовов, хотя на практике эти ожидания оправдались лишь отчасти.

The register set of the 8087, as seen by the programmer. From 8086 Family Numerics Supplement.
Набор регистров 8087 с точки зрения программиста. Источник: 8086 Family Numerics Supplement.

Большинство операций 8087 завязаны на вершину стека. Например, команда извлечения корня модифицирует именно элемент ST(0). А как быть, если требуется обработать число, застрявшее где-то в глубине стека? Именно для этого и предусмотрена инструкция FXCH, которой посвящена наша статья. Она попросту меняет местами верхушку стека с произвольным указанным регистром, открывая удобный доступ к запрятанным внутри данных.

Для понимания дальнейшего материала важно учесть еще одну деталь: каждый стековый элемент сопровождается тегом, определяющим характер его содержимого (штатное число, спецзначение, ноль или пустая ячейка). Полноценные числа получают метку valid. Бесконечности, нечисловые данные (NaN) или денормализованные величины маркируются как special. Нулевые значения получают тег zero. Опустошенный регистр (например, после извлечения данных) помечается как empty. Чип использует теги для внутренней оптимизации и контроля корректности.(примеч.1) Так, при попытке прочитать пустой регистр после избыточного выталкивания данных из стека, сопроцессор сгенерирует исключение недопустимой операции.

Помимо восьми видимых программисту регистров, 8087 имеет скрытые временные буферы для внутренних нужд. Нам понадобятся два из них: tmpA и tmpB. Как и основные регистры, они имеют 80-битную разрядность и собственную пару бит тегов.

Микрокод FXCH

Давайте проследим за логикой выполнения команды FXCH. Эта инструкция производит обмен между верхушкой стека и выбранной ячейкой. Обнаружение пустого состояния в любом из участвующих регистров провоцирует исключение невалидной операции, а недостающие данные заменяются заглушкой NaN (Not a Number).

Ниже представлена последовательность из 14 микрокоманд, реализующих этот процесс.(примеч.2) Первый шаг инициирует копирование: источник — вершина стека ST(0), приемник — временный регитр tmpA. При этом 64 бита мантиссы уходят на соответствующую шину, порядок со знаком — на свою шину, а теги транслируются в блок контроля. Третья микрокоманда действует аналогично, но в качестве источника выступает целевой регистр ST(i), индекс которого заложен в машинной команде.

Точка входа FXCH:
#0200 ST(0) -> tmpA            *прочитать вершину стека*
#0201 nop                      *подождать один такт*
#0202 ST(i) -> tmpB            *прочитать указанный стековый регистр*
#0203 if !(tmpA or tmpB empty) jmp #0210 *перейти, если оба регистра существуют*
#0204 set invalid exception    *возбудить исключение «недопустимая операция»*
#0205 if (unmasked) jmp #0213  *если исключение не замаскировано, завершить*
#0206 if !(tmpA empty) jmp #0208 *проверить, пуст ли tmpA*
#0207 NaN -> tmpA              *если пуст, записать NaN в tmpA*
#0208 if !(tmpB empty) jmp #0210 *проверить, пуст ли tmpB*
#0209 NaN -> tmpB              *если пуст, записать NaN в tmpB*

Здесь сходятся обычный сценарий и ветка обработки ошибки:

0210 tmpB -> ST(0) сохранить tmpB на вершине стека

0211 nop подождать один такт

0212 tmpA -> ST(i) сохранить tmpA в указанном стековом регистре

0213 RNI конец процедуры: Run Next Instruction

0214 nop не используется

0215 nop не используется

0216 nop не используется

Следующая строка (#0203) демонстрирует условный переход. Аппаратная логика опрашивает теги временных буферов на предмет признака пустоты. Специальный бит команды инвертирует проверяемое условие, а смещение на +6 перенаправляет поток выполнения на адрес #0210. Использование относительного смещения вместо абсолютного адреса экономит драгоценное место, требуя всего шесть бит.

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

Если же исключение скрыто маской, пустые ячейки заполняются специальным кодом NaN — сначала проверяется tmpA, затем tmpB. Формирование NaN заставляет шину порядка выставить единицы во все разряды, а шину мантиссы заполнить нулями за исключением пары старших бит, образуя стандартный паттерн нечислового значения.(примеч.3)

На метке #0210 ветвления объединяются, и содержимое временных буферов отправляется обратно в стековые регистры. На этом этапе и происходит искомый обмен: tmpA и tmpB записываются в зеркальные позиции стека. Завершающая команда (примеч.4) Потеря точности сопровождает львиную долю арифметических операций, поскольку вещественные форматы имеют конечную емкость. Наконец, денормализация фиксируется, когда число приближается к нулю настолько плотно, что теряет исходную разрядную точность.

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

Так, при переполнении возвращается бесконечность, при недопустимых действиях подставляется NaN, а при потере точности (скажем, при делении единицы на тройку) результат округляется. Проектировщики 8087 тщательно проработали сценарии послеаварийного восстановления, описав их в мануалах на нескольких страницах.(примеч.5)

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

При активной маске микрокод беспрепятственно подменяет пустые значения на NaN, после чего команда Извлечение микрокода

Массив ПЗУ микрокода 8087 вмещает 26 368 бит, кодирующих 1648 шестнадцатибитных инструкций. Для тех лет это был гигантский объем, и чтобы уместить его на кристалле, специалисты Intel применили полуаналоговую технологию: один транзистор кодировал сразу два бита за счет использования четырех градаций напряжения. Специальные компараторы на выходе преобразовывали уровни сигнала в пары двоичных разрядов.

<img src=»https://habrastorage.org/r/w1560/getpro/habr/upload_files/eca/010/bc5/eca010bc5227c7c4ff8d39ab3059e453.jpeg» alt=»A close-up of the 8087’s microcode ROM, showing 77 transistors. A transistor is formed where a vertical polysilicon line crosses a horizontal stripe of doped silicon.» title=»Крупный план ПЗУ микрокода 8087: видны 77 транзисторов. Транзистор образуется в месте пересечения вертикальной поликремниевой линии и горизонтальной полосы легированного кремния.» width=»1000″ height=»708″ sizes=»(max-width: 780px) 100vw, 50vw» srcset=»https://habrastorage.org/r/w780/getpro/habr/upload_files/eca/010/bc5/eca010bc5227c7c4ff8d39ab3059e453.jpeg 780w,
   https://habrastorage.org/r/w1560/getpro/habr/upload_files/eca/010/bc5/eca010bc5227c7c4ff8d39ab3059e453.jpeg 781h" loading="lazy" decode="async"><div><figcaption><em>Крупный план ПЗУ микрокода 8087: видны 77 транзисторов. Транзистор образуется в месте пересечения вертикальной поликремниевой линии и горизонтальной полосы легированного кремния.</em></figcaption></div></figure><p>Для извлечения прошивки я сфотографировал зоны ПЗУ в сверхвысоком разрешении после травления металлического слоя. Энтузиаст Gloriouscow задействовал нейросети для классификации транзисторов по габаритам (исходники доступны<a href="https://8087.martypc.net/"> здесь</a>). Сопоставление физической сетки транзисторов с битовой картой потребовало кропотливой работы: из-за схемотехнических оптимизаций строки и столбцы оказались перемешаны и зеркально развернуты. Отыскать соответствия помог анализ принципиальных схем, в результате чего мы получили дампы в виде привычной матрицы нулей и единиц.</p><p>Расшифровка смыслового наполнения прошивки оказалась еще более сложной задачей. Если для процессора 8086 существовали подробные патентные описания микроархитектуры, то патенты на 8087 хранили молчание. Нам пришлось восстанавливать назначение команд эмпирически: через изучение схем, поиск закономерностей и логический анализ.</p><p>Микрокод 8087 в принципе далек от простоты из-за экстремальных нагрузок на кремний своего времени. Разработчики были вынуждены использовать массу неочевидных трюков: например, условные переходы здесь могут параллельно обновлять регистры, а некоторые микрокоманды надолго меняют поведение последующих цепочек. Наше исследование продолжается, и многие тонкости аппаратного воплощения еще ждут своего часа.</p><details class="spoiler"><summary>Примечания и ссылки</summary><div class="spoiler__content"><a class="anchor" name="1" id="1"></a><p>1. В штатном режиме теги скрыты от прикладного софтвера, но их можно прочитать, выгрузив полный снимок памяти состояния сопроцессора, где они упакованы в 16-битное слово.</p><a class="anchor" name="2" id="2"></a><p>2. Первоисточник декодированного микрокода от Smartest Blob доступен<a href="https://github.com/a-mcego/granite/blob/a1d48a37dff90e46eef11a17baa9c788339cd1d2/tools/8087mc/bin/8087mc_out.txt#L201"> по ссылке</a>. Для читаемости формат был слегка модифицирован.</p><a class="anchor" name="3" id="3"></a><p>3. Специальное нечисловое значение NaN имеет множество вариаций (любой шаблон с единицами в поле порядка и ненулевой мантиссой). Для невалидных операций 8087 использует канонический вариант <em>real indefinite</em>: в 80-битном формате старшие биты мантиссы равны единице, остальные нули, а порядок со знаком заполнены единицами (см. стр. 87 и 90 в документации). В 32- и 64-битных аналогах макет выглядит чуть иначе из-за неявного старшего бита.</p><a class="anchor" name="4" id="4"></a><p>4. Классическое деление на ноль генерирует соответствующую ошибку, однако операция 0 ÷ 0 трактуется как невалидная, в то время как деление бесконечности на ноль считается легитимным и дает на выходе бесконечность. Это лишь один из факторов, усложнявших логику микрокода.</p><a class="anchor" name="5" id="5"></a><p>5. Исчерпывающие спецификации исключений 8087 приведены в издании<a href="https://ethw.org/w/images/2/2f/Intel_8086_family_users_numeric_supp.pdf"> 8086 Family Numerics Supplement</a> (описание системы на стр. 32, маски и флаги на стр. 24, детали сценариев на стр. 89).</p></div></details><p><strong>Читайте также</strong></p><blockquote><ul><li><p><a href="https://habr.com/ru/companies/otus/articles/980026/">Схемотехника стека сопроцессора Intel 8087 для чисел с плавающей запятой: реверс-инжиниринг</a> </p></li><li><p><a href="https://habr.com/ru/companies/otus/articles/985872/">Два бита на транзистор: ПЗУ микрокода повышенной плотности в FPU-сопроцессоре Intel 8087</a></p></li><li><p><a href="https://habr.com/ru/companies/otus/articles/1014200/">Декодирование инструкций в сопроцессоре с плавающей точкой Intel 8087</a></p></li><li><p><a href="https://habr.com/ru/companies/otus/articles/987032/">Анализ кристалла 8087: быстрый битовый шифтер математического сопроцессора</a></p></li></ul></blockquote></div></div>
 

Источник

Поделиться:

Похожие статьи

Поиск по играм, новостям и статьям…

Введите не менее двух символов

Введите не менее двух символов