Интерфейс и обмен регистрами в микрокоде сопроцессора Intel 8087
Intel 8087 выпускался в 40-выводном корпусе DIP (dual in-line package, корпус с двумя рядами выводов).
В 1980 году корпорация Intel презентовала 8087 — новаторский сопроцессор, который увеличивал скорость вычислений с плавающей запятой почти в сто раз. Этот чип заложил фундамент для всей современной архитектуры процессоров: подавляющее большинство актуальных CPU по сей день задействуют тот самый стандарт арифметики, впервые воплощенный в модели 8087.
Для безупречно точного расчета тригонометрических функций, экспонент и квадратных корней 8087 опирается на изощренные вычислительные алгоритмы. На кремниевом уровне они запрограммированы в виде микрокода. Будучи участником исследовательского объединения Opcode Collective, я занимаюсь обратной разработкой (реверс-инжинирингом) этого внутреннего кода. В настоящей публикации я детально препарирую микрокод конкретной инструкции 8087 — FXCH — и продемонстрирую ее изнанку. Команда В рамках этой статьи наше внимание привлекут временные и стековые регистры, подсвеченные красным цветом. Архитектура чипа включает два временных и восемь стековых регистров, каждый из которых удерживает порядок и значащую часть числа. Дополнительно каждый регистр располагает двумя битами тегов для идентификации типа хранимых данных. Логика управления стеком, расположенная справа, непрерывно отслеживает актуальную вершину стекового массива при операциях пуша и попа. Исполнение сложной математической команды вроде арктангенса требует сотен внутрипроцессорных итераций. Они запрограммированы на уровне микрокода, где каждая элементарная инструкция задает конкретный шаг алгоритма. (Здесь критически важно разделять макроинструкции ассемблера, видимые разработчику, и скрытые низкоуровневые микрокоманды внутри кремния). В ПЗУ микрокода прошито 1648 инструкций, обеспечивающих богатый набор функций 8087. Каждая микрокоманда имеет 16-битную длину и выполняет примитивные действия: пересылает байты между внутренними буферами, производит сложение или сдвиг информации. Совместно с Opcode Collective мы проводим реверс-инжиниринг этих структур для глубокого понимания логики чипа (ссылка). Внутреннее устройство микроинструкций 8087 довольно запутано из-за обилия исключений и специализированной логики, поэтому здесь я приведу лишь адаптированную выжимку. Каждый 16-битный элемент начинается с трех бит типа, определяющих интерпретацию остальных разрядов. Первый тип ведает переброской данных между внутренними регистрами, где отдельные поля задают отправителя и получателя, а оставшаяся тройка битов обрабатывает краевые сценарии. Второй тип управляет операциями сдвига с помощью баррельного сдвигача. Третий задействует арифметико-логическое устройство (сумматор-вычитатель), которое также может циклически применяться для умножения и деления. Четвертая категория объединяет управляющие команды: они конфигурируют сумматор, задают правила округления и т. д. Команды дальнего перехода и вызова адресуют подпрограммы из жесткого списка. Поле условий позволяет организовывать ветвления по разным критериям, а завершающий бит инвертирует логику условия. Локальные переходы служат для точечного смещения к соседним микрокомандам. Наконец, прочие типы инструкций выполняют вспомогательные функции: от возвратов из подпрограмм и генерации прерываний до фиксации завершения микрокода. Сопроцессор поддерживает массу форматов: различные типы чисел с плавающей точкой, целые величины и упакованные десятичные числа. Тем не менее на аппаратном уровне абсолютно все данные приводятся к единому 80-битному формату с плавающей запятой. Подобное число состоит из знакового бита, 15-битного порядка и 64-битной мантиссы. Внутри кремния функционируют два независимых тракта: один для мантиссы, второй — для знака и порядка. Для манипуляций с данными в процессе вычислений выделено восемь регистров, образующих верхний ряд на схеме. Их организация не совсем привычна — это стек, куда элементы заносятся последовательно и откуда извлекаются. Программа адресуется не к абстрактному «регистру №3», а к элементу ST(3), находящемуся на три позиции ниже вершины стека. При добавлении или извлечении данных индексы ячеек динамически смещаются. Изначально разработчики полагали, что такая стековая модель оптимизирует систему команд, облегчит написание компиляторов и повысит скорость вызовов, хотя на практике эти ожидания оправдались лишь отчасти. Большинство операций 8087 завязаны на вершину стека. Например, команда извлечения корня модифицирует именно элемент ST(0). А как быть, если требуется обработать число, застрявшее где-то в глубине стека? Именно для этого и предусмотрена инструкция FXCH, которой посвящена наша статья. Она попросту меняет местами верхушку стека с произвольным указанным регистром, открывая удобный доступ к запрятанным внутри данных. Для понимания дальнейшего материала важно учесть еще одну деталь: каждый стековый элемент сопровождается тегом, определяющим характер его содержимого (штатное число, спецзначение, ноль или пустая ячейка). Полноценные числа получают метку valid. Бесконечности, нечисловые данные (NaN) или денормализованные величины маркируются как special. Нулевые значения получают тег zero. Опустошенный регистр (например, после извлечения данных) помечается как empty. Чип использует теги для внутренней оптимизации и контроля корректности.(примеч.1) Так, при попытке прочитать пустой регистр после избыточного выталкивания данных из стека, сопроцессор сгенерирует исключение недопустимой операции. Помимо восьми видимых программисту регистров, 8087 имеет скрытые временные буферы для внутренних нужд. Нам понадобятся два из них: tmpA и tmpB. Как и основные регистры, они имеют 80-битную разрядность и собственную пару бит тегов. Давайте проследим за логикой выполнения команды FXCH. Эта инструкция производит обмен между верхушкой стека и выбранной ячейкой. Обнаружение пустого состояния в любом из участвующих регистров провоцирует исключение невалидной операции, а недостающие данные заменяются заглушкой NaN (Not a Number). Ниже представлена последовательность из 14 микрокоманд, реализующих этот процесс.(примеч.2) Первый шаг инициирует копирование: источник — вершина стека ST(0), приемник — временный регитр tmpA. При этом 64 бита мантиссы уходят на соответствующую шину, порядок со знаком — на свою шину, а теги транслируются в блок контроля. Третья микрокоманда действует аналогично, но в качестве источника выступает целевой регистр ST(i), индекс которого заложен в машинной команде. Здесь сходятся обычный сценарий и ветка обработки ошибки: Следующая строка (#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 применили полуаналоговую технологию: один транзистор кодировал сразу два бита за счет использования четырех градаций напряжения. Специальные компараторы на выходе преобразовывали уровни сигнала в пары двоичных разрядов.
Микрокод 8087

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

Микрокод FXCH
Точка входа 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 не используется
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>
Аудитория Китайского национального суперкомпьютерного интернета превысила 1,7 миллиона человек
Раскрыты характеристики компактного флагмана Honor Magic 9: экран 6,37 дюйма, батарея на 8000 мАч и две камеры по 200 Мп
36 тысяч смартфонов за сутки: в Китае успешно стартовали продажи новой модели Xiaomi за $1640
В Китае запущен крупнейший в мире подземный аккумулятор воздуха мощностью 700 МВт
Starlink открывает бесплатный доступ для жителей острова Кауаи после удара мощного урагана
В Китае поступил в продажу мощный NAS Feiniu Evo 4 Pro на процессоре Intel с 10GbE и до 96 ГБ ОЗУ
Momax Q.Pass X: ультратонкий аккумулятор в цвете iPhone 18 Pro с индикатором циклов заряда
Mechazilla подняла следующий Starship: корабль готовят к поимке при возвращении