Что может быть лучше, чем писать статью в прекрасную весеннюю погоду? Правильно, ничего.
Сегодня поговорим про разработку UEFI-приложений под архитектуру x64. Разработка тех же DXE-драйверов - направление узкое и специфическое, поэтому увлекаются им немногие. Но такие люди есть. Обычно это те, кто копает в сторону создания собственных ОС или исследует тему буткитов, ну либо пишет их, действительно, что может быть веселее разработки буткитов?
В этой статье мы разберем основы и поймем, как войти в этот закрытый клуб низкоуровневой разработки.
тности это люди которые интересруются разработкой буткитов, разработкой ОС иммногое другое. Начинаем, сегодня напишем два простых приложения для демонстрации.
Для разработки я выбрал свой любимый ассемблер FASM. Чтобы собирать UEFI-приложения, мы будем использовать формат format pe64 dll efi. А чтобы жизнь не превращалась в бесконечный поиск офсетов и смещений внутри системных таблиц, я подготовил отдельный INC-файл. В нем уже прописаны все нужные структуры, так что нам не придется держать их в голове.
Статью я бы хотел начать с базы, что вообще такое UEFI?
UEFI - это современная прошивка, которая поставляется вместе с материнской платой и служит своеобразным мостом между железом и операционной системой.
По сути, это полноценная мини-ОС со своим API, драйверами, UEFI-драйверы принято называть DXE, если расшифровывать - Driver Execution Environmen, по сути, DXE-драйверы - это программные модули, которые остаются в памяти и предоставляют сервисы для других приложений или самой ОС.
UEFI пришла на смену старому доброму Legacy BIOS, который верой и правдой служил индустрии десятилетиями, но в какой-то момент просто перестал вывозить аппетиты современного софта и объемы новых накопителей.
Главная проблема Legacy BIOS заключалась в его 16-битном режиме работы и жестком лимите адресного пространства в один мегабайт, но в те времена, когда компьютеры занимали целые комнаты, этого хватало с головой, но для современных реалий это стало бутылочным горлышком.
BIOS инициализировал железо по старинке, полагаясь на прерывания и фиксированные адреса, что делало процесс загрузки долгим и неповоротливым. К тому же, ограничение в 2.2 терабайта на размер диска из-за разметки MBR стало последним гвоздем в крышку гроба этой технологии.
Переход на UEFI в середине 2000-х (спасибо Intel и их инициативе Itanium) кардинально изменил правила игры, программисты, да и обычные люди получили полноценный 64-битный режим работы еще до того, как загрузится ядро Windows или Linux. Это позволило использовать нормальные драйверы, графические интерфейсы с поддержкой мышки и, что самое важное, разметку GPT. Теперь мы можем спокойно работать с дисками практически любого объема и не переживать, что система не увидит лишние терабайты.
Исторически UEFI развивалась как проект EFI (Extensible Firmware Interface), целью которого было создание гибкой и расширяемой среды.
Под UEFI можно писать свои драйверы (те самые DXE, о которых я говорил выше), создавать приложения и даже встраивать криптографические протоколы вроде SecureBoot/TPM для защиты цепочки загрузки, обход которых интересен разработчикам буткитов, условно через BYOVD, но - это уже другая тема, к данной статье не имеет никакого отношения.
Сегодня Legacy BIOS окончательно уходит в прошлое.
Что-ж, пора бы и на практике что-то попробовать.
Как я уже писал чуть выше, под программирование UEFI есть собственный формат файла. В нашем случае pe64 dll efi, то есть 64 битное UEFI-приложение, кто-то может задаться вопросом почему приписка dll, дело в том, что в FASM указывает компилятору, что наш бинарник должен иметь таблицу перебазирования, релокации, потому что прошивка может загрузить код по любому адресу в оперативной памяти, который будет свободен на момент старта.
Без этой таблицы внутренние адреса переходов в коде просто поплывут, и система намертво зависнет при попытке выполнить первую же инструкцию
И кстати, важно, PE был выбран спецификацией EFI как стандарт, чтобы не изобретать велосипед и использовать уже готовые инструменты линковки.
Давайте создадим простенькое приложение и скомпилируем его, к примеру: вывод Hello World.
Начнем разбор. Первое, что я бы хотел учесть - это то, что при разработке UEFI-приложений действуют правила похожие на Windows x64 ABI, первые четыре аргумента любой функции всегда передаются через регистры RCX, RDX, R8 и R9. Все, что не влезло, отправляется в стек.
Если же начать разбор куда, то все просто.
Можно взять этот адрес и сохранить его в переменную, потому что это единственный способ достучаться до функций системы, ввода с клавиатуры или вывода на экран. То есть без этой структуры никак.
Если сразу не сделать mov [SystemTable], rdx, то при первой же попытке что-то напечатать через тот же OutputString который используется для вывода на экран или выделить буфер возникнет проблема, ведь без этого указателя программа остается в полной изоляции от API прошивки. По сути это главная точка входа во все сервисы, поэтому сохранение значения из rdx в секцию .data всегда идет самой первой инструкцией, чтобы потом спокойно вызывать нужные функции через смещения.
Также важно учиывать, что в UEFI-приложениях строка создается через тип данных DU вместо DB, это происходит потому, что прошивка работает исключительно с кодировкой UTF-16.
В обычном ассемблере мы привыкли использовать db для записи каждого символа в один байт, но в UEFI каждый символ занимает строго два байта, если попробовать вывести обычную строку через db, функция
Использование директивы du в FASM заставляет компилятор автоматически выделять по 16 бит на каждый знак, что полностью соответствует спецификации.
Теперь касаемо ConOut, это указатель внутри системной таблице SystemTable, который ведет на интерфейс простого текстового вывода, через него вызывается функция
Структура функции выглядит так:
С-подобный:
Она находится внутри нашего EFI.inc, структура
служит для описания всех функций управления текстовым выводом на экран, в статье мы будем ею пользоваться для вызова
Это по сути набор указателей на системные подпрограммы, которые умеют очищать экран, менять цвет текста, перемещать курсор и печатать строки и многое другое.
Непосредственно в программе идет обращение к этой структуре, она принимает к себе указатель на переменную _hello которая хранит в себе строку "Hello from UEFI! This is forum-duty.free!", после чего значение оттуда передается этой функции и она вызывается через call rax.
Если же мы скомпилируем программу, то у нас появится файл [название.EFI]:
Как видно на скрине, компиляция прошла успешно, непосредственно для самой эмуляции файла и запуска внутри UEFI, нам пригодится эмулятор QEMU.
После скачивания эмулятора, мы можем приступать к следующему этапу, нам необходимо пройти несколько этапов:
1. Создаем отдельную папку на рабочем столе, назовите ее USB;
2. Внутри этой папки создайте папку EFI;
2. Внутри папки EFI создайте папку BOOT;
3. Переносите туда .EFI файл.
Важно, не забудьте переименовать файл в BOOTX64.efi, это нужно для того, чтобы прошивка смогла автоматически найти и запустить приложение при старте системы.
В спецификации UEFI прописан стандартный путь для поиска загрузчика на съемных носителях, файл должен лежать в папке \EFI\BOOT\ под строго определенным именем. Для архитектуры x64 это именно BOOTX64.efi, и если назвать файл иначе, то встроенный менеджер загрузки просто его не увидит.
После всех манипуцяций ваша задача зайти в корневой файл с QEMU, нажать по пустому месту и выбрать "Открыть в терминале"
После чего ввести команду:
После чего жмем Enter и видим как у нас запускается эмулятор и видим:
Отлично, мы вывели текст на экран, но не кажется ли вам, что текст мрачный и почти незаметный?
Давайте украсим его, сделаем красивее, покрасим в зеленый! Сделать это можно через функцию
Функция позволяет задать цвет текста и фона для вывода в консоль, чтобы приложение не выглядело как обычное черно-белое окно.
Можно взять и передать в регистр rdx байтовое значение, где младшие четыре бита отвечают за цвет текста, а следующие четыре - за цвет фона.
Передадим значение 1Eh, и посмотрим что из этого выйдет.
Важно понимать, что непосредственно красите вы текст еще до того, как его вывели, потому что функция
Можно представить это как выбор фломастера перед тем, как начать писать на бумаге, если сначала вызвать SetAttribute с нужным цветом, то весь последующий текст, который пойдет через
Скомпилируем нашу программу и посмотрим, что выйдет:
Как видим программа отработало на отлично, но все равно, не кажется ли вам, что сложно заметить строку? Давай же очистим к чертям все кроме нашей строки!
Сделать это можно через функцию
Функцию очистки всего кроме нашей строки нужно вызывать еще до настройки цвета и вывода чего либо на экран, потому что в консоли может остаться мусор от предыдущих этапов загрузки или самой прошивки.
В ассемблере это выглядит как еще один простой вызов - кладем указатель на
Как видим строка стала виднее, но ,много хлама.
За строкой идет поток логов от загрузчика прошивки, это происходит потому, что после вывода текста программа сразу завершает работу через ret, прошивка получает управление обратно и продолжает свою работу, вываливая в консоль техническую информацию о поиске других загрузочных устройств.
Чтобы экран остался чистым и на нем была только твоя надпись, нужно добавить бесконечный цикл или ожидание нажатия клавиши в конце кода.
Давайте реализуем это! И не только это.
Код немного видоизменился, я добавил функцию Reset и цикл обработки нажатий клавиши.
Сначала вызывается функция Reset для протокола ввода, это нужно сделать, чтобы очистить буфер клавиатуры от случайных нажатий, которые могли произойти еще до старта программы.
Затем идет основной цикл _wait_key. В нем вызывается функция
С-подобно:
Если в момент вызова мы ничего не нажали, вернется ошибка
Программа будет висеть в этом цикле до тех пор, пока в rax не придет заветный ноль, он же статус
P.S также я поменял цвет немного.
Давайте же скомпилируем программу и проверим работоспособность:
Как видим программа работает, если же мы нажмем любую кнопку сгенерируется EFI_SUCESS нам выведется отладочная информация и все.
В заключение хочется сказать, что разработка под UEFI это реально драйвовое занятие, здесь можно писать как PoC буткиты, так и разные сканеры прошивок, инжекторы или вообще свои мини-ОС, надеюсь, этот материал принесет хоть какую-то пользу и поможет быстрее вкатиться в тему, буду благодарен за фидбек, реакции и оценки под постом. Удачи всем!
Сегодня поговорим про разработку UEFI-приложений под архитектуру x64. Разработка тех же DXE-драйверов - направление узкое и специфическое, поэтому увлекаются им немногие. Но такие люди есть. Обычно это те, кто копает в сторону создания собственных ОС или исследует тему буткитов, ну либо пишет их, действительно, что может быть веселее разработки буткитов?
В этой статье мы разберем основы и поймем, как войти в этот закрытый клуб низкоуровневой разработки.
тности это люди которые интересруются разработкой буткитов, разработкой ОС иммногое другое. Начинаем, сегодня напишем два простых приложения для демонстрации.
Для разработки я выбрал свой любимый ассемблер FASM. Чтобы собирать UEFI-приложения, мы будем использовать формат format pe64 dll efi. А чтобы жизнь не превращалась в бесконечный поиск офсетов и смещений внутри системных таблиц, я подготовил отдельный INC-файл. В нем уже прописаны все нужные структуры, так что нам не придется держать их в голове.
Спойлер: EFI.inc
C:
// efi.inc
struc int8 {
. db ?
}
struc int16 {
align 2
. dw ?
}
struc int32 {
align 4
. dd ?
}
struc int64 {
align 8
. dq ?
}
struc intn {
align 8
. dq ?
}
struc dptr {
align 8
. dq ?
}
struc POINTER {
align 8
. dq ?
}
;symbols
EFIERR = 0x8000000000000000
EFI_SUCCESS = 0
EFI_LOAD_ERROR = EFIERR or 1
EFI_INVALID_PARAMETER = EFIERR or 2
EFI_UNSUPPORTED = EFIERR or 3
EFI_BAD_BUFFER_SIZE = EFIERR or 4
EFI_BUFFER_TOO_SMALL = EFIERR or 5
EFI_NOT_READY = EFIERR or 6
EFI_DEVICE_ERROR = EFIERR or 7
EFI_WRITE_PROTECTED = EFIERR or 8
EFI_OUT_OF_RESOURCES = EFIERR or 9
EFI_VOLUME_CORRUPTED = EFIERR or 10
EFI_VOLUME_FULL = EFIERR or 11
EFI_NO_MEDIA = EFIERR or 12
EFI_MEDIA_CHANGED = EFIERR or 13
EFI_NOT_FOUND = EFIERR or 14
EFI_ACCESS_DENIED = EFIERR or 15
EFI_NO_RESPONSE = EFIERR or 16
EFI_NO_MAPPING = EFIERR or 17
EFI_TIMEOUT = EFIERR or 18
EFI_NOT_STARTED = EFIERR or 19
EFI_ALREADY_STARTED = EFIERR or 20
EFI_ABORTED = EFIERR or 21
EFI_ICMP_ERROR = EFIERR or 22
EFI_TFTP_ERROR = EFIERR or 23
EFI_PROTOCOL_ERROR = EFIERR or 24
EfiResetCold = 0
EfiResetWarm = 1
EfiResetShutdown = 2
;helper macro for definition of relative structure member offsets
macro struct name
{
virtual at 0
name name
end virtual
}
;structures
struc SIMPLE_INPUT_INTERFACE {
.Reset dptr
.ReadKeyStroke dptr
.WaitForKey dptr
}
struct SIMPLE_INPUT_INTERFACE
struc EFI_TABLE_HEADER {
.Signature int64
.Revision int32
.HeaderSize int32
.CRC32 int32
.Reserved int32
}
struct EFI_TABLE_HEADER
struc EFI_SYSTEM_TABLE {
.Hdr EFI_TABLE_HEADER
.FirmwareVendor dptr
.FirmwareRevision int32
.ConsoleInHandle dptr
.ConIn dptr
.ConsoleOutHandle dptr
.ConOut dptr
.StandardErrorHandle dptr
.StdErr dptr
.RuntimeServices dptr
.BootServices dptr
.NumberOfTableEntries intn
.ConfigurationTable dptr
}
struct EFI_SYSTEM_TABLE
struc SIMPLE_TEXT_OUTPUT_INTERFACE {
.Reset dptr
.OutputString dptr
.TestString dptr
.QueryMode dptr
.SetMode dptr
.SetAttribute dptr
.ClearScreen dptr
.SetCursorPosition dptr
.EnableCursor dptr
.Mode dptr
}
struct SIMPLE_TEXT_OUTPUT_INTERFACE
struc EFI_BOOT_SERVICES {
.Hdr EFI_TABLE_HEADER
.RaiseTPL POINTER
.RestoreTPL POINTER
.AllocatePages POINTER
.FreePages POINTER
.GetMemoryMap POINTER
.AllocatePool POINTER
.FreePool POINTER
.CreateEvent POINTER
.SetTimer POINTER
.WaitForEvent POINTER
.SignalEvent POINTER
.CloseEvent POINTER
.CheckEvent POINTER
.InstallProtocolInterface POINTER
.ReinstallProtocolInterface POINTER
.UninstallProtocolInterface POINTER
.HandleProtocol POINTER
.Reserved POINTER
.RegisterProtocolNotify POINTER
.LocateHandle POINTER
.LocateDevicePath POINTER
.InstallConfigurationTable POINTER
.LoadImage POINTER
.StartImage POINTER
.Exit POINTER
.UnloadImage POINTER
.ExitBootServices POINTER
.GetNextMonotonicCount POINTER
.Stall POINTER
.SetWatchdogTimer POINTER
.ConnectController POINTER
.DisconnectController POINTER
.OpenProtocol POINTER
.CloseProtocol POINTER
.OpenProtocolInformation POINTER
.ProtocolsPerHandle POINTER
.LocateHandleBuffer POINTER
.LocateProtocol POINTER
.InstallMultipleProtocolInterfaces POINTER
.UninstallMultipleProtocolInterfaces POINTER
.CalculateCrc32 POINTER
.CopyMem POINTER
.SetMem POINTER
.CreateEventEx POINTER
}
struct EFI_BOOT_SERVICES
struc EFI_RUNTIME_SERVICES {
.Hdr EFI_TABLE_HEADER
.GetTime POINTER
.SetTime POINTER
.GetWakeupTime POINTER
.SetWakeupTime POINTER
.SetVirtualAddressMap POINTER
.ConvertPointer POINTER
.GetVariable POINTER
.GetNextVariableName POINTER
.SetVariable POINTER
.GetNextHighMonotonicCount POINTER
.ResetSystem POINTER
.UpdateCapsule POINTER
.QueryCapsuleCapabilities POINTER
.QueryVariableInfo POINTER
}
struct EFI_RUNTIME_SERVICES
struc EFI_GRAPHICS_OUTPUT_PROTOCOL {
.QueryMode POINTER
.SetMode POINTER
.Blt POINTER
.Mode POINTER
}
struct EFI_GRAPHICS_OUTPUT_PROTOCOL
struc EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE {
.MaxMode int32
.Mode int32
.Info POINTER
.SizeOfInfo intn
.FrameBufferBase int64
.FrameBufferSize intn
}
struct EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE
Статью я бы хотел начать с базы, что вообще такое UEFI?
UEFI - это современная прошивка, которая поставляется вместе с материнской платой и служит своеобразным мостом между железом и операционной системой.
По сути, это полноценная мини-ОС со своим API, драйверами, UEFI-драйверы принято называть DXE, если расшифровывать - Driver Execution Environmen, по сути, DXE-драйверы - это программные модули, которые остаются в памяти и предоставляют сервисы для других приложений или самой ОС.
UEFI пришла на смену старому доброму Legacy BIOS, который верой и правдой служил индустрии десятилетиями, но в какой-то момент просто перестал вывозить аппетиты современного софта и объемы новых накопителей.
Главная проблема Legacy BIOS заключалась в его 16-битном режиме работы и жестком лимите адресного пространства в один мегабайт, но в те времена, когда компьютеры занимали целые комнаты, этого хватало с головой, но для современных реалий это стало бутылочным горлышком.
BIOS инициализировал железо по старинке, полагаясь на прерывания и фиксированные адреса, что делало процесс загрузки долгим и неповоротливым. К тому же, ограничение в 2.2 терабайта на размер диска из-за разметки MBR стало последним гвоздем в крышку гроба этой технологии.
Переход на UEFI в середине 2000-х (спасибо Intel и их инициативе Itanium) кардинально изменил правила игры, программисты, да и обычные люди получили полноценный 64-битный режим работы еще до того, как загрузится ядро Windows или Linux. Это позволило использовать нормальные драйверы, графические интерфейсы с поддержкой мышки и, что самое важное, разметку GPT. Теперь мы можем спокойно работать с дисками практически любого объема и не переживать, что система не увидит лишние терабайты.
Исторически UEFI развивалась как проект EFI (Extensible Firmware Interface), целью которого было создание гибкой и расширяемой среды.
Под UEFI можно писать свои драйверы (те самые DXE, о которых я говорил выше), создавать приложения и даже встраивать криптографические протоколы вроде SecureBoot/TPM для защиты цепочки загрузки, обход которых интересен разработчикам буткитов, условно через BYOVD, но - это уже другая тема, к данной статье не имеет никакого отношения.
Сегодня Legacy BIOS окончательно уходит в прошлое.
Что-ж, пора бы и на практике что-то попробовать.
C:
format pe64 dll efi ;// непосредственно сам формат
entry main ;// точка входа(можно назвать любым)
include 'EFI.inc'
section '.text' code executable readable
main:
section '.data' data readable writeable
Без этой таблицы внутренние адреса переходов в коде просто поплывут, и система намертво зависнет при попытке выполнить первую же инструкцию
И кстати, важно, PE был выбран спецификацией EFI как стандарт, чтобы не изобретать велосипед и использовать уже готовые инструменты линковки.
Давайте создадим простенькое приложение и скомпилируем его, к примеру: вывод Hello World.
C:
format pe64 dll efi
entry main
include 'EFI.inc'
section '.text' code executable readable
main:
mov [SystemTable], rdx
sub rsp, 48h ;// выравнивание
mov rax, [SystemTable]
mov rcx, [rax + EFI_SYSTEM_TABLE.ConOut]
lea rdx, [_hello]
mov rax, [rcx + SIMPLE_TEXT_OUTPUT_INTERFACE.OutputString]
call rax
add rsp, 48h
mov rax, EFI_SUCCESS
ret
section '.data' data readable writeable
SystemTable dq ?
_hello du 'Hello from UEFI! This is forum-duty.free!', 13, 10, 0
Если же начать разбор куда, то все просто.
SystemTable всегда сохраняется в регистр rdx, SystemTable - это основной пульт управления всей средой UEFI, при старте приложения прошивка всегда передает указатель на эту структуру.Можно взять этот адрес и сохранить его в переменную, потому что это единственный способ достучаться до функций системы, ввода с клавиатуры или вывода на экран. То есть без этой структуры никак.
Если сразу не сделать mov [SystemTable], rdx, то при первой же попытке что-то напечатать через тот же OutputString который используется для вывода на экран или выделить буфер возникнет проблема, ведь без этого указателя программа остается в полной изоляции от API прошивки. По сути это главная точка входа во все сервисы, поэтому сохранение значения из rdx в секцию .data всегда идет самой первой инструкцией, чтобы потом спокойно вызывать нужные функции через смещения.
Также важно учиывать, что в UEFI-приложениях строка создается через тип данных DU вместо DB, это происходит потому, что прошивка работает исключительно с кодировкой UTF-16.
В обычном ассемблере мы привыкли использовать db для записи каждого символа в один байт, но в UEFI каждый символ занимает строго два байта, если попробовать вывести обычную строку через db, функция
OutputString просто не поймет, где заканчивается буква, и на экране вместо текста появится непонятная каша из иероглифов или пустых мест.Использование директивы du в FASM заставляет компилятор автоматически выделять по 16 бит на каждый знак, что полностью соответствует спецификации.
Теперь касаемо ConOut, это указатель внутри системной таблице SystemTable, который ведет на интерфейс простого текстового вывода, через него вызывается функция
OutputString которая как я уже сказал, выводит на экран текст.Структура функции выглядит так:
Не удалось прогрузить медиа
С-подобный:
C:
typedef struct _SIMPLE_TEXT_OUTPUT_INTERFACE {
unsigned long long Reset;
unsigned long long (*OutputString)(struct _SIMPLE_TEXT_OUTPUT_INTERFACE *This, const unsigned short *String);
unsigned long long TestString;
} SIMPLE_TEXT_OUTPUT_INTERFACE;
typedef struct {
char Header[24];
void *FirmwareVendor;
unsigned int FirmwareRevision;
void *ConsoleInHandle;
void *ConIn;
void *ConsoleOutHandle;
SIMPLE_TEXT_OUTPUT_INTERFACE *ConOut;
} EFI_SYSTEM_TABLE;
SIMPLE_TEXT_OUTPUT_INTERFACE служит для описания всех функций управления текстовым выводом на экран, в статье мы будем ею пользоваться для вызова
OutputString.Это по сути набор указателей на системные подпрограммы, которые умеют очищать экран, менять цвет текста, перемещать курсор и печатать строки и многое другое.
C:
mov rcx, [rax + EFI_SYSTEM_TABLE.ConOut]
lea rdx, [_hello]
mov rax, [rcx + SIMPLE_TEXT_OUTPUT_INTERFACE.OutputString]
call rax
Если же мы скомпилируем программу, то у нас появится файл [название.EFI]:
Не удалось прогрузить медиа
Как видно на скрине, компиляция прошла успешно, непосредственно для самой эмуляции файла и запуска внутри UEFI, нам пригодится эмулятор QEMU.
После скачивания эмулятора, мы можем приступать к следующему этапу, нам необходимо пройти несколько этапов:
1. Создаем отдельную папку на рабочем столе, назовите ее USB;
2. Внутри этой папки создайте папку EFI;
2. Внутри папки EFI создайте папку BOOT;
3. Переносите туда .EFI файл.
Не удалось прогрузить медиа
Важно, не забудьте переименовать файл в BOOTX64.efi, это нужно для того, чтобы прошивка смогла автоматически найти и запустить приложение при старте системы.
В спецификации UEFI прописан стандартный путь для поиска загрузчика на съемных носителях, файл должен лежать в папке \EFI\BOOT\ под строго определенным именем. Для архитектуры x64 это именно BOOTX64.efi, и если назвать файл иначе, то встроенный менеджер загрузки просто его не увидит.
После всех манипуцяций ваша задача зайти в корневой файл с QEMU, нажать по пустому месту и выбрать "Открыть в терминале"
После чего ввести команду:
Code:
.\qemu-system-x86_64.exe -L . -drive if=pflash,format=raw,readonly=on,file=.\share\edk2-x86_64-code.fd -drive format=raw,file=fat:rw:C:\ВАШ ПУТЬ ДО ФАЙЛА USB
Не удалось прогрузить медиа
Отлично, мы вывели текст на экран, но не кажется ли вам, что текст мрачный и почти незаметный?
Давайте украсим его, сделаем красивее, покрасим в зеленый! Сделать это можно через функцию
SetAttribute которая находится в структуре SIMPLE_TEXT_OUTPUT_INTERFACE, которую мы уже обсуждали:)Функция позволяет задать цвет текста и фона для вывода в консоль, чтобы приложение не выглядело как обычное черно-белое окно.
Можно взять и передать в регистр rdx байтовое значение, где младшие четыре бита отвечают за цвет текста, а следующие четыре - за цвет фона.
Передадим значение 1Eh, и посмотрим что из этого выйдет.
Code:
mov rax, [SystemTable]
mov rcx, [rax + EFI_SYSTEM_TABLE.ConOut]
mov rdx, 1Eh
mov rax, [rcx + SIMPLE_TEXT_OUTPUT_INTERFACE.SetAttribute]
call rax
mov rax, [SystemTable]
mov rcx, [rax + EFI_SYSTEM_TABLE.ConOut]
lea rdx, [_hello]
mov rax, [rcx + SIMPLE_TEXT_OUTPUT_INTERFACE.OutputString]
call rax
SetAttribute просто меняет текущее состояние консоли.Можно представить это как выбор фломастера перед тем, как начать писать на бумаге, если сначала вызвать SetAttribute с нужным цветом, то весь последующий текст, который пойдет через
OutputString, будет окрашен именно в этот оттенок.Скомпилируем нашу программу и посмотрим, что выйдет:
Не удалось прогрузить медиа
Как видим программа отработало на отлично, но все равно, не кажется ли вам, что сложно заметить строку? Давай же очистим к чертям все кроме нашей строки!
Сделать это можно через функцию
ClearScreen из нашей любимой структуры SIMPLE_TEXT_OUTPUT_INTERFACE.Функцию очистки всего кроме нашей строки нужно вызывать еще до настройки цвета и вывода чего либо на экран, потому что в консоли может остаться мусор от предыдущих этапов загрузки или самой прошивки.
В ассемблере это выглядит как еще один простой вызов - кладем указатель на
ConOut в rcx и вызываем ClearScreen по нужному смещению, после чего экран станет чище.Не удалось прогрузить медиа
Как видим строка стала виднее, но ,много хлама.
За строкой идет поток логов от загрузчика прошивки, это происходит потому, что после вывода текста программа сразу завершает работу через ret, прошивка получает управление обратно и продолжает свою работу, вываливая в консоль техническую информацию о поиске других загрузочных устройств.
Чтобы экран остался чистым и на нем была только твоя надпись, нужно добавить бесконечный цикл или ожидание нажатия клавиши в конце кода.
Давайте реализуем это! И не только это.
Code:
mov [SystemTable], rdx
sub rsp, 48h
mov rax, [SystemTable]
mov rcx, [rax + EFI_SYSTEM_TABLE.ConOut]
mov rax, [rcx + SIMPLE_TEXT_OUTPUT_INTERFACE.ClearScreen]
call rax
mov rax, [SystemTable]
mov rcx, [rax + EFI_SYSTEM_TABLE.ConOut]
mov rdx, 1Eh
mov rax, [rcx + SIMPLE_TEXT_OUTPUT_INTERFACE.SetAttribute]
call rax
mov rax, [SystemTable]
mov rcx, [rax + EFI_SYSTEM_TABLE.ConOut]
lea rdx, [_hello]
mov rax, [rcx + SIMPLE_TEXT_OUTPUT_INTERFACE.OutputString]
call rax
mov rax, [SystemTable]
mov rcx, [rax + EFI_SYSTEM_TABLE.ConIn]
xor rdx, rdx
mov rax, [rcx + SIMPLE_INPUT_INTERFACE.Reset]
call rax
_wait_key:
mov rax, [SystemTable]
mov rcx, [rax + EFI_SYSTEM_TABLE.ConIn]
lea rdx, [InputKey]
mov rax, [rcx + SIMPLE_INPUT_INTERFACE.ReadKeyStroke]
call rax
test rax, rax
jnz _wait_key
add rsp, 48h
mov rax, EFI_SUCCESS
ret
Сначала вызывается функция Reset для протокола ввода, это нужно сделать, чтобы очистить буфер клавиатуры от случайных нажатий, которые могли произойти еще до старта программы.
Затем идет основной цикл _wait_key. В нем вызывается функция
ReadKeyStroke из структуры SIMPLE_INPUT_INTERFACE которая пытается прочитать нажатую клавишу и записывает данные в структуру InputKey.Не удалось прогрузить медиа
С-подобно:
C:
typedef struct _SIMPLE_INPUT_INTERFACE {
unsigned long long (*Reset)(struct _SIMPLE_INPUT_INTERFACE *This, unsigned char ExtendedVerification);
unsigned long long (*ReadKeyStroke)(struct _SIMPLE_INPUT_INTERFACE *This, EFI_INPUT_KEY *Key);
void *WaitForKey;
} SIMPLE_INPUT_INTERFACE;
typedef struct {
unsigned short ScanCode;
unsigned short UnicodeChar;
} EFI_INPUT_KEY;
EFI_NOT_READY.Программа будет висеть в этом цикле до тех пор, пока в rax не придет заветный ноль, он же статус
EFI_SUCCESS. P.S также я поменял цвет немного.
Давайте же скомпилируем программу и проверим работоспособность:
Не удалось прогрузить медиа
Как видим программа работает, если же мы нажмем любую кнопку сгенерируется EFI_SUCESS нам выведется отладочная информация и все.
Спойлер: Code
C:
;// ghotycal57 created
format pe64 dll efi
entry main
include 'EFI.inc'
section '.text' code executable readable
main:
mov [SystemTable], rdx
sub rsp, 48h
mov rax, [SystemTable]
mov rcx, [rax + EFI_SYSTEM_TABLE.ConOut]
mov rax, [rcx + SIMPLE_TEXT_OUTPUT_INTERFACE.ClearScreen]
call rax
mov rax, [SystemTable]
mov rcx, [rax + EFI_SYSTEM_TABLE.ConOut]
mov rdx, 1Eh
mov rax, [rcx + SIMPLE_TEXT_OUTPUT_INTERFACE.SetAttribute]
call rax
mov rax, [SystemTable]
mov rcx, [rax + EFI_SYSTEM_TABLE.ConOut]
lea rdx, [_hello]
mov rax, [rcx + SIMPLE_TEXT_OUTPUT_INTERFACE.OutputString]
call rax
mov rax, [SystemTable]
mov rcx, [rax + EFI_SYSTEM_TABLE.ConIn]
xor rdx, rdx
mov rax, [rcx + SIMPLE_INPUT_INTERFACE.Reset]
call rax
_wait_key:
mov rax, [SystemTable]
mov rcx, [rax + EFI_SYSTEM_TABLE.ConIn]
lea rdx, [InputKey]
mov rax, [rcx + SIMPLE_INPUT_INTERFACE.ReadKeyStroke]
call rax
test rax, rax
jnz _wait_key
add rsp, 48h
mov rax, EFI_SUCCESS
ret
section '.data' data readable writeable
SystemTable dq ?
InputKey dw 0, 0
_hello du 'Hello from UEFI! This is forum-duty.free!', 13, 10
du 'Press any key to exit...', 0
👍43
🔥6
😁7
😮7
LIMB