Пишем для UEFI BIOS в Visual Studio. Часть 2 – создаем свой первый драйвер и ускоряем отладку

  • Tutorial

Введение


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

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


Те, кто заинтересовался — добро пожаловать под кат.

Подготовка


Сразу же сделаем свой каталог для наших упражнений в корне edk2 и назовем его EducationPkg. Все проекты будем создавать внутри него. Можно создавать в корне edk2 и каждый отдельный проект, никаких препятствий для этого нет, но примерно на десятом проекте в корне разведется зоопарк из своих проектов и пакетов фреймворка edk2, что приведет, в лучшем случае, к путанице. Итак, создаем каталог C:\FW\edk2\EducationPkg.

Использование UEFI Driver Wizard


Созание набора файлов для проекта edk2 — большая тема, заслуживающая отдельной статьи. Можно про это почитать, если хочется узнать правду прямо сейчас, в документе EDK II Module Writer's Guide, глава 3 Module Developmentздесь или погуглите. Мы же пока будем использовать для создания набора файлов интеловскую утилиту UEFI Driver Wizard, которая лежит в скачанном каталоге в C:\FW\UEFIDriverWizard

Запускаем UEFI Driver Wizard, и первое, что надо сделать – указать рабочую среду Workspace C:\FW\edk2 по команде File → OPEN WORKSPACE. Если этого не сделать вначале, то UEFI Driver Wizard заботливо проведет нас по всем этапам создания, а потом, с извинениями, скажет, что проект драйвера создать не может.

После указания Workspace выбираем File → New UEFI Driver и производим следующие действия:

1. Жмем на кнопку Browse, заходим в каталог C:\FW\edk2\EducationPkg и создаем внутри него свой каталог MyFirstDriver, в котором и будем работать дальше. Имя драйвера примет то же название

2. Выставляем Driver Revision в 1.0. Меньше единицы выставлять не советую — в Wizard ошибка, в результате которой ревизия получается 0.0 после создания файлов проекта.

3. Все остальное оставляем без изменений, так чтобы получилось, как показано на скриншоте



Жмем кнопку Next и переходим к следующему экрану:

Ставим галочки напротив Component Name Protocol и Component Name 2 Protocol, остальное не трогаем:



и после сразу жмем Finish, не трогая другие настройки. Получаем сообщение об успешном создании проекта:


Жмем Ок и закрываем UEFI Driver Wizard, он нам больше не понадобится.

Добавление модуля в список для компиляции


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

