Chapter 4 – Protocols You Should Know

Common sense ain’t common.

—Will Rogers

This chapter describes protocols that everyone who is working with the Unified Extensible Firmware Interface (UEFI), whether creating device drivers, UEFI pre-OS applications, or platform firmware, should know. The protocols are illustrated by a few examples, beginning with the most common exercise from any programming text, namely “Hello world.” The test application listed here is the simplest possible application that can be written. It does not depend upon any UEFI Library functions, so the UEFI Library is not linked into the executable that is generated. This test application uses the SystemTable that is passed into the entry point to get access to the UEFI console devices. The console output device is used to display a message using the OutputString() function of the SIMPLE_TEXT_OUTPUT_INTERFACE protocol, and the application waits for a keystroke from the user on the console input device using the WaitForEvent() service with the WaitForKey event in the SIMPLE_INPUT_INTERFACE protocol. Once a key is pressed, the application exits.

To execute an UEFI application, type the program’s name at the UEFI Shell command line. The following examples show how to run the test application described above from the UEFI Shell. The application waits for the user to press a key before returning to the UEFI Shell prompt. It is assumed that hello.efi is in the search path of the UEFI Shell environment.

EFI OS Loaders

This section discusses the special considerations that are required when writing an OS loader. An OS loader is a special type of UEFI application responsible for transitioning a system from a firmware environment into an OS environment. To accomplish this task, several important steps must be taken:

  1. The OS loader must determine from where it was loaded. This determination allows an OS loader to retrieve additional files from the same location.
  2. The OS loader must determine where in the system the OS exists. Typically, the OS resides on a partition of a hard drive. However, the partition where the OS exists may not use a file system that is recognized by the UEFI environment. In this case, the OS loader can only access the partition as a block device using only block I/ O operations. The OS loader will then be required to implement or load the file system driver to access files on the OS partition.
  3. The OS loader must build a memory map of the physical memory resources so that the OS kernel can know what memory to manage. Some of the physical memory in the system must remain untouched by the OS kernel, so the OS loader must use the UEFI APIs to retrieve the system’s current memory map.
  4. An OS has the option of storing boot paths and boot options in nonvolatile storage in the form of environment variables. The OS loader may need to use some of the environment variables that are stored in nonvolatile storage. In addition, the OS loader may be required to pass some of the environment variables to the OS kernel.
  5. The next step is to call ExitBootServices(). This call can be done from either the OS loader or from the OS kernel. Special care must be taken to guarantee that the most current memory map has been retrieved prior to making this call. Once ExitBootServices() had been called, no more UEFI Boot Services calls can be made. At some point, either just prior to calling Exit-BootServices() or just after, the OS loader will transfer control to the OS kernel.
  6. Finally, after ExitBootServices() has been called, the UEFI Boot Services calls are no longer available. This lack of availability means that once an OS kernel has taken control of the system, the OS kernel may only call UEFI Runtime Services.

A complete listing of a sample application for an OS loader can be found below. The code fragments in the following sections do not perform any error checking. Also, the OS loader sample application makes use of several UEFI Library functions to simplify the implementation.

The output shown below starts by printing out the device path and the file path of the OS loader itself. It also shows where in memory the OS loader resides and how many bytes it is using. Next, it loads the file OSKERNEL.BIN into memory. The file OSKERNEL.BIN is retrieved from the same directory as the image of the OS loader sample of Figure 4.1.

Figure 4.1: EFI Loader in System Diagram

The next section of the output shows the first block of several block devices. The first one is the first block of the floppy drive with a FAT12 file system. The second one is the Master Boot Record (MBR) from the hard drive. The third one is the first block of a large FAT32 partition on the same hard drive, and the fourth one is the first block of a smaller FAT16 partition on the same hard drive.

The final step shows the pointers to all the system configuration tables, the system’s current memory map, and a list of all the system’s environment variables. The very last step shown is the OS loader calling ExitBootServices().

Device Path and Image Information of the OS Loader

The following code fragment shows the steps that are required to get the device path and file path to the OS loader itself. The first call to HandleProtocol() gets the LOADED_IMAGE_PROTOCOL interface from the ImageHandle that was passed into the OS loader application. The second call to HandleProtocol() gets the DEVICE_PATH_PROTOCOL interface to the device handle of the OS loader image. These two calls transmit the device path of the OS loader image, the file path, and other image information to the OS loader itself.

Accessing Files in the Device Path of the OS Loader

