We are writing a DXE driver for taking screenshots from BIOS Setup and other UEFI applications
- Tutorial
In the last article about SecureBoot, I really lacked the ability to take a screenshot when configuring UEFI via BIOS Setup, but then redirected the text console to the serial port. This is a great solution, but it is available on few server motherboards, and through it you can get only pseudographics, but I would like to get the real one - it looks nicer, and you do not need to cut it out every time from the terminal window.
That’s exactly what we’ll do in this article, but at the same time I’ll tell you what the DXE driver is and how to write, assemble and test it yourself, how keyboard input and screen output in UEFI work, how to find this among the connected storage devices, on which you can write files, how to save something to a file from UEFI and how to adapt some external C code to work as part of the firmware.
If you are still interested, I’m waiting for you under the cut.
Before talking about writing and debugging drivers for UEFI, it’s worthwhile to immediately say that experimenting with firmware is a dangerous thing, they can lead to a “brick”, and in the most unfortunate rare cases, to hardware failure, so I warn you in advance: Everything that you read here, you use at your own peril and risk, I do not and will not be responsible for the loss of functionality of your firmware or board. Before starting any experiments with the firmware, it is necessary to make a full copy of the entire contents of the SPI flash using the programmer. Only in this way can you guarantee successful recovery of the firmware after any software failure.
If you do not have a programmer, but you really want to try to write and debug a DXE driver, use OVMF for this, VmWare Workstation 12 or any other virtualization system with UEFI support of your choice.
Our task is to take a screenshot from the whole screen during the operation of some UEFI application, for example BIOS Setup, by pressing a certain key combination, find a file system with write access and save the resulting screenshot to it. It would also be nice to get some kind of status indication. Because in order to take a screenshot, it is necessary to interrupt the operation of UEFI applications, the program itself for removing them cannot be an application, because there is no preemptive multitasking in UEFI, so we need a DXE driver.
The scheme of his work is planned approximately as follows:
0 . We load only after the appearance of text input (to handle keystrokes) and graphic output (so that there is something to take screenshots from).
1. We hang up the handler for pressing the combination LCtrl + LAlt + F12 (or any other to your taste) to all available input text consoles.
2 . In the handler, we find all the output graphic consoles, take a screenshot from them and encode it into PNG format (since UEFI applications usually do not use millions of colors, in this format screenshots are obtained in the size of tens of kilobytes instead of several megabytes in BMP).
3 . In the same handler, we find the first file system that can be written to the root and save the received files there.
It is possible to expand the functionality by choosing not the first FS found, but, for example, only USB devices or only ESP partitions , let us leave it to the reader to work independently.
There are two different SDKs for writing new code for working in UEFI - the newer EDK2 from the UEFI Forum and GNU-EFI from independent developers, based on the old Intel code. Both solutions imply that you will write code in C and / or assembler, in our case we will try to get by with a clean C.
It’s not for me to judge which SDK is better, but I suggest using EDK2, because it is official and cross-platform, and new features (along with fixing old bugs) appear in it much faster due to the proximity to the source of changes, plus it is used by all the IBVs I know to write my code.
EDK2 is in the process of constant development and it’s consistently adding 2-3 commits per day to its trunk, but since we don’t chase the latest trends here (they still don’t work for anyone), so we will use the latter for this moment stable cut EDK2, which is called UDK2015 .
To provide cross-platform and build capabilities by various compilers, EDK2 generates makefiles for each platform using the TXT (environment configuration), DEC, DSC and FDF (package configuration) and INF (component configuration) configuration files, I will tell you more about them along the way narratives, and now you need to get EDK2 and build HelloWorld, which is what we’ll do, if you can’t wait to find out more right now - follow the documentation.
It is understood that the software necessary for building code in C and assembler is already installed on your machine. If not, I suggest that Windows users install Visual Studio 2013 Express for Windows Desktop; Linux and OSX users will need GCC 4.4-4.9 and NASM.
If all this is already installed, you just have to download UDK2015 , unzip all the contents of UDK2015.MyWorkSpace.zip to where you have the right to create files (at least directly to your desktop or home directory), and then unzip the contents of BaseTools (Windows) .zip or BaseTools (Unix.zip) into the MyWorkSpace directory obtained in the previous step , which you then rename to something decent, for example, in UDK2015.
Now open the terminal, go to the newly created UDK2015 directory and execute the edksetup.bat (or .sh ) script there , which will copy a set of text files into the Conf subdirectory , we will be interested in tools_def.txt and target.txt .
The first file is large enough, it contains the definitions of environment variables with paths to the C and ASL compilers , assemblers, linkers, etc., necessary for the assembly environment . If you need to, you can correct the paths indicated there or add your own set of utilities (the so-called ToolChain), but if you followed my advice, then VS2013 (if you have 32-bit Windows), or VS2013x86 (in the case of 64-bit Windows), or GCC44 | ... | GCC49 (depending on your version of GCC, which it kindly displays in response to gcc --version ).
The second file contains the default assembly settings, in it I recommend setting the following values:
Now we will test the build environment with the build command , if everything was done correctly, then after a certain time it will end with a message like
Applications and drivers in EDK2 are not assembled separately, but as part of the so-called Package, i.e. package. In addition to the applications themselves, the package also includes libraries, sets of header files and files with a description of the configuration of the package and its contents. This is done in order to allow different drivers and applications to use different library implementations, to have access to various header files and GUIDs. We will use MdeModulePkg, it is a very general package without any dependencies on architecture and hardware, and if our driver can be built in it, it will almost guaranteed to work on any implementations of UEFI 2.1 and newer. The disadvantage of this approach is that most of the libraries in it (for example, DebugLib, used to get debug output) are just stubs, and you will have to write them yourself if the need arises.
To build our driver, you need an INF file with information about which libraries, protocols and files it needs to build, as well as adding the path to this INF file to the package's DSC file, so that the assembly system knows that such INF the file is.
Let's start from the end: open the file UDK2015 / MdeModulePkg / MdeModulePkg.dsc and scroll through it to the [Components] section (you can find it by searching - it's faster). The section lists all the files belonging to the package in order; the beginning of the section looks like this:
Now you need to fill in the INF file itself:
Now we finally have a blank for our driver, and you can go directly to writing the code. It’s clear that such an assembly system is no good, and working with it through editing text files is not very pleasant, so each IBV has its own solution for integrating the EDK2 assembly system into some modern IDE, for example, the AMI Visual eBIOS environment is This is laden with Eclipse plug-ins, while Phoenix and Insyde laden them with Visual Studio.
There is still a wonderful VisualUefi project the authorship of the famous computer security specialist Alex Ionescu, and if you also love Visual Studio - I suggest you try it, but for now we will continue to burn through hardcore, maintain the spirit of the old school and all that.
Everything is quite simple here: when loading the driver, we will sort through all instances of the SimpleTextInputEx protocol , which is published by the keyboard driver and most often exactly one, even if several keyboards are connected to the system - the buffer is common, unless you specifically change something. Nevertheless, just in case, we will sort through all available instances, calling each function RegisterKeyNotify , which as a parameter accepts the key combination that we intend to respond to, and a pointer to the callback function that will be called after pressing, we need a combination, and in it all the main work will already be done.
Before you search for available graphic consoles and take screenshots from them, you need to find out whether these screenshots can be saved somewhere. To do this, find all instances of the SimpleFileSystem protocol , which is published by the PartitionDxe driver for each detected volume whose FS is known to the firmware. Most often, the only known FSs are the FAT12 / 16/32 family (sometimes only FAT32), which, according to the UEFI standard, can be used for ESP. Next, you need to check that it is possible to write to the found FS, you can do it in different ways, the easiest way is to try to create a file on it and open it for reading and writing, if it works out, you can write to this FS. The solution, of course, is not the most optimal, but working, the correct implementation is offered to readers as an exercise.
After checking that there are plenty of screenshots to save, let's take them off. To do this, it is necessary to sort through all instances of the GOP protocol published by GOP drivers and VideoBIOS (more precisely, not VBIOS itself, which knows nothing about any protocols, but the ConSplitter driver, which implements the layer between old VBIOS and UEFI) for each output device with graphics. This protocol has a Blt function for copying images from and to the framebuffer until we need only the first. Using the Mode Objectof the same protocol, you can get the current screen resolution, which is needed to select the buffer of the desired size and take a screenshot from the entire screen, and not from some part of it. Having received the screenshot, it is worth checking that it is not completely black, because saving such ones is an unnecessary waste of time and space on the FS; you can also draw a black rectangle of the right size in Paint. Then you need to convert the image from BGR (in which Blt gives it) to RGB (which PNG encoder needs), otherwise the colors in the screenshots will be wrong. We encode the image obtained after conversion and save it to a file on the file system that we found in the previous step. We will collect the file name in 8.3 format from the current date and time, so there is less chance that one screenshot will overwrite another.
The best way to write code is not to write it, so let's take a ready-made library for encoding and decoding PNG called lodepng . Download, put next to our C-file, add our lines to lodepng.h and lodepng.c in the INF file in the [Sources.common] section , include the header file, and ... nothing compiles, because lodepng does not expect that the standard C language library can be taken like this and gone completely. We’ll finish it for the first time.
At the beginning of lodepng.h, add the following: which we don’t have either. You can, of course, create a local file with the same name by defining the type size_t there, but since we have begun to change, we will change it.
With lodepng.c a little harder, because from the standard library, besides size_t, it also needs memset, memcpy, malloc, realloc, free, qsort, and it also uses floating point calculations. Qsort implementation can be dragged away from Apple , memory functions can be made wrappers over gBS-> CopyMem , gBS-> SetMem , gBS-> AllocatePool and gBS-> FreePool respectively, and in order to signal about working with FPU you need to define the CONST INT32 constant _fltused = 0;otherwise the linker will swear at her absence. I’m not talking about commenting files with standard #includes anymore - everything is clear. Qsort.c is reduced
to a normal battle in the same way , just remember to add it to the .inf file.
It remains to write the ShowStatus function and our driver is ready. You can get this same status in different ways, for example, outputting numbers from 0x00 to 0xFF in the CPU IO port 80h, which is connected to the POST encoder, but not everyone has it, but on laptops it does not occur at all. You can squeak a speaker, but this is, firstly, platform-dependent, and secondly, it infuriates me wildly after a couple of screenshots. You can flash the lights on the keyboard, this is an additional task for the reader, and we will show the status of working with the graphic console directly through this graphic console - displaying a small square of the desired color in the upper left corner of the screen. In this case, a white square will mean “the driver has been loaded successfully”, yellow - “FS with no recording capabilities found”, blue - “The screenshot of the current console is completely black, there is no point in saving”, red - “an error occurred” and, finally, green - “screenshot taken and saved”. It is necessary to display this square on all consoles, and after a short time to restore that piece of the image that they had been overwritten.
We take our assembled driver from UDK2015 / Build / MdeModulePkg / RELEASE / X64 / MdeModulePkg / CrScreenshotDxe / CrScreenshotDxe / OUTPUT , we only need two files from there - the CrScreenshotDxe.efi driver itself and the dependency section for it to test the CrScreenspexDef driver
to start. Shell Copy the file CrScreenshotDxe.efi to the USB stick with the UEFI Shell, boot into it, go to the root of the flash drive with the fs0 command : (the number may change depending on the number of drives connected to your system) and run the load crScreenshotDxe.efi command. If you see a success message and a white square flashed in the upper corner of the screen, it means the driver is up and running. It looks like this for me:
Well, everything works from the shell and you can take screenshots from UEFI applications, like this:
Unfortunately, the screenshot from the BIOS Setup cannot be removed in this way - the driver loads too late. There are two possible solutions, the first is to add our driver along with the dependencies section in the DXE volume of the firmware using UEFITool , the second is to add it to OptionROM of some PCIe device, then you will not need to modify the firmware. I’ll try to implement the second method later, when I get the right piece of hardware, but there are no problems with the first one. We insert, sew, start, stick the USB flash drive, go to BIOS Setup, press LCtrl + LAlt + F12 - voila, we see the blue and green squares, everything works. The result looks like this:
The driver is written, the code is posted on GitHub , it remains to check the idea with OptionROM, and the topic can be said to be closed.
If you still don’t understand what’s going on here, have you found a bug in the code, or just want to discuss an article, an author, the monstrosity of UEFI or how good it was during the legacy BIOS - welcome to comment.
Thanks to the readers for their attention, good DXE drivers.
That’s exactly what we’ll do in this article, but at the same time I’ll tell you what the DXE driver is and how to write, assemble and test it yourself, how keyboard input and screen output in UEFI work, how to find this among the connected storage devices, on which you can write files, how to save something to a file from UEFI and how to adapt some external C code to work as part of the firmware.
If you are still interested, I’m waiting for you under the cut.
Denial of responsibility
Before talking about writing and debugging drivers for UEFI, it’s worthwhile to immediately say that experimenting with firmware is a dangerous thing, they can lead to a “brick”, and in the most unfortunate rare cases, to hardware failure, so I warn you in advance: Everything that you read here, you use at your own peril and risk, I do not and will not be responsible for the loss of functionality of your firmware or board. Before starting any experiments with the firmware, it is necessary to make a full copy of the entire contents of the SPI flash using the programmer. Only in this way can you guarantee successful recovery of the firmware after any software failure.
If you do not have a programmer, but you really want to try to write and debug a DXE driver, use OVMF for this, VmWare Workstation 12 or any other virtualization system with UEFI support of your choice.
What is needed there and why is it a DXE driver
Our task is to take a screenshot from the whole screen during the operation of some UEFI application, for example BIOS Setup, by pressing a certain key combination, find a file system with write access and save the resulting screenshot to it. It would also be nice to get some kind of status indication. Because in order to take a screenshot, it is necessary to interrupt the operation of UEFI applications, the program itself for removing them cannot be an application, because there is no preemptive multitasking in UEFI, so we need a DXE driver.
The scheme of his work is planned approximately as follows:
0 . We load only after the appearance of text input (to handle keystrokes) and graphic output (so that there is something to take screenshots from).
1. We hang up the handler for pressing the combination LCtrl + LAlt + F12 (or any other to your taste) to all available input text consoles.
2 . In the handler, we find all the output graphic consoles, take a screenshot from them and encode it into PNG format (since UEFI applications usually do not use millions of colors, in this format screenshots are obtained in the size of tens of kilobytes instead of several megabytes in BMP).
3 . In the same handler, we find the first file system that can be written to the root and save the received files there.
It is possible to expand the functionality by choosing not the first FS found, but, for example, only USB devices or only ESP partitions , let us leave it to the reader to work independently.
Choose SDK
There are two different SDKs for writing new code for working in UEFI - the newer EDK2 from the UEFI Forum and GNU-EFI from independent developers, based on the old Intel code. Both solutions imply that you will write code in C and / or assembler, in our case we will try to get by with a clean C.
It’s not for me to judge which SDK is better, but I suggest using EDK2, because it is official and cross-platform, and new features (along with fixing old bugs) appear in it much faster due to the proximity to the source of changes, plus it is used by all the IBVs I know to write my code.
EDK2 is in the process of constant development and it’s consistently adding 2-3 commits per day to its trunk, but since we don’t chase the latest trends here (they still don’t work for anyone), so we will use the latter for this moment stable cut EDK2, which is called UDK2015 .
To provide cross-platform and build capabilities by various compilers, EDK2 generates makefiles for each platform using the TXT (environment configuration), DEC, DSC and FDF (package configuration) and INF (component configuration) configuration files, I will tell you more about them along the way narratives, and now you need to get EDK2 and build HelloWorld, which is what we’ll do, if you can’t wait to find out more right now - follow the documentation.
Customize the build environment
It is understood that the software necessary for building code in C and assembler is already installed on your machine. If not, I suggest that Windows users install Visual Studio 2013 Express for Windows Desktop; Linux and OSX users will need GCC 4.4-4.9 and NASM.
If all this is already installed, you just have to download UDK2015 , unzip all the contents of UDK2015.MyWorkSpace.zip to where you have the right to create files (at least directly to your desktop or home directory), and then unzip the contents of BaseTools (Windows) .zip or BaseTools (Unix.zip) into the MyWorkSpace directory obtained in the previous step , which you then rename to something decent, for example, in UDK2015.
Now open the terminal, go to the newly created UDK2015 directory and execute the edksetup.bat (or .sh ) script there , which will copy a set of text files into the Conf subdirectory , we will be interested in tools_def.txt and target.txt .
The first file is large enough, it contains the definitions of environment variables with paths to the C and ASL compilers , assemblers, linkers, etc., necessary for the assembly environment . If you need to, you can correct the paths indicated there or add your own set of utilities (the so-called ToolChain), but if you followed my advice, then VS2013 (if you have 32-bit Windows), or VS2013x86 (in the case of 64-bit Windows), or GCC44 | ... | GCC49 (depending on your version of GCC, which it kindly displays in response to gcc --version ).
The second file contains the default assembly settings, in it I recommend setting the following values:
ACTIVE_PLATFROM = MdeModulePkg/MdeModulePkg.dsc # Основной пакет для разработки модулей
TARGET = RELEASE # Релизная конфигурация
TARGET_ARCH = X64 # DXE на большинстве современным машин 64-битная, исключения очень редки и очень болезненны
TOOL_CHAN_TAG = VS2013x86 # | VS2013 | GCC44 | ... | GCC49 | YOUR_FANCY_TOOLCHAIN, выберите наиболее подходящий в вашем случае
Open another terminal in UDK2015 and on Linux / OSX run the command: . edksetup.sh BaseTools
In the case of Windows, the usual edksetup.bat with no parameters is enough . Now we will test the build environment with the build command , if everything was done correctly, then after a certain time it will end with a message like
- Done -
Build end time: ...
Build total time: ...
If instead of Done you see Failed , then something is wrong with your settings. I checked the above on VS2013x86 on Windows and GCC48 on Xubuntu 14.04.3 - OIA .Project structure
Applications and drivers in EDK2 are not assembled separately, but as part of the so-called Package, i.e. package. In addition to the applications themselves, the package also includes libraries, sets of header files and files with a description of the configuration of the package and its contents. This is done in order to allow different drivers and applications to use different library implementations, to have access to various header files and GUIDs. We will use MdeModulePkg, it is a very general package without any dependencies on architecture and hardware, and if our driver can be built in it, it will almost guaranteed to work on any implementations of UEFI 2.1 and newer. The disadvantage of this approach is that most of the libraries in it (for example, DebugLib, used to get debug output) are just stubs, and you will have to write them yourself if the need arises.
To build our driver, you need an INF file with information about which libraries, protocols and files it needs to build, as well as adding the path to this INF file to the package's DSC file, so that the assembly system knows that such INF the file is.
Let's start from the end: open the file UDK2015 / MdeModulePkg / MdeModulePkg.dsc and scroll through it to the [Components] section (you can find it by searching - it's faster). The section lists all the files belonging to the package in order; the beginning of the section looks like this:
[Components]
MdeModulePkg/Application/HelloWorld/HelloWorld.inf
MdeModulePkg/Application/MemoryProfileInfo/MemoryProfileInfo.inf
...
Add your future INF file there along with the path to it relative to UDK2015 . I propose to create right in for him MdeModulePkg folder CrScreenshotDxe , and he INF-file called CrScreenshotDxe.inf . As you may have guessed, Cr is from CodeRush, and the author of this article is modesty itself. The result is something like this:[Components]
MdeModulePkg/CrScreenshotDxe/CrScreenshotDxe.inf
MdeModulePkg/Application/HelloWorld/HelloWorld.inf
MdeModulePkg/Application/MemoryProfileInfo/MemoryProfileInfo.inf
...
We save the changes and close the DSC file, we won’t change it anymore if we don’t want to configure the debug output, but this is a completely different story. Now you need to fill in the INF file itself:
It will look something like this
It remains to create the CrScreenshotDxe.s file mentioned above :[Defines] # Основные определения
INF_VERSION = 0x00010005 # Версия спецификации, нам достаточно 1.5
BASE_NAME = CrScreenshotDxe # Название компонента
FILE_GUID = cab058df-e938-4f85-8978-1f7e6aabdb96 # GUID компонента
MODULE_TYPE = DXE_DRIVER # Тип компонента
VERSION_STRING = 1.0 # Версия компонента
ENTRY_POINT = CrScreenshotDxeEntry # Имя точки входа
[Sources.common] # Файлы для сборки, common - общие для всех арзитектур
CrScreenshotDxe.c # Код нашего драйвера
#... # Может быть, нам понадобится что-то еще, конвертер в PNG, к примеру
[Packages] # Используемые пакеты
MdePkg/MdePkg.dec # Основной пакет, без него не обходится ни один компонент UEFI
MdeModulePkg/MdeModulePkg.dec # Второй основной пакет, нужный драйверам и приложениям
[LibraryClasses] # Используемые библиотеки
UefiBootServicesTableLib # Удобный доступ к UEFI Boot Services через указатель gBS
UefiRuntimeServicesTableLib # Не менее удобный доступ к UEFI Runtime services через указатель gRT
UefiDriverEntryPoint # Точка входа в UEFI-драйвер, без нее конструкторы библиотек не сработают, а они нужны
DebugLib # Для макроса DEBUG
PrintLib # Для UnicodeSPrint, местного аналога snprintf
[Protocols] # Используемые протоколы
gEfiGraphicsOutputProtocolGuid # Доступ к графической консоли
gEfiSimpleTextInputExProtocolGuid # Доступ к текстовому вводу
gEfiSimpleFileSystemProtocolGuid # Доступ к файловым системам
[Depex] # Зависимости драйвера, пока эти протоколы недоступны, драйвер не запустится
gEfiGraphicsOutputProtocolGuid AND # Доступ к ФС для запуска не обязателен, потом проверим его наличие в рантайме
gEfiSimpleTextInputExProtocolGuid #
With this content
If you now repeat the build command , it must be successful, otherwise you did something wrong. #include
#include
#include
#include
#include
#include
#include
#include
#include
EFI_STATUS
EFIAPI
CrScreenshotDxeEntry (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
return EFI_SUCCESS;
}
Now we finally have a blank for our driver, and you can go directly to writing the code. It’s clear that such an assembly system is no good, and working with it through editing text files is not very pleasant, so each IBV has its own solution for integrating the EDK2 assembly system into some modern IDE, for example, the AMI Visual eBIOS environment is This is laden with Eclipse plug-ins, while Phoenix and Insyde laden them with Visual Studio.
There is still a wonderful VisualUefi project the authorship of the famous computer security specialist Alex Ionescu, and if you also love Visual Studio - I suggest you try it, but for now we will continue to burn through hardcore, maintain the spirit of the old school and all that.
Responding to a keystroke
Everything is quite simple here: when loading the driver, we will sort through all instances of the SimpleTextInputEx protocol , which is published by the keyboard driver and most often exactly one, even if several keyboards are connected to the system - the buffer is common, unless you specifically change something. Nevertheless, just in case, we will sort through all available instances, calling each function RegisterKeyNotify , which as a parameter accepts the key combination that we intend to respond to, and a pointer to the callback function that will be called after pressing, we need a combination, and in it all the main work will already be done.
Translate from Russian to C
For successful compilation, the TakeScreenshot and ShowStatus functions , which are described below, are still not enough .EFI_STATUS
EFIAPI
CrScreenshotDxeEntry (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
EFI_STATUS Status;
EFI_KEY_DATA KeyStroke;
UINTN HandleCount;
EFI_HANDLE *HandleBuffer = NULL;
UINTN i;
// Set keystroke to be LCtrl+LAlt+F12
KeyStroke.Key.ScanCode = SCAN_F12;
KeyStroke.Key.UnicodeChar = 0;
KeyStroke.KeyState.KeyShiftState = EFI_SHIFT_STATE_VALID | EFI_LEFT_CONTROL_PRESSED | EFI_LEFT_ALT_PRESSED;
KeyStroke.KeyState.KeyToggleState = 0;
// Locate all SimpleTextInEx protocols
Status = gBS->LocateHandleBuffer(ByProtocol, &gEfiSimpleTextInputExProtocolGuid, NULL, &HandleCount, &HandleBuffer);
if (EFI_ERROR (Status)) {
DEBUG((-1, "CrScreenshotDxeEntry: gBS->LocateHandleBuffer returned %r\n", Status));
return EFI_UNSUPPORTED;
}
// For each instance
for (i = 0; i < HandleCount; i++) {
EFI_HANDLE Handle;
EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *SimpleTextInEx;
// Get protocol handle
Status = gBS->HandleProtocol (HandleBuffer[i], &gEfiSimpleTextInputExProtocolGuid, (VOID **) &SimpleTextInEx);
if (EFI_ERROR (Status)) {
DEBUG((-1, "CrScreenshotDxeEntry: gBS->HandleProtocol[%d] returned %r\n", i, Status));
continue;
}
// Register key notification function
Status = SimpleTextInEx->RegisterKeyNotify(
SimpleTextInEx,
&KeyStroke,
TakeScreenshot,
&Handle);
if (EFI_ERROR (Status)) {
DEBUG((-1, "CrScreenshotDxeEntry: SimpleTextInEx->RegisterKeyNotify[%d] returned %r\n", i, Status));
}
}
// Free memory used for handle buffer
if (HandleBuffer)
gBS->FreePool(HandleBuffer);
// Show driver loaded
ShowStatus(0xFF, 0xFF, 0xFF); // White
return EFI_SUCCESS;
}
We are looking for FS with write access, write data to a file
Before you search for available graphic consoles and take screenshots from them, you need to find out whether these screenshots can be saved somewhere. To do this, find all instances of the SimpleFileSystem protocol , which is published by the PartitionDxe driver for each detected volume whose FS is known to the firmware. Most often, the only known FSs are the FAT12 / 16/32 family (sometimes only FAT32), which, according to the UEFI standard, can be used for ESP. Next, you need to check that it is possible to write to the found FS, you can do it in different ways, the easiest way is to try to create a file on it and open it for reading and writing, if it works out, you can write to this FS. The solution, of course, is not the most optimal, but working, the correct implementation is offered to readers as an exercise.
Again translate from Russian to C
This code does not need anything else, it works as is.EFI_STATUS
EFIAPI
FindWritableFs (
OUT EFI_FILE_PROTOCOL **WritableFs
)
{
EFI_HANDLE *HandleBuffer = NULL;
UINTN HandleCount;
UINTN i;
// Locate all the simple file system devices in the system
EFI_STATUS Status = gBS->LocateHandleBuffer(ByProtocol, &gEfiSimpleFileSystemProtocolGuid, NULL, &HandleCount, &HandleBuffer);
if (!EFI_ERROR (Status)) {
EFI_FILE_PROTOCOL *Fs = NULL;
// For each located volume
for (i = 0; i < HandleCount; i++) {
EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *SimpleFs = NULL;
EFI_FILE_PROTOCOL *File = NULL;
// Get protocol pointer for current volume
Status = gBS->HandleProtocol(HandleBuffer[i], &gEfiSimpleFileSystemProtocolGuid, (VOID **) &SimpleFs);
if (EFI_ERROR (Status)) {
DEBUG((-1, "FindWritableFs: gBS->HandleProtocol[%d] returned %r\n", i, Status));
continue;
}
// Open the volume
Status = SimpleFs->OpenVolume(SimpleFs, &Fs);
if (EFI_ERROR (Status)) {
DEBUG((-1, "FindWritableFs: SimpleFs->OpenVolume[%d] returned %r\n", i, Status));
continue;
}
// Try opening a file for writing
Status = Fs->Open(Fs, &File, L"crsdtest.fil", EFI_FILE_MODE_CREATE | EFI_FILE_MODE_READ | EFI_FILE_MODE_WRITE, 0);
if (EFI_ERROR (Status)) {
DEBUG((-1, "FindWritableFs: Fs->Open[%d] returned %r\n", i, Status));
continue;
}
// Writable FS found
Fs->Delete(File);
*WritableFs = Fs;
Status = EFI_SUCCESS;
break;
}
}
// Free memory
if (HandleBuffer) {
gBS->FreePool(HandleBuffer);
}
return Status;
}
We are looking for a graphical console and take a screenshot of it
After checking that there are plenty of screenshots to save, let's take them off. To do this, it is necessary to sort through all instances of the GOP protocol published by GOP drivers and VideoBIOS (more precisely, not VBIOS itself, which knows nothing about any protocols, but the ConSplitter driver, which implements the layer between old VBIOS and UEFI) for each output device with graphics. This protocol has a Blt function for copying images from and to the framebuffer until we need only the first. Using the Mode Objectof the same protocol, you can get the current screen resolution, which is needed to select the buffer of the desired size and take a screenshot from the entire screen, and not from some part of it. Having received the screenshot, it is worth checking that it is not completely black, because saving such ones is an unnecessary waste of time and space on the FS; you can also draw a black rectangle of the right size in Paint. Then you need to convert the image from BGR (in which Blt gives it) to RGB (which PNG encoder needs), otherwise the colors in the screenshots will be wrong. We encode the image obtained after conversion and save it to a file on the file system that we found in the previous step. We will collect the file name in 8.3 format from the current date and time, so there is less chance that one screenshot will overwrite another.
Again translate from Russian to C
To work lacks lodepng_encode32 and already mentioned showStatus , will continue.EFI_STATUS
EFIAPI
TakeScreenshot (
IN EFI_KEY_DATA *KeyData
)
{
EFI_FILE_PROTOCOL *Fs = NULL;
EFI_FILE_PROTOCOL *File = NULL;
EFI_GRAPHICS_OUTPUT_PROTOCOL *GraphicsOutput = NULL;
EFI_GRAPHICS_OUTPUT_BLT_PIXEL *Image = NULL;
UINTN ImageSize; // Size in pixels
UINT8 *PngFile = NULL;
UINTN PngFileSize; // Size in bytes
EFI_STATUS Status;
UINTN HandleCount;
EFI_HANDLE *HandleBuffer = NULL;
UINT32 ScreenWidth;
UINT32 ScreenHeight;
CHAR16 FileName[8+1+3+1]; // 0-terminated 8.3 file name
EFI_TIME Time;
UINTN i, j;
// Find writable FS
Status = FindWritableFs(&Fs);
if (EFI_ERROR (Status)) {
DEBUG((-1, "TakeScreenshot: Can't find writable FS\n"));
ShowStatus(0xFF, 0xFF, 0x00); // Yellow
return EFI_SUCCESS;
}
// Locate all instances of GOP
Status = gBS->LocateHandleBuffer(ByProtocol, &gEfiGraphicsOutputProtocolGuid, NULL, &HandleCount, &HandleBuffer);
if (EFI_ERROR (Status)) {
DEBUG((-1, "ShowStatus: Graphics output protocol not found\n"));
return EFI_SUCCESS;
}
// For each GOP instance
for (i = 0; i < HandleCount; i++) {
do { // Break from do used instead of "goto error"
// Handle protocol
Status = gBS->HandleProtocol(HandleBuffer[i], &gEfiGraphicsOutputProtocolGuid, (VOID **) &GraphicsOutput);
if (EFI_ERROR (Status)) {
DEBUG((-1, "ShowStatus: gBS->HandleProtocol[%d] returned %r\n", i, Status));
break;
}
// Set screen width, height and image size in pixels
ScreenWidth = GraphicsOutput->Mode->Info->HorizontalResolution;
ScreenHeight = GraphicsOutput->Mode->Info->VerticalResolution;
ImageSize = ScreenWidth * ScreenHeight;
// Get current time
Status = gRT->GetTime(&Time, NULL);
if (!EFI_ERROR(Status)) {
// Set file name to current day and time
UnicodeSPrint(FileName, 26, L"%02d%02d%02d%02d.png", Time.Day, Time.Hour, Time.Minute, Time.Second);
}
else {
// Set file name to scrnshot.png
UnicodeSPrint(FileName, 26, L"scrnshot.png");
}
// Allocate memory for screenshot
Status = gBS->AllocatePool(EfiBootServicesData, ImageSize * sizeof(EFI_GRAPHICS_OUTPUT_BLT_PIXEL), (VOID **)&Image);
if (EFI_ERROR(Status)) {
DEBUG((-1, "TakeScreenshot: gBS->AllocatePool returned %r\n", Status));
break;
}
// Take screenshot
Status = GraphicsOutput->Blt(GraphicsOutput, Image, EfiBltVideoToBltBuffer, 0, 0, 0, 0, ScreenWidth, ScreenHeight, 0);
if (EFI_ERROR(Status)) {
DEBUG((-1, "TakeScreenshot: GraphicsOutput->Blt returned %r\n", Status));
break;
}
// Check for pitch black image (it means we are using a wrong GOP)
for (j = 0; j < ImageSize; j++) {
if (Image[j].Red != 0x00 || Image[j].Green != 0x00 || Image[j].Blue != 0x00)
break;
}
if (j == ImageSize) {
DEBUG((-1, "TakeScreenshot: GraphicsOutput->Blt returned pitch black image, skipped\n"));
ShowStatus(0x00, 0x00, 0xFF); // Blue
break;
}
// Open or create output file
Status = Fs->Open(Fs, &File, FileName, EFI_FILE_MODE_CREATE | EFI_FILE_MODE_READ | EFI_FILE_MODE_WRITE, 0);
if (EFI_ERROR (Status)) {
DEBUG((-1, "TakeScreenshot: Fs->Open of %s returned %r\n", FileName, Status));
break;
}
// Convert BGR to RGBA with Alpha set to 0xFF
for (j = 0; j < ImageSize; j++) {
UINT8 Temp = Image[j].Blue;
Image[j].Blue = Image[j].Red;
Image[j].Red = Temp;
Image[j].Reserved = 0xFF;
}
// Encode raw RGB image to PNG format
j = lodepng_encode32(&PngFile, &PngFileSize, (CONST UINT8*)Image, ScreenWidth, ScreenHeight);
if (j) {
DEBUG((-1, "TakeScreenshot: lodepng_encode32 returned %d\n", j));
break;
}
// Write PNG image into the file and close it
Status = File->Write(File, &PngFileSize, PngFile);
File->Close(File);
if (EFI_ERROR(Status)) {
DEBUG((-1, "TakeScreenshot: File->Write returned %r\n", Status));
break;
}
// Show success
ShowStatus(0x00, 0xFF, 0x00); // Green
} while(0);
// Free memory
if (Image)
gBS->FreePool(Image);
if (PngFile)
gBS->FreePool(PngFile);
Image = NULL;
PngFile = NULL;
}
// Show error
if (EFI_ERROR(Status))
ShowStatus(0xFF, 0x00, 0x00); // Red
return EFI_SUCCESS;
}
We encode the image in PNG format
The best way to write code is not to write it, so let's take a ready-made library for encoding and decoding PNG called lodepng . Download, put next to our C-file, add our lines to lodepng.h and lodepng.c in the INF file in the [Sources.common] section , include the header file, and ... nothing compiles, because lodepng does not expect that the standard C language library can be taken like this and gone completely. We’ll finish it for the first time.
At the beginning of lodepng.h, add the following:
#include // Для успешной сборки в среде UEFI
#define LODEPNG_NO_COMPILE_DECODER // Отключаем декодер PNG
#define LODEPNG_NO_COMPILE_DISK // Отключаем запись на диск, т.к. fopen/fwrite у нас нет
#define LODEPNG_NO_COMPILE_ALLOCATORS // Отключаем стандартные malloc/realloc/free, т.к. их у нас нет
#define LODEPNG_NO_COMPILE_ERROR_TEXT // Отключаем сообщения об ошибках
#define LODEPNG_NO_COMPILE_ANCILLARY_CHUNKS // Отключаем текстовые данные в PNG, т.к. не нужны
#if !defined(_MSC_VER) // Определяем тип size_t для GCC, у MS он встроен при настройках сборки по умолчанию
#define size_t UINTN
#endif
And comment out the line with #includeWith lodepng.c a little harder, because from the standard library, besides size_t, it also needs memset, memcpy, malloc, realloc, free, qsort, and it also uses floating point calculations. Qsort implementation can be dragged away from Apple , memory functions can be made wrappers over gBS-> CopyMem , gBS-> SetMem , gBS-> AllocatePool and gBS-> FreePool respectively, and in order to signal about working with FPU you need to define the CONST INT32 constant _fltused = 0;otherwise the linker will swear at her absence. I’m not talking about commenting files with standard #includes anymore - everything is clear. Qsort.c is reduced
to a normal battle in the same way , just remember to add it to the .inf file.
Display status
It remains to write the ShowStatus function and our driver is ready. You can get this same status in different ways, for example, outputting numbers from 0x00 to 0xFF in the CPU IO port 80h, which is connected to the POST encoder, but not everyone has it, but on laptops it does not occur at all. You can squeak a speaker, but this is, firstly, platform-dependent, and secondly, it infuriates me wildly after a couple of screenshots. You can flash the lights on the keyboard, this is an additional task for the reader, and we will show the status of working with the graphic console directly through this graphic console - displaying a small square of the desired color in the upper left corner of the screen. In this case, a white square will mean “the driver has been loaded successfully”, yellow - “FS with no recording capabilities found”, blue - “The screenshot of the current console is completely black, there is no point in saving”, red - “an error occurred” and, finally, green - “screenshot taken and saved”. It is necessary to display this square on all consoles, and after a short time to restore that piece of the image that they had been overwritten.
Last time translate from Russian to C
Now everything is ready and successfully going, if not, saw it until it is ready , or download my ready-made driver from GitHub and compare it with yours, maybe I forgot to describe some changes.EFI_STATUS
EFIAPI
ShowStatus (
IN UINT8 Red,
IN UINT8 Green,
IN UINT8 Blue
)
{
// Determines the size of status square
#define STATUS_SQUARE_SIDE 5
UINTN HandleCount;
EFI_HANDLE *HandleBuffer = NULL;
EFI_GRAPHICS_OUTPUT_PROTOCOL *GraphicsOutput = NULL;
EFI_GRAPHICS_OUTPUT_BLT_PIXEL Square[STATUS_SQUARE_SIDE * STATUS_SQUARE_SIDE];
EFI_GRAPHICS_OUTPUT_BLT_PIXEL Backup[STATUS_SQUARE_SIDE * STATUS_SQUARE_SIDE];
UINTN i;
// Locate all instances of GOP
EFI_STATUS Status = gBS->LocateHandleBuffer(ByProtocol, &gEfiGraphicsOutputProtocolGuid, NULL, &HandleCount, &HandleBuffer);
if (EFI_ERROR (Status)) {
DEBUG((-1, "ShowStatus: Graphics output protocol not found\n"));
return EFI_UNSUPPORTED;
}
// Set square color
for (i = 0 ; i < STATUS_SQUARE_SIDE * STATUS_SQUARE_SIDE; i++) {
Square[i].Blue = Blue;
Square[i].Green = Green;
Square[i].Red = Red;
Square[i].Reserved = 0x00;
}
// For each GOP instance
for (i = 0; i < HandleCount; i ++) {
// Handle protocol
Status = gBS->HandleProtocol(HandleBuffer[i], &gEfiGraphicsOutputProtocolGuid, (VOID **) &GraphicsOutput);
if (EFI_ERROR (Status)) {
DEBUG((-1, "ShowStatus: gBS->HandleProtocol[%d] returned %r\n", i, Status));
continue;
}
// Backup current image
GraphicsOutput->Blt(GraphicsOutput, Backup, EfiBltVideoToBltBuffer, 0, 0, 0, 0, STATUS_SQUARE_SIDE, STATUS_SQUARE_SIDE, 0);
// Draw the status square
GraphicsOutput->Blt(GraphicsOutput, Square, EfiBltBufferToVideo, 0, 0, 0, 0, STATUS_SQUARE_SIDE, STATUS_SQUARE_SIDE, 0);
// Wait 500ms
gBS->Stall(500*1000);
// Restore the backup
GraphicsOutput->Blt(GraphicsOutput, Backup, EfiBltBufferToVideo, 0, 0, 0, 0, STATUS_SQUARE_SIDE, STATUS_SQUARE_SIDE, 0);
}
return EFI_SUCCESS;
}
Testing the result in UEFI Shell
We take our assembled driver from UDK2015 / Build / MdeModulePkg / RELEASE / X64 / MdeModulePkg / CrScreenshotDxe / CrScreenshotDxe / OUTPUT , we only need two files from there - the CrScreenshotDxe.efi driver itself and the dependency section for it to test the CrScreenspexDef driver
to start. Shell Copy the file CrScreenshotDxe.efi to the USB stick with the UEFI Shell, boot into it, go to the root of the flash drive with the fs0 command : (the number may change depending on the number of drives connected to your system) and run the load crScreenshotDxe.efi command. If you see a success message and a white square flashed in the upper corner of the screen, it means the driver is up and running. It looks like this for me:
UEFI Shell
This screenshot, like all subsequent ones, was taken by our driver, therefore, a square in the corner is not visible on it.
Then boldly press LCtrl + LAlt + F12 and watch the status. On my systems with AMI, there is only one graphic console, and therefore I see a flashing green square and I get one screenshot for one click of a combination. On my systems with Phoenix and Insyde, there were two graphic consoles, one of which is empty, so I see a blue square first and then a green square, but there is also only one screenshot. The test result from UEFI Shell looks the same on them, only the resolution there is no longer 800x600, but 1366x768. This screenshot, like all subsequent ones, was taken by our driver, therefore, a square in the corner is not visible on it.
Well, everything works from the shell and you can take screenshots from UEFI applications, like this:
RU.efi
Testing the result in modified firmware
Unfortunately, the screenshot from the BIOS Setup cannot be removed in this way - the driver loads too late. There are two possible solutions, the first is to add our driver along with the dependencies section in the DXE volume of the firmware using UEFITool , the second is to add it to OptionROM of some PCIe device, then you will not need to modify the firmware. I’ll try to implement the second method later, when I get the right piece of hardware, but there are no problems with the first one. We insert, sew, start, stick the USB flash drive, go to BIOS Setup, press LCtrl + LAlt + F12 - voila, we see the blue and green squares, everything works. The result looks like this:
Password Entry Form
Information Tab
Main Tab
Security Tab
Boot tab
Exit Tab
This is a success, gentlemen.Conclusion
The driver is written, the code is posted on GitHub , it remains to check the idea with OptionROM, and the topic can be said to be closed.
If you still don’t understand what’s going on here, have you found a bug in the code, or just want to discuss an article, an author, the monstrosity of UEFI or how good it was during the legacy BIOS - welcome to comment.
Thanks to the readers for their attention, good DXE drivers.