Пишем DXE-драйвер для снятия скриншотов с BIOS Setup и других UEFI-приложений

Пишем DXE-драйвер для снятия скриншотов с BIOS Setup и других UEFI-приложений

В прошлой статье про SecureBoot мне очень не хватало возможности сделать снимок экрана при настройке UEFI через BIOS Setup, но тогда выручило перенаправление текстовой консоли в последовательный порт. Это отличное решение, но доступно оно на немногих серверных материнских платах, и через него можно получить только псевдографику, а хотелось бы получить настоящую — она и выглядит приятнее, и вырезать ее каждый раз из окна терминала не надо. Вот именно этим мы и займемся в этой статье, а заодно я расскажу, что такое DXE-драйвер и как написать, собрать и протестировать такой самостоятельно, как работают ввод с клавиатуры и вывод на экран в UEFI, как найти среди подключенных устройств хранения такое, на которое можно записывать файлы, как сохранить что-нибудь в файл из UEFI и как адаптировать какой-то внешний код на С для работы в составе прошивки. Если вам все еще интересно — жду вас под катом.

Отказ от ответственности

Прежде чем говорить о написании и отладке драйверов для UEFI, стоит сразу же сказать, что эксперименты с прошивкой — дело опасное, они могут привести к «кирпичу», а в самых неудачных редких случаях — к выходу из строя аппаратуры, поэтому я заранее предупреждаю: всё, что вы тут прочитаете, вы используете на свой страх и риск, я не несу и не буду нести ответственность за потерю работоспособности вашей прошивки или платы. Прежде чем начинать любые эксперименты с прошивкой, необходимо сделать полную копию всего содержимого SPI flash при помощи программатора. Только так вы можете гарантировать успешное восстановление прошивки после любого программного сбоя. Если у вас нет программатора, но попробовать написать и отладить DXE -драйвер очень хочется, используйте для этого OVMF, VmWare Workstation 12 или любые другие системы виртуализации с поддержкой UEFI на ваш выбор.

Что там нужно и почему это DXE-драйвер

Задача наша состоит в том, чтобы снять скриншот со всего экрана во время работы какого-то UEFI-приложения, например BIOS Setup, нажатием определенной комбинации клавиш, найти файловую систему с доступом на запись и сохранить полученный скриншот на нее. Также было бы неплохо получить какую-то индикацию статуса. Т.к. для снятия скриншота потребуется прерывать работу UEFI-приложений, сама программа по их снятию приложением быть не может, ведь никакой вытесняющей многозадачности в UEFI пока еще не предусмотрено, поэтому нам нужен DXE-драйвер. Схема его работы планируется примерно следующая: 0. Загружаемся только после появления текстового ввода (чтобы обрабатывать нажатия комбинации клавиш) и графического вывода (чтобы было с чего снимать скриншоты). 1. Вешаем обработчик нажатия комбинации LCtrl + LAlt + F12 (или любой другой на ваш вкус) на все доступные входные текстовые консоли. 2. В обработчике находим все выходные графические консоли, делаем с них скриншот и перекодируем его в формат PNG (т.к. UEFI-приложения обычно не используют миллионы цветов, то в этом формате скриншоты получаются размером в десятки килобайт вместо нескольких мегабайт в BMP). 3. В том же обработчике находим первую попавшуюся ФС с возможностью записи в корень и сохраняем туда полученные файлы. Можно расширить функциональность выбором не первой попавшейся ФС, а, к примеру, только USB-устройств или только разделов ESP , оставим это на самостоятельную работу читателю.

Выбираем SDK

Для написания нового кода для работы в UEFI имеются два различных SDK — более новый EDK2 от UEFI Forum и GNU-EFI от независимых разработчиков, основанный на старом коде Intel. Оба решения подразумевают, что вы будете писать код на C и/или ассемблере, в нашем случае постараемся обойтись чистым C. Не мне судить, какой SDK лучше, но я предлагаю использовать EDK2, т.к. он официальный и кроссплатформенный, и новые фичи (вместе с исправлением старых багов) появляются в нем значительно быстрее благодаря близости к источнику изменений, плюс именно его используют все известные мне IBV для написания своего кода. EDK2 находится в процессе постоянной разработки, и в его trunk стабильно добавляют по 2-3 коммита в день, но так как мы здесь за самыми последними веяниями не гонимся (все равно они еще ни у кого не работают), поэтому будем использовать последний на данный момент стабильный срез EDK2, который называется UDK2015. Чтобы обеспечить кроссплатформенность и возможность сборки различными компиляторами, EDK2 генерирует make-файлы для каждой платформы, используя конфигурационные файлы TXT (конфигурация окружения), DEC, DSC и FDF (конфигурация пакета) и INF (конфигурация компонента), подробнее о них я расскажу по ходу повествования, а сейчас нужно достать EDK2 и собрать HelloWorld, чем и займемся, если же вам не терпится узнать подробности прямо сейчас — проследуйте в документацию.

Настраиваем сборочное окружение

