Микропроцессор 8086 — это революционный процессор, представленный компанией Intel в 1978 году. Его появление привело к тому, что архитектура x86 и сегодня продолжает доминировать в сфере десктопов и серверов. При реверс-инжиниринге 8086 по фотографиям кристалла моё внимание привлекла одна цепь, потому что её физическая структура на кристалле не соответствовала окружающим её цепям. Оказалось, что эта цепь реализует особую функциональность для пары команд, немного изменяя способ их взаимодействия с прерываниями. В процессе веб-поисков выяснилось, что это поведение было изменено Intel в 1978 году, чтобы устранить проблему с первыми версиями чипа 8086. Изучив кристалл, мы можем понять, как Intel справлялась с багами в микропроцессоре 8086.
В современных CPU баги часто можно устранять при помощи патча микрокода, который обновляет CPU при запуске. [Современный процесс обновления микрокода сложнее, чем я ожидал: обновления возможны ещё до вмешательства BIOS, при запуске или даже при выполнении приложений. Подробности компания Intel излагает здесь. Очевидно, Intel изначально добавила микрокод с возможностью патчинга ещё в Pentium Pro, чтобы иметь возможность отладки и тестирования чипа, но потом компания осознала, что это будет полезной функцией для устранения багов в реальных условиях использования.]
Однако до Pentium Pro (1995 год) микропроцессоры можно было исправлять только изменениями в архитектуре, которые устраняли ошибки в кремнии. Это стало для Intel большой проблемой, когда возник знаменитый баг Pentium деления с плавающей запятой. Оказалось, что чип содержит баг, приводящий к редким, но серьёзным ошибкам при делении. В 1994 году Intel отозвала дефектные процессоры и заменила их, что стоило компании $475 миллионов.
Электросхема кристалла
На фото ниже показан кристалл 8086 с помеченными основными функциональными блоками. На фото виден металлический слой поверх кремния. У современных чипов может быть более дюжины слоёв металла, а у 8086 он был только один. Но несмотря на это, металл по большей мере скрывает находящийся под ним кремний. По краю кристалла мы видим провода, соединяющие контактные площадки чипа с 40 внешними контактами.
Кристалл 8086 с помеченными основными функциональными блоками.
Важная для нас часть чипа — это Group Decode ROM в верхнем центральном участке. Назначение этой схемы — разбиение команд на группы, управляющие тем, как они декодируются и обрабатываются. Например, очень простые команды (скажем, устанавливающие флаг) могут выполняться напрямую, в одном цикле. Другие команды являются не полными командами, а префиксами, модифицирующими следующую за ними команду. Оставшаяся часть команд реализована в микрокоде, который хранится в правом нижнем углу чипа. Многие из этих команд имеют второй байт (байт «Mod R/M»), указывающий регистр и схему адресации памяти. Некоторые команды имеют две версии: одна для 8-битного операнда, другая для 16-битного операнда. У некоторых операций есть бит, меняющий местами источник и получатель. Group Decode ROM изучает 8 битов команды и решает, к каким группам она относится.
Увеличенный снимок Group Decode ROM. Это фото составлено из металлического, поликремниевого и кремниевого слоёв.
На фото выше показано более детальное изображение Group Decode ROM. Строго говоря, Group Decode ROM больше похожа на PLA (Programmable Logic Array), чем на ROM, но Intel назвала её ROM. Это равномерная сетка логических элементов, позволяющая плотно упаковать вентили. Нижняя часть состоит из вентилей NOR, соответствующих различным паттернам команд. Биты команд подаются горизонтально слева, а каждый вентиль NOR выстроен вертикально. Выводы этих вентилей NOR подаются на набор горизонтальных вентилей NOR в верхней половине, комбинирующих сигналы из нижней половины для создания групповых выводов. Эти вентили NOR имеют вертикальные вводы и горизонтальные выводы.
На схеме ниже показан увеличенный вид Group Decode ROM, демонстрирующий структуру вентилей NOR. Розоватые области — это кремний, легированный примесями для создания полупроводника. Серые горизонтальные линии — это поликремний, особый тип кремния сверху. Там, где поликремний пересекается с проводящим кремнием, он образует транзистор. Транзисторы соединяются между собой металлическими проводами сверху. (Я растворил слой металла кислотой, чтобы показать кремний; синими линиями показано, где находились два металлических проводника.) Когда ввод имеет высокий уровень, он включает соответствующие транзисторы, отключая вертикальные линии. Это создаёт вентили NOR с несколькими вводами. Основная идея PLA заключается в том, что в каждой точке пересечения горизонтальных и вертикальных линий может присутствовать или отсутствовать транзистор для выбора нужных вводов вентилей. Легируя кремний в нужном паттерне, можно по необходимости создавать или не создавать транзисторы. На схеме ниже выделено два транзистора. Видно, что в некоторых из других локаций есть транзисторы, а в прочих их нет. Так PLA позволяет гибким образом создавать плотный массив выводов из массива вводов.
Увеличенный снимок части Gate Decode ROM, демонстрирующий несколько транзисторов.
Немного изменив масштаб, можно увидеть, что PLA соединён с какой-то необычной схемой, показанной ниже. Последние два столбца в PLA довольно любопытны. Верхняя половина не используется. Вместо этого два сигнала выходят сбоку PLA горизонтально и обходят верхнюю часть PLA. Эти сигналы идут к вентилю NOR и инвертору, который как будто находится в пустоте, отделённый от остальной части логики. Вывод из этих вентилей идёт к вентилю NOR с тремя вводами, который любопытным образом разбит на две части. Нижняя часть — это обычный вентиль NOR с двумя вводами, но потом идёт транзистор для третьего ввода (тот, который мы изучали) на некотором расстоянии от него. Необычно, что вентиль разделён таким расстоянием.
Схема, которую мы видим на кристалле.
Возможно, вам трудно понять масштаб этих схем. Выделенный на изображении ниже прямоугольник соответствует области, показанной выше. Как видите, рассматриваемая нами схема занимает довольно большую часть кристалла.
Красный прямоугольник на этом рисунке соответствует области на схеме выше.
Далее я захотел ответить на вопрос, на какие команды влияет эта загадочная схема. Посмотрев на паттерн транзисторов в Group Decode ROM, я определил, что два интересующих нас столбца соответствуют командам с битами 10001110 и 000xx111. Изучив документацию по 8086, можно понять, что первый битовый паттерн соответствует командам MOV sr,xxx
, которые загружают значение в сегментный регистр. Второй битовый паттерн соответствует командам POP sr
, извлекающим значение из стека в сегментный регистр. Но почему эти команды требуют особой обработки?
Баг прерывания
Поискав информацию об этих командах, я наткнулся на список выявленных дефектов, в котором говорилось: «Прерывания, следующие за командами MOV SS,xxx и POP SS, могут повреждать память. На первых процессорах Intel 8088 (с маркировкой „INTEL ‘78“ или „© 1978“), в случае, если прерывание происходит непосредственно после команды MOV SS,xxx или POP SS, данные могут быть извлечены с неправильным адресом стека, что приводит к повреждению памяти». Оказалось, именно для устранения этого бага и нужна загадочная схема.
Расскажу немного подробнее. У 8086, как и у большинства процессоров, есть функция прерывания: внешний сигнал, например, таймер или ввод/вывод, может прервать текущую программу. Процессор начинает выполнять другой код для обработки прерывания, а затем возвращается к исходной программе, продолжая с того, на чём закончил. При прерывании процессор использует свой стек в памяти, чтобы отслеживать то, что происходило в исходной программе, для дальнейшего её выполнения. Указатель стека (SP) — это регистр, отслеживающий, где в памяти находится стек.
Сложность в том, что 8086 использует «сегментированную память»: память разделена на блоки (сегменты), имеющие разное предназначение. У 8086 есть четыре сегмента: сегмент кода (Code Segment), сегмент данных (Data Segment), сегмент стека (Stack Segment) и дополнительный сегмент (Extra Segment). С каждым сегментом связан соответствующий сегментный регистр, хранящий начальный адрес этого сегмента в памяти. Допустим, вам нужно изменить местоположение стека в памяти, например, потому, что вы запускаете новую программу. Вам нужно изменить регистр сегмента стека (Stack Segment, SS) так, чтобы он указывал на новое местоположение сегмента стека. Также нужно изменить регистр указателя стека (Stack Pointer, SP), чтобы он указывал на текущую позицию стека в сегменте стека.
Проблема возникает, если процессор получает прерывание после изменения регистра сегмента стека, но до того, как был изменён регистр указателя стека. Процессор будет сохранять информацию в стеке, используя старый адрес указателя стека, но в новом сегменте. Таким образом, информация, по сути, сохраняется в случайное место в памяти, а это плохо. [Очевидное решение этой проблемы заключается в том, чтобы отключать прерывания во время изменения регистра сегмента стека, а после изменения снова включать прерывания. Это стандартный способ борьбы с возникновением прерываний в «неподходящий момент». Проблема в том, что 8086 (как и большинство микропроцессоров) имеет немаскируемое прерывание (non-maskable interrupt, NMI); оно предназначено для очень важных вещей и его нельзя отключить.]
Intel решила проблему так: процессор откладывает прерывание после завершения обновления регистра сегмента стека, чтобы была возможность обновить указатель стека.
[Intel задокументировала такое поведение в примечании на странице 2-24 руководства пользователя:
Существует несколько случаев, при которых запрос прерывания не распознаётся до момента завершения выполнения следующей команды. Префиксы повтора, LOCK и переопределения сегмента считаются «частью» команд, которые они предваряют; между исполнением префикса и команды прерывания не распознаются. Команда MOV (move) в сегментный регистр и команда POP в сегментный регистр обрабатываются аналогично: прерывание не распознаётся до момента завершения следующей команды. Этот механизм защищает программу, переходящую на новый стек (обновлением SS и SP). Если бы прерывание распознавалось после изменения SS, но до изменения SP, процессор бы записал флаги, CS и IP в неверную область памяти. Из этого следует, что когда сегментный регистр и другое значение должны обновляться одновременно, первым следует изменять сегментный регистр, а сразу же за этим должна следовать команда, изменяющая другое значение. Также существует два случая (WAIT и повторяющиеся команды со строками), в которых запрос на прерывание распознаётся посередине команды. В таких случаях прерывания принимаются после любой завершённой примитивной операции или тестового такта ожидания.
Любопытно, что устранение проблемы на чипе сделано неоправданно широкомасштабным: прерывание задерживает команда MOV
или POP
для любого сегментного регистра. На то нет никаких аппаратных причин: благодаря структуре PLA все необходимые биты команд имеются в наличии, и было бы не сложнее выполнять проверку конкретно на сегмент стека. Исправление путём откладывания прерываний после POP или MOV сохраняется в архитектуре x86 и по сей день. Однако его улучшили: теперь задержку вызывают только команды, влияющие на регистр сегмента стека; операции с другими сегментными регистрами на неё никак не влияют.]
Регистр сегмента стека можно изменять двумя способами. Во-первых, можно записать значение в регистр (MOV SS, xxx
на языке ассемблера) или извлечь значение из стека в регистр сегмента стека (POP SS
). На эти две команды и влияет наша загадочная схема. Таким образом, мы видим, что Intel добавила схему для выполнения задержки сразу после одной из этих команд и устранения бага.
Выводы
Один из интересных аспектов реверс-инжиниринга 8086 заключается в том, что я нашёл любопытную особенность на кристалле, а затем обнаружил, что он соответствует малоизвестной части документации 8086. В целом, это преднамеренные архитектурные решения, однако они демонстрируют, насколько сложной и бессистемной была архитектура 8086, у которой было много особых случаев. Каждый из этих случаев приводил к появлению новых схем и вентилей, усложняющих чип. (Для сравнения: я выполнил реверс-инжиниринг ARM1 — RISC-процессора, ставшего началом архитектуры ARM. Процессор ARM1 имеет гораздо более простую архитектуру с очень малым количеством пограничных случаев. Это отразилось и в схеме, которая оказалась гораздо проще.)
Однако ситуация с сегментными регистрами и прерываниями стала первым найденным мной фрагментом кристалла 8086, являющимся частью устранения бага. Похоже, это исправление было довольно хитрым, из-за него по неиспользованным частям чипа разбросано множество вентилей. Было бы интересно получить фото кристалла ранних версий чипа 8086, ещё до этого исправления, чтобы убедиться в изменениях и посмотреть, что ещё было модифицировано.
Если вас интересует процессор 8086, то я уже писал о кристалле 8086, процессе уменьшения структур кристалла и регистрах 8086.
Автор:
PatientZero