Write for UEFI BIOS in Visual Studio. Part 2 - create our first driver and speed up debugging


This article will cover basic things about text I / O programming. In the program, which we will create this time ourselves from scratch, we first enter from the keyboard, and then we will display a text line, touching along the path of some unobvious programming features under UEFI. Previous article about setting development environment is placed here.

The second half of the article will be about speeding up the driver load when starting up for debugging, since two minutes that are spent on it now, if you launch it several times during the debugging process, your driver will be very annoying.

Immediately make your catalog for our exercises in the root of edk2 and call it EducationPkg . All projects will be created inside it. You can create every single project at the root of edk2 , there are no obstacles for this, but approximately on the tenth project, the zoo from its projects and packages of the edk2 framework will divorce at the root, which will lead, at best, to confusion. So, create the directory C:\FW\edk2\EducationPkg .

Using the UEFI Driver Wizard

Creating a set of files for the edk2 project is a big topic that deserves a separate article. You can read about it if you want to know the truth right now, in the EDK II Module Writer's Guide , Chapter 3 Module Development - here or google it. For the time being, we will use the Intel utility UEFI Driver Wizard , which lies in the downloaded directory in C:\FW\UEFIDriverWizard, to create the fileset.

We start the UEFI Driver Wizard , and the first thing to do is to specify the Workspace C:\FW\edk2 working environment using the File → OPEN WORKSPACE command . If this is not done first, then the UEFI Driver Wizard will carefully guide us through all the stages of creation, and then, with apologies, will say that the project driver can not create.

After specifying the Workspace, select File → New UEFI Driver and perform the following actions:

1. Click on the Browse button, go to the directory C:\FW\edk2\EducationPkg and create inside it your directory MyFirstDriver , in which we will work further. The driver name will take the same name.

2. Set Driver Revision to 1.0. I do not advise to put less than one - in the Wizard there is an error, as a result of which a revision results in 0.0 after creating project files.

3. Everything else is left unchanged, so that it turns out, as shown in the screenshot

Click the Next button and go to the next screen:

We tick the Component Name Protocol and Component Name 2 Protocol , do not touch the rest:

and then immediately click Finish, without touching the other settings. We receive a message about the successful creation of the project:

Click OK and close the UEFI Driver Wizard , we will not need it anymore.


Adding a module to the list for compilation

Since there is nothing ideal in the world, we will have to manually add a line for our driver to the list of modules for compilation. Here we must stop and give a little help.

All modules in edk2 are included in the so-called. Packages are groups of modules that are interconnected according to some common feature. To include a new module in any package , we need to add the path to the source code of the module to the PackageName.dsc file ( dsc - short for Description), and this module will be compiled as part of the Package . UEFI Wizard Driver our driver for compilation can not be added automatically, alas, for objective reasons (how could he know what you created for your new package (or which of the existing ones you intend to work with?). Therefore, we prescribe with pens.

Please understand that our EducationPkg is not yet a package , but just a collection of projects, and no more.

Open the file C:\FW\edk2\Nt32Pkg\Nt32Pkg.dsc , look for a line in it

# Add new modules here

and after this line we set the path to the file MyFirstDriver.inf of our driver relative to C:\FW\edk2 . It should turn out like this:

# Add new modules here 

Important note

Now you sadly think that you have to re-configure the project in Visual Studio , and it is in vain. Remember, in the first article we did not indicate anywhere which project we are talking about. Therefore, you do not need to edit anything, compilation after adding our inf-file to Nt32Pkg will go like this. This leads us to a very important consequence: we can add to an existing Visual Studio project, for example, NT32 , files of any project from the huge edk2 tree and all of them will be available for editing and, most importantly, setting control points, Watch and all others. features that Visual Studio offers. Actually, this is one of the most interesting advantages of this approach to working at UEFI using Visual Studio . And when you click on F5, before the compilation starts, the autosave of the modified sources added to the project will be performed. True, we pay for this approach with an increased compilation time, but we will further investigate this problem.

Let's do what we just talked about for our project. Right click on the NT32 project in Visual Studio ( remember to switch it to the default project in Solution ), select Add → Existing Item , go to our directory

select all files with the mouse and click on Add , adding them to our NT32 project.

Compiling and running the driver

Click on F5 or the Debugging button in Visual Studio , wait for the Shell to load, enter in it:

load MyFirstDriver.efi

And then we look at the message about the successful loading of our driver. It does nothing, which is not at all surprising - we haven’t added any functionality to it yet. We just need to check that we correctly added our driver to edk2 , and nothing more.
Here is what we need to see (the address can be any):

To close the window, click the Stop Debugging buttons, or Shift + F5 in Visual Studio .

Add our code

Open the file MyFirstDriver.c from the resulting project tree in Visual Studio and add our code to it. But first, a little theory, before moving on to practice.

In the UEFI BIOS, all interaction with the hardware, in our case, by emulating it on a virtual machine, occurs through protocols , you cannot take and write a certain byte to a specific port directly. In a very simplified form, you can consider the protocol as the name of a “class”, an instance of which is created to work with the device when it is registered in the system. Like a regular class instance, the protocol contains data and functions, and all the work with the equipment is ensured by calling the appropriate private functions of the class.

When working in UEFI , tables are used that contain pointers to all instances of the “classes”. There are several of these tables; in the case of our driver, we use the System Table , using the already declared gST pointer. The hierarchy of these tables is quite simple: there is a main System Table , which contains (among other things) references to Boot Services and Runtime Services . However, it will probably be easier to show the code:

gST = *SystemTable; gBS = gST->BootServices; gRT = gST->RuntimeServices;
By variable name: gST stands for:
g - means that the variable is global
ST , you guessed it, means System Table

So, our task is to send the string I have written my first UEFI driver to the output device (in our case, the display). It would be nice, of course, to use the same printf in unix-style and just specify different streams, but alas - there are very serious limitations with the use of printf in UEFI , so for now let's keep it aside, until better times.

Insert the output of our line into the function EntryPoint () of our driver. Add in the variable declaration area our variable of type CHAR16 (two-byte character encoding UCS-2 is used ) in the function MyFirstDriverDriverEntryPoint () in MyFirstDriver.c :

CHAR16 *MyString = L"I have written my first UEFI driver\r\n";
A lyrical note
edk2 does not forgive Warning -s, for it is all the same - that Warning , that Error - it sends to, hmm, the fix in both cases, so the compiler options in edk2 are configured by default. He thus implicitly says "Newbies can't work here." Of course, you can disable this option in configs, but you shouldn’t explain how to do this — if you can disable it, you can remove the sources of Warning messages in the code too, and you don’t need it. Therefore, type the types explicitly and comment on unused variables. Sometimes, in the case of using third-party sources, the complete suppression of errors can be quite a difficult task.

After, at the very end of the MyFirstDriverDriverEntryPoint () function, insert the output code of our text variable to the output console (default screen, in our case):
gST->ConOut->OutputString(gST->ConOut, MyString);
Click on F5, enter fs0: load MyFirstDriver.efi, and get our line on the screen:

The first program from scratch is written and works. We can congratulate yourself.

Let us now analyze our line:

gST->ConOut->OutputString(gST->ConOut, MyString);
gST - pointer to the System Table
ConOut - protocol , or, as we conditionally called it, the "class" of text output
OutputString is the output function itself. The first parameter in it, as it is easy to guess, is this for our protocol EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL .

Let's reformulate the above in a different form, for better understanding. Here is the hierarchy of the main System Table , obtained in the Watch window when you stop at a breakpoint in Visual Studio . The OutputString function we used is highlighted . Notice also the BootServices and RuntimeServices elements at the bottom of the table, which were discussed earlier:

Entering text from the keyboard and displaying it on the screen

Well, we derived the text contained in the string constant. Now you need to enter something into a string variable and then display this variable on the screen to make sure that you have never made a mistake in the process.

Let's take another screenshot of the same gST , but already in the ConIn part. It looks like this:

In our next example, we will memorize the password entered from the keyboard, displaying the characters entered using multi-colored asterisks in the New Year (or drug addict, as one friend noted) style, and after pressing Enter , display the password in the next line. This is just a training example, replacing the entered password with asterisks is already implemented in the HII API , as well as re-entering the password, and checking the re-match.

According to Feng Shui, it would be necessary to make a “simplifying” pointer * ConOut = gST-> ConOut , but for the purpose of learning, let us leave it as it is, so as not to recall which table we are currently accessing.

First we add local variables in the MyFirstDriverDriverEntryPoint () function instead of our previously added variable MyString.
UINTN EventIndex; UINTN Index = 0; EFI_INPUT_KEY Keys; CHAR16 PasswordString[256];
Now enter the text of the program, replacing the previously added function
gST->ConOut->OutputString(gST->ConOut, MyString);
to the following:
gST->ConOut->ClearScreen(gST->ConOut); // Hope it's obvious 
gST->ConOut->SetCursorPosition(gST->ConOut, 3, 10); // Put the cursor at 10-th line, 3rd position
// Print out our string at pointed place
gST->ConOut->OutputString(gST->ConOut, L"Enter password up to 255 characters lenght, then press 'Enter' to continue\r\n");
do {
// Wait for pressing a key
gBS->WaitForEvent (1, &gST->ConIn->WaitForKey, &EventIndex);
// Read key code
gST->ConIn->ReadKeyStroke (gST->ConIn, &Keys);
// Change both font and background colors
gST->ConOut->SetAttribute(gST->ConOut, Index & 0x7F);
// Build text array for output
PasswordString[Index++] = (Keys.UnicodeChar);
// Replace entered text to stars, the enemies are around :)
gST->ConOut->OutputString(gST->ConOut, L"*");
} // While "Enter" was pressed or 255 symbols were entered, fill in the text array by entered symbols
while (!(Keys.UnicodeChar == CHAR_LINEFEED || Keys.UnicodeChar == CHAR_CARRIAGE_RETURN || Index == 254));
// Terminate the text string
PasswordString[Index++] = '\0';
// Restore previous colors for font and background
gST->ConOut->SetAttribute(gST->ConOut, 0x0F);
// Print out what we entered
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");
Hit F5 and settle in the already usual pose for a couple of minutes:

After the Shell prompt, as always, enter fs0: and load MyFirstDriver (or load my and then press Tab twice, for Shell ). We get such a picture (of course, the text will be the one that you entered yourself):


Press Shift + F5 to close debugging, after having admired.

Further in the article we will get acquainted closer with the UEFI Shell, PCD environment and debug messages - without this, it is impossible to move on. But we will not get acquainted in the abstract, but in the useful process of accelerating and automating the launch of a driver for debugging.

Creating and editing the boot script for UEFI Shell

Now, since you probably have already recompiled the program more than once or twice, you are morally ripe to reduce this annoying waiting time for loading UEFI Shell and driving the same commands each time to load our driver. To begin with, we remove all manual text input to Shell by writing the appropriate script. We will write the script directly in Shell . From the point of view of common sense, it is better to open the script file in Far Manager and edit it there, but what will happen to you in the future that you will be thrown into the Shell of a real machine, and not a virtual machine with access to its file system from the host. Therefore, once we create a script in the Shell editor and write it down to get the appropriate skill.

The script file that is executed at startup (similar to autoexec.bat or bashrc ) for UEFI Shell is called startup.nsh , the one that Shell invites us to skip every time at boot. Log in to the UEFI Shell by pressing the F5 key in Visual Studio and enter fs0: to switch to our file system. Now from Shell enter the command

edit startup.nsh
and in the opened editor, enter, with a line break by Enter :
load MyFirstDriver.efi

Next, press F2 , then Enter to write and then F3 to exit the editor back to Shell .

We will not re-compile everything this time, since we have not changed anything in the program. We type the Exit command in the Shell , and in the text box that opens a-la BIOS Setup , and in UEFI terms, the Main Form , select Continue . After that, we will be thrown back into Shell again, but this time the startup.nsh script we created will be executed and our driver will start automatically.

Debug messages

For now, we can write debugging messages to the screen, but when we work with HII forms ( Human Interface Infrastructure ) - such an opportunity will not present itself, the screen will be occupied with hardware configuration forms. What to do in this case?
Somewhat abstract topic
At the very beginning, after receiving a new board from the factory, it is not always, oh, the display console for debugging output is not always available. In terms of the likelihood of possible connection errors, the display goes much ahead of the serial port, in which you can only screw up with the RX-TX direction. Therefore, there is also a serial port output, completely analogous to the one on the screen, with ConOut replaced by StdErr . Those. function
gST->ConOut->OutputString(gST->ConOut, L”Test string”);
will display the “Test string” on the display, and the function
gST->StdErr->OutputString(gST->StdErr, L”Test string”);
will output a test message to the serial port. Try it. Later, we will redirect the output in the virtual machine from the serial port to the log file, which may be useful in the future - write logs to a USB flash drive inserted into the USB port on a real hardware.

Display debug information in the OVMF window

To display information in the OVMF window, there is a DEBUG macro, which is usually used as follows:
DEBUG((EFI_D_INFO, "Test message is: %s\r\n", Message));
Where the first argument is a certain analogue of the Linux level of error messages ERROR_LEVEL , and the second, as you might guess, is a pointer to a string that needs to be output to the OVMF console.

The first argument can take the following values:

#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    // LOADFILE 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

By adjusting the level of appearance of these messages in the log, we can maintain the required balance between the level of logging and the performance of the system as a whole.

A bit more about the DEBUG macro
1. Its output has formatting and works like printf formatting. The % r format is very useful for debugging, which displays the Status diagnostic variable not as a 32-bit number in HEX, but as a human-readable line like Supported , Invalid Argument , etc.

2. This macro is automatically disabled when changing Debug to Release , so don’t be in a hurry to comment on it or hang up with ifndefs - everything has already been done for us.

3. Double brackets are really needed there. Try to remove and see what happens.

A small example
To illustrate what was written, let's add to the MyFirstDriverDriverEntryPoint () function, immediately after declaring variables, output text from several messages to the log with different levels of debugging and see which ones are displayed and which ones are filtered:
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"));
We start to debug and look into the OVMF window:

It can be seen that the messages with the EFI_D_INFO and EFI_D_ERROR levels got into the log, but they did not hit with the EFI_D_WARN level.

The mechanism for adjusting debug levels is quite simple: as we see in the list above, each level is characterized by one bit in a 32-bit word. To filter out messages that we do not need at the moment, we set the bit mask to the value of the level that cuts off bits that do not fall into this mask. In our case, the mask was 0x80000040 , because the EFI_D_WARN level with a value of 0x00000002 was filtered and did not hit the output, and the EFI_D_INFO level with a value of 0x00000040 , and EFI_D_ERROR with its value of 0x80000000 were in the mask and the corresponding messages were output.

Now we will not further delve into consideration of the implementation of the debug output level adjustment mechanism, we will consider only ways to change it in practice. There are two of them, the first one is fast, and the second one is correct. We begin, of course, with fast. Open the file c:\FW\Nt32Pkg\Nt32Pkg.dsc and look for the line in it containing PcdDebugPrintErrorLevel . Here she is:


Change the mask value to 0x80000042 and start building and debugging again.

Since we have changed the configuration file, edk2 will again rebuild all-all-all binaries for Nt32Pkg , but this process is finite, and here we see all three lines in the log, which was required to prove:

How to make quickly sorted out. Now do the right thing.

Platform Configuration Database (PCD)

The problem with the previous approach is that the edk2 tree and the Nt32Pkg package are the only ones, and changing the system settings for the only project is a direct path to the underworld, because at best in a week you will forget about this change completely and curse the fiend of hell called edk2 , that a month ago, regularly created from tested source codes under version control exactly what was needed, and now it gives out something completely different. Therefore, edk2 has a mechanism for changing system settings for a single project in order to localize changes to these settings only for this project. This mechanism is called PCD - Platform Configuration Database , and allows a lot. In general, a good style in edk2 is considered to be to remove from the source code in PCD any parameter that may be changed in the future. The length of the article does not allow one to dwell on the PCD description in more detail; therefore, it is better to look at the details about PCD here in Section 3.7.3 or here . For the first time, it is enough to restrict ourselves to reading the file C:\FW\edk2\MdeModulePkg\Universal\PCD\Dxe\Pcd.inf

From the point of view of practice, configuration using PCD is done like this: in the same file c:\FW\Nt32Pkg\Nt32Pkg.dsc change the display level of messages in the OVMF window:

EducationPkg/MyFirstDriver/MyFirstDriver.inf { 
<PcdsFixedAtBuild> gEfiMdePkgTokenSpaceGuid.PcdDebugPrintErrorLevel|0x80000042
Do not rush to immediately write the file. First, correct back 0x80000042 to the default value 0x80000040 in the line that we edited earlier:
And now you can write a file and rebuild the project. We start on debugging on F5 and we see our treasured three lines in the debug console.

Speeding up debugging

Remove a couple more annoying delays when starting up for debugging. Obviously, the first candidate is waiting for those 5 seconds before launching the startup.nsh script. Of course, it can also press a space, but any self-respecting lazy programmer should automate manual operations as much as possible.

Now it is necessary to contradict what was said earlier. The problem is that in these 5 seconds the delays are not written through PCD, but contrary to Feng Shui, directly, in the Shell sources. Therefore, we, whether we want it or not, will have to do the same: open the file C:\FW\edk2\ShellPkg\Application\Shell\Shell.c and change the initial value "5" to "1"

ShellInfoObject.ShellInitSettings.Delay = 1;//5;
It would be possible to set it to 0, but you never know ... We will forget to change it on a real hardware system, but it will not be possible to recompile later.

Hit F5 and enjoy 1 sec. delays instead of 5.

If someone knows how to set this delay value correctly, through PCD and without editing the Shell source code - let me know in a personal or in the comments, I will correct it.


Still accelerating

We recall about the progress bar, but in reality - just the timer waiting for the selection of the boot option. This is how it looks on the main screen:

And in the OVMF launch window, it looks a little different:

We must find the source of this "Zzzz" and then reduce waiting time to 1 sec.

Run a search on all files with the * .c extension of the Zzzz line, find this line in the source code C:\FW\MdeModulePkg\Universal\BdsDxe\BdsEntry.c and see there such a block of code:

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);
Accordingly, it is clear that the TimeoutRemain variable is read from the PCD configuration database , in the PcdPlatformBootTimeOut parameter . Ok, open our configuration file c:\FW\Nt32Pkg\Nt32Pkg.dsc , look for the line with PcdPlatformBootTimeOut there:
There we can't alter it locally, for our driver only - for that case the delay will be applied before time when our MyFirstDriver module will be loaded by startup script statup.nsh. So, we have to alter it in global way, whether we need it or not. For our case we need because additional delay in driver development process is undesirable. Alter 10 to 1 in our configuration file, press F5 and enjoy for fast load. For my machine loading time is 23 seconds.
One more tuning, for the interface at that time
We remove the second display, which we have no reason for yet, but has already begun to annoy. We edit the lines in our favorite configuration file c:\FW\Nt32Pkg\Nt32Pkg.dsc , opening it and removing ! My EDK II 2 and ! UGA Window 2 in two lines to make it:
gEfiNt32PkgTokenSpaceGuid.PcdWinNtGop|L"UGA Window 1"|VOID|52
gEfiNt32PkgTokenSpaceGuid.PcdWinNtUga|L"UGA Window 1"|VOID|52
You can, of course, change the very inscription in the header of the UGA Window 1 window for something spiritually closer to you personally, but this is up to you.

For the future

There is another big reserve for reducing the compile time and launch of the project (at times) - to compile not the entire edk2 , but only our module. But about this, as they say - in the next series. You can still try to do it yourself, an elementary decision, as you will see later.

You may now seem overkill - paying so much attention to reducing compilation and launch time, but believe me - when you start writing your programs, the difference between the target 10 seconds before launch, to which we will come, and 2 minutes, which were initially - is huge. Time spent now very quickly will pay off - the third article is not far off, and it will have to debug a lot.


My original article on Russian was published on: https://habr.com/ru/post/338404/

You can send me any questions to This email address is being protected from spambots. You need JavaScript enabled to view it.


©2021 Nikolay Bodunov. All Rights Reserved.