Все модули в edk2 входят в т.н. рackages – группы модулей, которые объединены между собой по какому-то общему признаку. Чтобы включить новый модуль в состав какого-либо package, нам необходимо добавить путь к исходникам модуля в файл PackageName.dsc (dsc – сокращение от Description), и этот модуль будет компилироваться в составе Package. UEFI Wizard Driver наш драйвер для компиляции добавить автоматически, увы, не может по объективным причинам (ну откуда ему знать, что за свой новый package вы создали (или с каким из существующих намерены работать?). Поэтому – прописываем ручками.

Просьба понимать, что наш EducationPkg — пока не package, а всего лишь набор проектов, и не более того.

Открываем файл C:\FW\edk2\Nt32Pkg\Nt32Pkg.dsc, ищем в нем строчку

# Add new modules here

и после этой строки прописываем путь к файлу MyFirstDriver.inf нашего драйвера относительно C:\FW\edk2. Должно получиться вот так:

# Add new modules here
EducationPkg/MyFirstDriver/MyFirstDriver.inf
##############################################################################

Очень важное лирическое отступление


Теперь вы уныло думаете, что придется снова настраивать проект в Visual Studio, и совершенно напрасно. Вспомните, мы в первой статье нигде не указали, о каком именно проекте идет речь. Поэтому править ничего не надо, компиляция после добавления в Nt32Pkg нашего inf-файла пойдет и так. Это приводит нас к очень важному следствию: мы можем добавить в уже имеющийся проект Visual Studio, например, NT32, файлы любого проекта из огромного дерева edk2 и все они будут доступны на редактирование и – это главное – постановку контрольных точек, просмотра Watch и всех остальных возможностей, которые предлагает Visual Studio. Собственно, в этом и состоит одно из наиболее интересных преимуществ данного подхода к работе в UEFI с помощью Visual Studio. И при нажатии на F5 перед началом компиляции будет произведено автосохранение измененных исходников, добавленных в проект. Мы, правда, платим за этот подход увеличенным временем компиляции, но дальше разберемся и с этой проблемой.

Давайте проделаем то, о чем только что говорили, для нашего проекта. Щелкаем в Visual Studio правой клавишей на проекте NT32 (не забыв переключить его в проект по умолчанию в Solution), выбираем Add → Existing Item, переходим в наш каталог

C:\FW\edk2\EducationPkg\MyFirstDriver

выделяем мышкой все файлы и жмем на Add, добавляя их в наш проект NT32.

Компиляция и запуск драйвера


Жмем в Visual Studio на F5 или кнопку Debugging, ждем загрузки Shell, в ней вводим:

fs0:
load MyFirstDriver.efi


И затем смотрим на сообщение об успешной загрузке нашего драйвера. Он ничего не делает, что совсем не удивительно – никакую функциональность в него мы еще не добавляли. Нам надо лишь проверить, что мы правильно добавили наш драйвер в среду edk2, и не более того.
Вот то, что мы должны видеть (адрес может быть любой):


Закрываем окно нажатим кнопки Stop Debugging, или Shift + F5 в Visual Studio.

Добавление своего кода


Открываем в Visual Studio файл MyFirstDriver.c из получившегося дерева проекта и добавляем в него наш код. Но вначале немного теории, перед тем, как перейдем к практике.

В UEFI BIOS все взаимодействие с «железом» — в нашем случае, его эмуляцией на виртуальной машине – происходит через протоколы, вот прямо так взять и записать определенный байт в определенный порт — нельзя. В очень упрощенном виде можно рассматривать протокол как имя “класса”, экземпляр которого создается для работы с устройством при его регистрации в системе. Как и обычный экземпляр класса, протокол содержит данные и функции, и вся работа с аппаратурой обеспечивается вызовом соответствующих приватных функций класса.

При работе в UEFI используются таблицы, в которых содержатся указатели на все экземпляры «классов». Этих таблиц несколько, в случае с нашим драйвером мы используем System Table, используя уже объявленный указатель gST. Иерархия этих таблиц довольно проста: есть основная таблица System Table, в которой содержатся (среди прочего) ссылки на таблицы Boot Services и Runtime Services. Впрочем, наверное, будет проще показать код:

gST = *SystemTable; 
gBS = gST->BootServices; 
gRT = gST->RuntimeServices;

По названиям переменных: gST расшифровывается как:
g — означает, что переменная глобальная
ST, как вы уже догадались, означает System Table

Итак, наша задача – послать на устройство вывода (в нашем случае – дисплей) строку I have written my first UEFI driver. Хорошо бы, конечно, при этом в unix-style использовать ту же printf и просто указать разные потоки, но увы – с использованием printf в UEFI есть очень серьезные ограничения, поэтому пока давайте оставим ее в сторонке, до лучших времен.

Вставим вывод нашей строки в функцию EntryPoint() нашего драйвера. Добавляем в области объявления переменных нашу переменную типа CHAR16 (используется двухбайтовая кодировка символов UCS-2) в функции MyFirstDriverDriverEntryPoint() в MyFirstDriver.c:

CHAR16 *MyString = L"I have written my first UEFI driver\r\n";

Лирическое отступление
edk2 не прощает Warning-ов, для него все едино – что Warning, что Error – он посылает на, хм, исправление в обоих случаях, так опции компилятора в edk2 настроены по дефолту. Он, таким образом, неявно говорит «Пионэрам здесь не место». Отключить эту опцию в конфигах, разумеется, можно, но не стоит здесь объяснять, как это сделать – если вы сможете ее отключить, то и убрать источники Warning-ов в коде тоже сможете, и вам это не нужно. Поэтому приводите типы явно и комментируйте неиспользуемые переменные. Иногда, в случае использования сторонних исходников, полное подавление ошибок может оказаться довольно непростой задачей.

После, в самом конце функции MyFirstDriverDriverEntryPoint() вставьте код вывода нашей текстовой переменной на консоль вывода (экран по дефолту, в нашем случае):

gST->ConOut->OutputString(gST->ConOut, MyString);

Жмем на F5, вводим fs0:, load MyFirstDriver.efi, и получаем нашу строку на экране:



Первая программа с нуля написана и работает. Можем себя поздравить.

Разберем сейчас эту нашу строчку:

gST->ConOut->OutputString(gST->ConOut, MyString);

В ней:
gST – указатель на таблицу System Table
ConOutпротокол, или, как мы его условно обозвали, «класс» вывода текста
OutputString – сама функция вывода. Первый параметр в ней, как легко догадаться – this для нашего протокола EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL.

Давайте переформулируем вышесказанное в другом виде, для лучшего понимания. Вот иерархия главной таблицы System Table, получена в окне Watch при останове на breakpoint в Visual Studio. Подсвечена используемая нами функция OutputString. Обратите также внимание на элементы BootServices и RuntimeServices в нижней части таблицы, о чем шла речь ранее:



Ввод текста с клавиатуры и вывод его на экран


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

Сделаем еще один скриншот той же gST, но уже в части ConIn. Выглядит он вот так:


В нашем следующем примере будем запоминать вводимый с клавиатуры пароль, отображая вводимые символы разноцветными звездочками в новогоднем (или наркоманском, как заметил один товарищ) стиле, а после нажатия Enter — выводить пароль в следующей строке. Это всего лишь учебный пример, замещение вводимого пароля звездочками уже реализовано в HII API, равно как и повторный ввод пароля, и проверка совпадения повторного ввода.

Дисклаймер
По фэн-шуй надо бы сделать «упрощающий» указатель *ConOut = gST->ConOut, но в целях обучения – оставим как есть, чтобы не вспоминать, к какой из таблиц обращаемся в данный момент.

Вначале добавим локальные переменные в функции MyFirstDriverDriverEntryPoint() вместо нашей ранее добавленной переменной MyString.

  UINTN					EventIndex;
  UINTN					Index = 0;
  EFI_INPUT_KEY				Keys;
  CHAR16		        	PasswordString[256];

Теперь вводим текст программы, заместив ранее добавленную нами функцию

gST->ConOut->OutputString(gST->ConOut, MyString);

на следующее:

gST->ConOut->ClearScreen(gST->ConOut); // Надеюсь, понятно
gST->ConOut->SetCursorPosition(gST->ConOut, 3, 10); // Ставим курсор в 10-ю строку, 3-ю позицию
gST->ConOut->OutputString(gST->ConOut, L"Enter password up to 255 characters lenght, then press 'Enter' to continue\r\n"); //Выводим нашу строку с вышеуказанной позиции
	do {
                //ждем нажатия на клавишу
		gBS->WaitForEvent (1, &gST->ConIn->WaitForKey, &EventIndex);
                // считываем код нажатия на клавишу 
		gST->ConIn->ReadKeyStroke (gST->ConIn, &Keys);
                //Изменяем цвета шрифта и фона	
		gST->ConOut->SetAttribute(gST->ConOut, Index & 0x7F);  
                // формируем строку для вывода
		PasswordString[Index++] = (Keys.UnicodeChar); 
                // Заменяем текст на звездочки – пароль же, враги рыщут повсюду 
		gST->ConOut->OutputString(gST->ConOut, L"*");
	} //пока не нажали Enter или пока не ввели 255 символов, заполняем текстовый массив вводимыми буквами
           while (!(Keys.UnicodeChar == CHAR_LINEFEED || Keys.UnicodeChar == CHAR_CARRIAGE_RETURN || Index == 254)); 
// Терминация текстовой строки, занимает 1 элемент массива
PasswordString[Index++] = '\0';
// Возвращаем исходные цвета шрифта и фона
gST->ConOut->SetAttribute(gST->ConOut, 0x0F);
// Дальше понятно, уже проходили
gST->ConOut->OutputString(gST->ConOut, L"\r\nEntered password is: \r\n"); 
gST->ConOut->OutputString(gST->ConOut, PasswordString);
gST->ConOut->OutputString(gST->ConOut, L"\r\n"); 

Жмем F5 и устраиваемся в уже ставшей привычной позе на пару минут:



После приглашения Shell, как всегда, вводим fs0: и load MyFirstDriver (или load my и затем два раза жмем на Tab, ибо Shell). Получаем такую вот картинку (разумеется, текст будет тот, что вы ввели сами):



Жмем Shift+F5, чтобы закрыть отладку, после того, как налюбовались.

Дальше в статье будем знакомиться ближе со средой UEFI Shell, PCD и отладочными сообщениями — без этого двигаться дальше нельзя. Но знакомиться будем не абстрактно, а в полезном процессе ускорения и автоматизации запуска драйвера на отладку.

Создание и редактирование загрузочного скрипта UEFI Shell


Теперь, поскольку вы, вероятно, уже перекомпилировали программу не раз и не два, то морально дозрели до того, чтобы сократить это раздражающее время ожидания загрузки UEFI Shell и вбивания каждый раз одних и тех же команд для загрузки нашего драйвера. Начнем с того, что уберем весь ручной ввод текста в Shell написанием соответствующего скрипта. Писать скрипт будем прямо в Shell. С точки зрения здравого смысла, лучше открыть файл скрипта в Far Manager и отредактировать его там, но мало ли какими судьбами в будущем вас выбросит в Shell реальной машины, а не виртуалки с доступом к ее файловой системе с хоста. Поэтому один раз создадим скрипт в редакторе Shell и запишем его, чтобы получить соответствующий навык.

Файл скрипта, исполняемый при старте (подобный autoexec.bat или bashrc) для UEFI Shell, называется startup.nsh, тот самый, что Shell каждый раз предлагает нам пропустить при загрузке. Загрузитесь в UEFI Shell, нажав в Visual Studio клавишу F5, и введите fs0:, чтобы перейти в нашу файловую систему. Теперь из Shell введем команду

edit startup.nsh

и в открывшемся редакторе введем, с переводом строки по Enter:

FS0:
load MyFirstDriver.efi



Дальше жмем F2, затем Enter для записи и затем F3 для выхода из редактора обратно в Shell.

Перекомпилять все заново на этот раз не будем, поскольку мы в программе ничего не меняли. Наберем в Shell команду Exit, и в открывшемся текстовом окне a-la BIOS Setup, а в терминах UEFIMain Form, выберем пункт Continue. После этого нас снова выбросит в Shell, но в этот раз исполнится созданный нами скрипт startup.nsh и автоматом запустится наш драйвер.

Отладочные сообщения


Пока мы можем писать отладочные сообщения на экран, но когда будем работать с формами HII (Human Interface Infrastructure) – такой возможности не представится, экран будет занят формами конфигурирования аппаратуры. Что делать в этом случае?

Несколько отвлеченная тема
В самом начале после получения новой платы с завода не всегда, ох, не всегда бывает доступна дисплейная консоль вывода отладки. По вероятности возможных ошибок подключения дисплей идет сильно впереди последовательного порта, в котором напортачить можно разве что с направлением RX-TX. Поэтому существует также и вывод в последовательный порт, полностью аналогичный выводу на экран, с заменой ConOut на StdErr. Т.е. функция
gST->ConOut->OutputString(gST->ConOut, L”Test string”);

будет выводить “Test string” на дисплей, а функция
gST->StdErr->OutputString(gST->StdErr, L”Test string”);

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

Вывод отладочной информации в окно OVMF


Для вывода информации в окно OVMF есть макрос DEBUG”, который обычно используется следующим образом:

DEBUG((EFI_D_INFO, "Test message is: %s\r\n", Message));

Где первый аргумент – некий аналог линуксового уровня сообщений об ошибках ERROR_LEVEL, а второй, как можно догадаться, указатель на строку, которую надо вывести в OVMF консоль.

Первый аргумент может принимать следующие значения:

#define EFI_D_INIT 0x00000001 // Initialization style messages
#define EFI_D_WARN 0x00000002 // Warnings
#define EFI_D_LOAD 0x00000004 // Load events
#define EFI_D_FS 0x00000008 // EFI File system
#define EFI_D_POOL 0x00000010 // Alloc & Free's
#define EFI_D_PAGE 0x00000020 // Alloc & Free's
#define EFI_D_INFO 0x00000040 // Informational debug messages
#define EFI_D_VARIABLE 0x00000100 // Variable
#define EFI_D_BM 0x00000400 // Boot Manager (BDS)
#define EFI_D_BLKIO 0x00001000 // BlkIo Driver
#define EFI_D_NET 0x00004000 // SNI Driver
#define EFI_D_UNDI 0x00010000 // UNDI Driver
#define EFI_D_LOADFILE 0x00020000 // UNDI Driver
#define EFI_D_EVENT 0x00080000 // Event messages
#define EFI_D_VERBOSE 0x00400000 // Detailed debug messages that may significantly impact boot performance
#define EFI_D_ERROR 0x80000000 // Error


Регулируя уровень появления этих сообщений в логе, мы можем выдержать требуемый баланс между уровнем логгирования и производительностью системы в целом.

Еще немного о макросе DEBUG
1. Его вывод имеет форматирование и работает как форматирование printf. Для отладки очень полезен формат %r, который выводит диагностическую переменную Status не в виде 32-битного числа в HEX, а в виде человекочитаемой строки типа Supported, Invalid Argument и т.п.

2. Этот макрос автоматически отключается при смене Debug на Release, поэтому не спешите его комментировать или обвешивать ifndef-ами – все уже сделано за нас.

3. Двойные скобки там действительно нужны. Попробуйте убрать и посмотреть, что получится.

Небольшой пример


Чтобы проиллюстрировать написанное, добавим в функцию MyFirstDriverDriverEntryPoint(), сразу после объявления переменных, вывод текста из нескольких сообщений в лог с разными уровнями отладки и посмотрим, какие из них выведутся, а какие будут отфильтрованы:

DEBUG((EFI_D_INFO, "Informational debug messages\r\n"));
DEBUG((EFI_D_ERROR, "Error messages\r\n"));
DEBUG((EFI_D_WARN, "Warning messages\r\n"));

Запускаем на отладку и смотрим в окно OVMF:



Видно, что сообщения с уровнем EFI_D_INFO и EFI_D_ERROR попали в лог, а с уровнем EFI_D_WARN – не попало.

Механизм регулирования уровней отладки достаточно прост: как мы видим в списке выше, каждый уровень характеризуется одним битом в 32-битном слове. Чтобы отфильтровать ненужные нам в данный момент сообщения, мы ставим битовую маску на значение уровня, который отсекает биты, не попадающие в данную маску. В нашем случае маска была 0x80000040, поскольку уровень EFI_D_WARN со значением 0x00000002 отфильтровался и не попал в вывод, а уровень EFI_D_INFO со значением 0x00000040, и EFI_D_ERROR с его значением 0x80000000 попали в маску и соответствующие сообщения были выведены.

Сейчас не будем дальше углубляться в рассмотрение реализации механизма настройки уровня вывода отладки, рассмотрим только способы его изменения на практике. Их два, первый из них быстрый, а второй – правильный. Начнем, понятно, с быстрого. Открываем файл c:\FW\Nt32Pkg\Nt32Pkg.dsc и ищем в нем строку, содержащую PcdDebugPrintErrorLevel. Вот она:

gEfiMdePkgTokenSpaceGuid.PcdDebugPrintErrorLevel|0x80000040

Изменяем значение маски на 0x80000042 и запускаем на построение и отладку снова.

Поскольку мы изменили конфигурационный файл, edk2 будет снова долго пересобирать все-все-все бинарники для Nt32Pkg, но этот процесс конечен, и вот мы видим в логе закономерные все три строчки, что и требовалось доказать:



Как сделать быстро, разобрались. Теперь сделаем правильно.

Platform Configuration Database (PCD)


Проблема предыдущего подхода в том, что дерево edk2 и пакет Nt32Pkg у нас единственные, и менять системные настройки ради единственного проекта – прямой путь в преисподнюю, ибо в лучшем случае через неделю вы про это изменение забудете напрочь и будете проклинать исчадье ада под названием edk2, что месяц назад исправно создавало из оттестированных исходников под версионным контролем именно то, что надо, а сейчас выдает нечто совершенно другое. Поэтому в edk2 реализован механизм изменения системных настроек под единственный проект, чтобы локализовать изменения этих настроек только для данного проекта. Называется этот механизм PCDPlatform Configuration Database, и позволяет очень многое. Вообще, хорошим стилем в edk2 считается выносить из исходников в PCD любой параметр, который может быть изменен в будущем. Объем статьи не позволяет остановиться на описании PCD подробнее, поэтому лучше подробности про PCD посмотреть вот здесь в п.3.7.3 или вот здесь. На первое время вполне достаточно ограничиться прочтением файла C:\FW\edk2\MdeModulePkg\Universal\PCD\Dxe\Pcd.inf

С точки зрения практики, конфигурирование при помощи PCD производится вот так: в том же самом файле c:\FW\Nt32Pkg\Nt32Pkg.dsc изменяем уровень показа сообщений в окне OVMF:

EducationPkg/MyFirstDriver/MyFirstDriver.inf {
  <PcdsFixedAtBuild>
    gEfiMdePkgTokenSpaceGuid.PcdDebugPrintErrorLevel|0x80000042
 } 

Не спешите сразу записывать файл. Вначале поправьте обратно 0x80000042 к дефолтному значению 0x80000040 в строчке, которую мы редактировали ранее:

gEfiMdePkgTokenSpaceGuid.PcdDebugPrintErrorLevel|0x80000042

И вот теперь можно записать файл и пересобрать проект. Запускаем на отладку по F5 и видим наши заветные три строчки в отладочной консоли.

Ускоряем отладку дальше


Уберем еще пару раздражающих задержек при запуске на отладку. Очевидно, первый кандидат – это ожидание тех самых 5 сек перед запуском скрипта startup.nsh. Оно, конечно, можно и на пробел нажать, но любой уважающий себя лентяй программист должен автоматизировать ручные операции насколько возможно.

Теперь придется противоречить тому, что было сказано ранее. Проблема состоит в том, что в эти 5 секунд задержки прописываются не через PCD, а вопреки фэн-шуй, напрямую, в исходниках Shell. Поэтому и нам, хотим мы того или нет, придется поступить аналогично: откроем файл C:\FW\edk2\ShellPkg\Application\Shell\Shell.c и поменяем в инициализации значение «5» на «1»

ShellInfoObject.ShellInitSettings.Delay = 1;//5;

Можно было бы и в 0 выставить, но мало ли… Забудем поменять на реальной аппаратной системе, а возможности перекомпилировать потом не будет.

Жмем F5 и радуемся 1 сек. задержки вместо 5.

Если кто-то знает, как выставить это значение задержки правильно, через PCD и без правки исходников Shell — дайте знать в личку или в комменты, я поправлю.

Еще ускоряемся


Вспоминаем про строку прогресса, а на самом деле – просто таймера ожидания выбора опции загрузки. Вот так это выглядит на основном экране:



А в окне запуска OVMF это смотрится несколько иначе:



Как говорил Винни-Пух, «Это ж-ж-ж – неспроста!» Надо найти источник и также уменьшить до 1 сек.

Запускаем поиск по всем файлам с расширением *.c строки Zzzzz, находим эту строку в исходнике C:\FW\MdeModulePkg\Universal\BdsDxe\BdsEntry.c и видим там вот такой блок кода:

  DEBUG ((EFI_D_INFO, "[Bds]BdsWait ...Zzzzzzzzzzzz...\n"));
  TimeoutRemain = PcdGet16 (PcdPlatformBootTimeOut);
  while (TimeoutRemain != 0) {
    DEBUG ((EFI_D_INFO, "[Bds]BdsWait(%d)..Zzzz...\n", (UINTN) TimeoutRemain));
    PlatformBootManagerWaitCallback (TimeoutRemain);

Соответственно, понятно, что переменная TimeoutRemain читается из конфигурационной базы PCD, в параметре PcdPlatformBootTimeOut. Ок, открываем наш конфигурационный файл c:\FW\Nt32Pkg\Nt32Pkg.dsc, ищем там строку с PcdPlatformBootTimeOut:

  gEfiMdePkgTokenSpaceGuid.PcdPlatformBootTimeOut|L"Timeout"|gEfiGlobalVariableGuid|0x0|10

Здесь уже вариант, как ранее, с конфигурацией PcdDebugPrintErrorLevel исключительно для нашего драйвера, не получится – в данном случае задержка будет выполняться задолго до того, как наш модуль MyFirstDriver будет загружен стартовым скриптом startup.nsh, поэтому придется менять глобально, хотим мы этого или нет. В данном случае — хотим, потому что эта задержка в процессе разработки драйверов обычно ни к чему. Меняем 10 на 1 в нашем конфигурационном файле, жмем F5 и радуемся быстрой загрузке. На моей машине это занимает теперь 23 секунды.

Еще тюнинг, на этот раз — интерфейса


Убираем второй дисплей, который нам незачем пока что, а раздражать уже начал. Правим строки в нашем любимом конфигурационном файле c:\FW\Nt32Pkg\Nt32Pkg.dsc, открыв его и убрав !My EDK II 2 и !UGA Window 2 в двух строчках, чтобы получилось:

gEfiNt32PkgTokenSpaceGuid.PcdWinNtGop|L"UGA Window 1"|VOID|52

и

gEfiNt32PkgTokenSpaceGuid.PcdWinNtUga|L"UGA Window 1"|VOID|52

Можно, разумеется, и саму надпись в заголовке окна UGA Window 1 поменять на что-то духовно более близкое вам лично, но это уже на ваше усмотрение.

На будущее


Есть еще один большой резерв сокращения времени компиляции и запуска проекта (в разы) – компилировать не весь edk2, а только наш модуль. Но об этом, как говорится – в следующей серии. Можете пока попробовать сделать это сами, решение элементарное, как убедитесь чуть позже.

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

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

  • +12
  • 5,1k
  • 7
Поделиться публикацией
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 7
  • +3

    Отлично, замечу только, что с gST->ConOut и gST->ConIn напрямую стоит работать только в процессе начального обучения, потому что цепочки из X->Y->Z(X->Y, T) сильно замусоревают код и его становится неприятно читать, а читаемость — это, на мой взгляд, вторая по важности характериска кода прошивки после работоспособности.
    В реальных приложениях и драйверах обычно используют библиотеку PrintLib, которая превращает кошмарный gST->ConOut->OutputString(gST->ConOut, L"String") во вполне безобидные Print(L"String") или AsciiPrint("String").
    Скорее всего, это все будет в следующих частях, ждем с нетерпением.

    • +3
      Это специально, чтобы с самого начала уяснили, откуда у консольного вывода ноги растут. Этой же цели и скриншоты с watch для gST служат.
      И, в целях обучения же, не стал пока с Status возиться, который по фен-шуй полагается анализировать для OutputString() — все равно там только EFI_SUCCESS будет, нет смысла показывать новичкам, что он всегда такой, он иногда очень даже полезен :)
      • 0

        Спасибо, все очень доступно! Планируете описать процесс деплоя драйвера в виртуалку более подробно? Интересуюсь с целью встраивания данного подхода в CI.

        • 0
          Что означает «процесс деплоя драйвера в виртуалку»?

          — встроить драйвер в NVRAM, по аналогии с Линуксом — в ядро, чтобы не грузить каждый раз при помощи startup.nsh?

          Это просто — открываем C:\fw\edk2\Nt32PkgNt32Pkg.fdf и прописываем внизу списка модулей, следующих за строкой "# DXE Phase modules", ставшую нам знакомой строчку

          EducationPkg/MyFirstDriver/MyFirstDriver.inf

          После чего надо закомментировать загрузку нашего драйвера в startup.nsh и все, после перекомпиляции MyFirstDriver будет сидеть в NVRAM.
          Я умышленно не стал этого делать, поскольку драйвер явно не из тех, что требуется иметь всегда :) Он к тому же останавливает загрузку, требуя ручного ввода.

          — показать реализацию функций драйвера Loadimage() — StartImage — UnloadImage() и Supported() — Start() — Stop() — Unload() в соответствии с UEFI Driver Model?

          В руководствах (Zimmer, Charlstrom) это объяснено довольно запутанно, точнее, нормально по отдельности, но слабо воспринимается в комплексе. У меня есть готовая статья на эту тему, но там именно объяснения, как это все работает, своего кода немного. Я боюсь, никто не будет ее читать, народ любит сам попрограммировать — а время на подготовку статьи немаленькое, бесполезный шлак выпускать не хочется, надо все из статьи досконально в VirtualBox на «чистом» дереве edk2 проверять, постоянно думая, что из очевидного (для меня) надо описывать в тексте, и описывать подробно.

          И что такое Cl в данном контексте?
          • 0

            Имелось в виду, где происходит монтирование файловой системы виртуалки для загрузки собранного драйвера, какие средства под Windows для этого используются, как собственно запускается образ ВМ (qemu?) Возможно ли это использовать в отрыве от edk2.

            • 0
              Если нужно что-то вроде cookbook для qemu под Windows, то вот оно.

              А глубже — надо документацию читать, на их сайте, или перевод

              edk2 тут вообще не нужен, он использует порт qemu под названием ovmf, с ограничениями, которые ни к чему без необходимости
        • +1
          Замечательная статья, Николай, спасибо. В отличии от первой, лично для себя отметил что-то новое. Приятно, что «UEFI-энтузиастов» становится больше.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.