Подразумевается, что нужное для сборки кода на C и ассемблере ПО уже установлено на вашей машине. Если нет, пользователям Windows предлагаю установить Visual Studio 2013 Express for Windows Desktop, пользователям Linux и OSX понадобятся GCC 4.4-4.9 и NASM. Если все это уже установлено, осталось только скачать UDK2015, распаковать все содержимое UDK2015.MyWorkSpace.zip туда, где у вас есть право на создание файлов (да хоть прямо на рабочий стол или в домашнюю директорию), а затем распаковать содержимое BaseTools(Windows).zip или BaseTools(Unix.zip) в получившуюся на предыдущем шаге директорию MyWorkSpace, которую затем переименовать в что-то приличное, например в UDK2015. Теперь открываем терминал, переходим в только что созданную директорию UDK2015 и выполняем там скрипт edksetup.bat (или .sh), который скопирует в поддиректорию Conf набор текстовых файлов, нас будут интересовать tools_def.txt и target.txt. Первый файл достаточно большой, в нем находятся определения переменных окружения с путями до необходимых сборочному окружению компиляторов C и ASL , ассемблеров, линковщиков и т.п. Если вам нужно, можете исправить указанные там пути или добавить свой набор утилит (т.н. ToolChain), но если вы послушали моего совета, то вам без изменений подойдет либо VS2013 (если у вас 32-разрядная Windows), либо VS2013x86 (в случае 64-разрядной Windows), либо GCC44 |… | GCC49 (в зависимости от вашей версии GCC, которую тот любезно показывает в ответ на gcc --version). Во втором файле содержатся настройки сборки по умолчанию, в нем я рекомендую установить следующие значения:Откройте еще один терминал в UDK2015 и в Linux/OSX выполните команду: В случае Windows достаточно обычного edksetup.bat без параметров. Теперь протестируем сборочное окружение командой build, если все было сделано верно, то после определенного времени на закончится сообщением вроде Если же вместо Done вы видите Failed, значит с вашими настройками что-то не так. Я проверил вышеуказанное на VS2013x86 в Windows и GCC48 в Xubuntu 14.04.3 — УМВР .

Структура проекта

Если теперь повторить команду build, она должна быть успешной, иначе вы что-то сделали неправильно. Вот теперь у нас, наконец, есть заготовка для нашего драйвера, и можно перейти непосредственно к написанию кода. Совершенно ясно, что такая сборочная система никуда не годится, и работать с ней через редактирование текстовых файлов не очень приятно, поэтому каждый из IBV имеет собственное решение по интеграции сборочной системы EDK2 в какую-нибудь современную IDE, к примеру среда AMI Visual eBIOS — это такой обвешенный плагинами Eclipse, а Phoenix и Insyde обвешивают ими же Visual Studio. Есть еще замечательный проект VisualUefi за авторством известного специалиста по компьютерной безопасности Алекса Ионеску, и если вы тоже любите Visual Studio — предлагаю попробовать его, а мы пока продолжим угарать по хардкору, поддерживать дух старой школы и всё такое.

Реагируем на нажатие комбинации клавиш

Для успешной компиляции пока не хватает функций TakeScreenshot и ShowStatus, о которых ниже.

Ищем ФС с доступом на запись, пишем данные в файл

Этому коду больше ничего не нужно, работает как есть.

Ищем графическую консоль и делаем снимок её экрана

Для работы не хватает lodepng_encode32 и уже упоминавшейся выше ShowStatus, продолжим.

Кодируем изображение в формат PNG

Лучший способ писать код — не писать его, поэтому возьмем готовую библиотеку для кодирования и декодирования PNG по имени lodepng. Качаем, кладем рядом с нашим С-файлом, добавляем наш в INF-файл в раздел [Sources.common] строки lodepng.h и lodepng.c, включаем заголовочный файл, иии… ничего не компилируется, т.к lodepng не ожидает, что стандартная библиотека языка C может вот так вот брать и отсутствовать целиком. Ничего, допилим, не впервой. В начало lodepng.h добавим следующее:И закомментируем строку с #include <string.h>, которого у нас тоже нет. Можно, конечно, создать локальный файл с тем же именем, определив там тип size_t, но раз уж принялись менять — будем менять. С lodepng.c немного сложнее, т.к. из стандартной библиотеки, кроме size_t, ему также нужны memset, memcpy, malloc, realloc, free, qsort, а еще он использует вычисления с плавающей точкой. Реализацию qsort можно утащить у Apple, функции работы с памятью сделать обертками над gBS->CopyMem, gBS->SetMem, gBS->AllocatePool и gBS->FreePool соответственно, а для того, чтобы сигнализировать о работе с FPU нужно определить константу CONST INT32 _fltused = 0;, иначе линковщик будет ругаться на её отсутствие. Про комментирование файлов со стандартными #include'ами я уже не говорю — все и так понятно. Аналогичным образом к нормальному бою приводится и qsort.c, не забудьте только добавить его в INF-файл.

Выводим статус

Вот теперь все готово и успешно собирается, если нет — пилите, пока не соберется, либо скачайте мой готовый драйвер с GitHub и сравните с вашим, может быть я какие-то изменения банально забыл описать.

📎📎📎📎📎📎📎📎📎📎