[Перевод] Толстые слои легаси: как запускаются современные процессоры Intel

roteefok4ued1thu9puvsujze-q.jpeg


Центральные процессоры (CPU) не могут ничего сделать, пока им не скажут, что делать. Возникает очевидная проблема — как вообще заставить CPU что-то делать? Во многих CPU эта задача решается при помощи вектора сброса — жёстко прописанного в CPU адреса, из которого нужно начинать считывать команды при подаче питания. Адрес, на который указывает вектор сброса, обычно представляет собой какую-нибудь ROM или флэш-память, которую CPU может считать, даже если никакое другое оборудование ещё не сконфигурировано. Это позволяет производителю системы создавать код, который будет исполнен сразу же после включения питания, сконфигурирует всё остальное оборудование и постепенно переведёт систему в состояние, при котором она сможет выполнять пользовательский код.

Конкретная реализация вектора сброса в системах x86 со временем менялась, но, по сути, это всегда были 16 байтов ниже верхушки адресного пространства, то есть 0xffff0 на 20-битном 8086, 0xfffff0 на 24-битном 80286 и 0xfffffff0 на 32-битном 80386. По стандарту в системах x86 ОЗУ начинается с адреса 0, поэтому верхушку адресного пространства можно использовать для размещения вектора сброса с минимальной вероятностью конфликта с ОЗУ.
Однако самое примечательное в x86 здесь то, что когда он начинает выполнять код из вектора сброса, он всё ещё находится в режиме реальной адресации (real mode). Real mode x86 — это рудимент гораздо более ранней компьютерной эпохи. Адреса не абсолютны (то есть, когда ты обращаешься к 32-битному адресу, то хранишь весь адрес в 32-битном регистре или регистре большего размера), это 16-битные смещения, прибавляемые к значению, хранящемуся в «сегментном регистре». Для кода, данных и стека существуют собственные сегментные регистры, поэтому 16-битный адрес может относиться к разным реальным адресам в зависимости от того, как он интерпретируется: переход на 16-битный адрес приведёт к тому, что этот адрес прибавляется к сегментному регистру кода, а считывание из 16-битного адреса приведёт к тому, что адрес будет прибавлен к сегментному регистру данных, и так далее. Всё это нужно для сохранения совместимости со старыми чипами, вплоть до того, что даже 64-битные x86 запускаются в real mode с сегментами и всем остальным (и тоже начинают исполнение с 0xfffffff0, а не с 0xfffffffffffffff0 — 64-битный режим не поддерживает real mode, поэтому 64-битный физический адрес невозможно выразить при помощи сегментных регистров, и мы всё равно начинаем сразу ниже 4 ГБ, даже несмотря на то, что доступно гораздо большее адресное пространство).

Ну да ладно, это знают все. В современных системах UEFI-прошивка, запускаемая из вектора сброса, перепрограммирует CPU во вразумительный режим (то есть без всей этой фигни с сегментацией), выполняет различные действия, например конфигурирует контроллер памяти, чтобы можно было получить доступ к ОЗУ (при этом процессе кэш CPU используется как ОЗУ, потому что программирование контроллера памяти — довольно сложная задача, ведь нужно хранить больше информации о состоянии, чем поместится в регистрах, то есть необходимо ОЗУ, но у нас нет ОЗУ, пока не заработает контроллер памяти; к счастью, у CPU есть много собственных мегабайтов ОЗУ, так что можно вздохнуть с облегчением). Это довольно криво, но таковы последствия хорошо понятных легаси-решений.

Впрочем… на самом деле, всё не так. Современный Intel x86 запускается иначе. Всё гораздо страннее. Да, кажется, что именно так всё и происходит, но за кулисами выполняется множество других действий. Давайте поговорим о безопасности запуска. Принцип любого вида верифицируемого запуска (например, UEFI Secure Boot) в том, что подпись на следующем компоненте цепочки запуска валидируется до исполнения компонента. Но что верифицирует первый компонент цепочки запуска? Нельзя просто попросить BIOS проверить саму себя — если нападающий сможет заменить BIOS, то его версия BIOS просто соврёт о том, что это сделано. Intel решила эту проблему с помощью Boot Guard.

Но прежде чем мы доберёмся до Boot Guard, нам нужно обеспечить работу CPU в максимально свободном от багов состоянии. Поэтому при своём запуске CPU исследует флэш-память системы и ищет заголовок, указывающий на обновления микрокода CPU. В комплекте CPU компании Intel есть встроенный микрокод, но часто он старый и забагованный, поэтому прошивка системы может решить включить копию, которая достаточно новая, чтобы надёжно работать. Образ микрокода копируется из флэш-памяти, проверяется подпись и новый микрокод начинает работать. Это справедливо и с использованием Boot Guard, и без него. Однако в случае Boot Guard перед переходом к вектору сброса микрокод в CPU считывает из флэш-памяти Authenticated Code Module (ACM) и проверяет его подпись с прописанным Intel ключом. Если они совпадают, он начинает исполнять ACM. Здесь следует помнить, что CPU не может просто верифицировать ACM, а затем исполнить его непосредственно из флэш-памяти: если бы он сделал это, то флэш-память могла бы распознать это, передавать для верификации подлинный ACM, а затем передать CPU другие команды при их повторном считывании для исполнения (уязвимость Time of Check vs Time of Use, или TOCTOU). То есть перед верификацией и исполнением ACM его нужно скопировать в CPU, то есть нам необходима ОЗУ, то есть CPU уже нужно знать, как сконфигурировать свой кэш, чтобы можно было использовать его в качестве ОЗУ.

Ну да ладно. Теперь мы загрузили и верифицировали ACM, после чего его можно спокойно исполнить. ACM выполняет разные вещи, но самое важное с точки зрения Boot Guard заключается в том, что он считывает набор фьюзов с однократной записью на чипсете материнской платы, представляющий собой SHA256 публичного ключа. Затем он считывает первый блок прошивки (Initial Boot Block, или IBB) в ОЗУ (точнее, как говорилось выше, в кэш) и парсит его. В нём есть блок, содержащий публичный ключ — он хэширует этот ключ и верифицирует, что он соответствует SHA256 из фьюзов. Затем он использует этот ключ для валидации подписи на IBB. Если всё правильно, он исполняет IBB, и всё начинает выглядеть как красивая простая модель, о которой мы говорили выше.

Только не кажется ли вам, что весь этот код чрезвычайно сложно реализовать в real mode? И да, выполнение всех этих расчётов современной криптографии только с 16-битными регистрами было бы очень мучительной задачей. Поэтому всё происходит не так. Всё это происходит в абсолютно вразумительном 32-битном режиме, а после этого CPU на самом деле переключается в ужасную конфигурацию, чтобы сохранить совместимость с 80386, выпущенным в 1986 году. «Хорошая» новость в том, что хотя бы прошивка может определить, что CPU уже конфигурировал кэш как ОЗУ и может не делать этого самостоятельно.

Здесь я пропускаю несколько этапов — на самом деле ACM выполняет и другие задачи: проверяет прошивку в TPM и настраивает TXT для тех, кому нужен DRTM, но если вкратце, то CPU переводит себя в состояние, в котором он работает как современный CPU, а затем намеренно снова отключает кучу удобной функциональности, прежде чем начать исполнять прошивку. Также я опускаю тот факт, что весь этот процесс запускается только после того, как разрешит Management Engine, а это означает, что мы ждём, пока полностью независимый x86 запустит всю ОС, прежде чем CPU хотя бы начнёт притворяться, что исполняет прошивку системы.

Разумеется, как говорилось выше, в современных системах прошивка потом перепрограммирует CPU во что-то более вразумительное, поэтому разработчикам ОС больше не нужно об этом волноваться [1][2]. Это значит, что мы перескакивали между несколькими состояниями только из-за вероятности того, что кто-то захочет запустить легаси-BIOS, а затем загрузить DOS на CPU, в котором на пять порядков больше транзисторов, чем в 8086.

Почему мой x86 не может пробудиться с уже имеющимся у него protected mode.

[1] Только при resume ACPI мы пропустим основную часть кода настройки прошивки, поэтому нам придётся работать с CPU в чёртовом 16-битном режиме, потому что suspend/resume — это, по сути, чрезвычайно долгий цикл перезапуска

[2] А, да, и ведь скорее всего, у вашего CPU несколько ядер: и плохие новости, связанные с состоянием, заключаются в том, что большинство ядер при запуске ОС прошивка не запускает, поэтому они будут находится в 16-битном real mode, даже если запускаемый CPU уже находится в 64-битном protected mode; немного иная кошмарная ситуация возникает, если вы использовали TXT. Точнее, так было раньше, но в ACPI 6.4 (выпущенном в 2021 году) есть механизм, позволяющий ОС просить прошивку разбудить CPU так, что это будет невидимо для ОС, но в таком случае прошивке всё равно придётся выполнять множество сложных задач.

© Habrahabr.ru