The previous section shows how to retrieve the device path and the image path of the OS loader image. The following code fragment shows how to use this information to open another file called OSKERNEL.BIN that resides in the same directory as the OS loader itself. The first step is to use HandleProtocol() to get the FILE_SYSTEM_PROTOCOL interface to the device handle retrieved in the previous section. Then, the disk volume can be opened so file access calls can be made. The end result is that the variable CurDir is a file handle to the same partition in which the OS loader resides.

The next step is to build a file path to OSKERNEL.BIN that exists in the same directory as the OS loader image. Once the path is built, the file handle CurDir can be used to call Open(), Close(), Read(), and Write() on the OSKERNEL.BIN file. The following code fragment builds a file path, opens the file, reads it into an allocated buffer, and closes the file.

Finding the OS Partition

The UEFI sample environment materializes a BLOCK_IO_PROTOCOL instance for every partition that is found in a system. An OS loader can search for OS partitions by looking at all the BLOCK_IO devices. The following code fragment uses LibLocateHandle() to get a list of BLOCK_IO device handles. These handles are then used to retrieve the first block from each one of these BLOCK_IO devices. The HandleProtocol() API is used to get the DEVICE_PATH_PROTOCOL and BLOCK_IO_PROTOCOL instances for each of the BLOCK_IO devices. The variable BlkIo is a handle to the BLOCK_IO device using the BLOCK_IO_PROTOCOL interface. At this point, a ReaddBlocks() call can be used to read the first block of a device. The sample OS loader just dumps the contents of the block to the display. A real OS loader would have to test each block read to see if it is a recognized partition. If a recognized partition is found, then the OS loader can implement a simple file system driver using the UEFI API ReadBlocks() function to load additional data from that partition.

Getting the Current System Configuration

The system configuration is available through the SystemTable data structure that is passed into the OS loader. The operating system loader is an UEFI application that is responsible for bridging the gap between the platform firmware and the operating system runtime. The System Table informs the loader of many things: the services available from the platform firmware (such as block and console services for loading the OS kernel binary from media and interacting with the user prior to the OS drivers are loaded, respectively) and access to industry standard tables like ACPI, SMBIOS, and so on. Five tables are available, and their structure and contents are described in the appropriate specifications.

Getting the Current Memory Map

One UEFI Library function can retrieve the memory map maintained by the UEFI environment. While the loader is running, the memory has been managed by the platform firmware. It has allocated memory for both firmware usage (boot services memory) and other memory that needs to persist into the OS runtime (runtime memory). Until the loader passes final control to the OS kernel and invokes Exit-BootServices(), the UEFI platform firmware manages the allocation of memory. The means by which the OS loader and other UEFI applications can ascertain the allocation of memory is via the memory map services. The following code fragment shows the use of this function to ascertain the memory map, and it displays the contents of the memory map. An OS loader must pay special attention to the MapKey parameter. Every time that the UEFI environment modifies the memory map that it maintains, the MapKey is incremented. An OS loader needs to pass the current memory map to the OS kernel. Depending on what functions the OS loader calls between the time the memory map is retrieved and the time that Exit-BootServices() is called, the memory map may be modified. In general, the OS loader should retrieve the memory map just before calling ExitBootServices(). If ExitBootServices() fails because the MapKey does not match, then the OS loader must get a new copy of the memory map and try again.

Getting Environment Variables

The following code fragment shows how to extract all the environment variables maintained by the UEFI environment. It uses the GetNextVariableName() API to walk the entire list.

Transitioning to an OS Kernel

A single call to ExitBootServices() terminates all the UEFI Boot Services that the UEFI environment provides. From that point on, only the UEFI Runtime Services may be used. Once this call is made, the OS loader needs to prepare for the transition to the OS kernel. It is assumed that the OS kernel has full control of the system and that only a few firmware functions are required by the OS kernel. These functions are the UEFI Runtime Services. The OS loader must pass the SystemTable to the OS kernel so that the OS kernel can make the Runtime Services calls. The exact mechanism that is used to transition from the OS loader to the OS kernel is implementationdependent. It is important to note that the OS loader could transition to the OS kernel prior to calling ExitBootServices(). In this case, the OS kernel would be responsible for calling ExitBootServices() before taking full control of the system.

Summary

This chapter has provided an overview of some common protocols and their demonstration via a sample operating system loader application. Given that UEFI has been primarily designed as an operating system loader environment, this is a key chapter for demonstrating the usage and capability of the UEFI service set.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset