Windows CE .NET 和可扩展固件接口 (EFI)

什么是 EFI?

如果回想一下 DOS 编程的时代,您可能还记得使用软件中断进行 BIOS 调用。PC BIOS 是 PC 硬件的低级别抽象,这种方式提供了从显示 ASCII 字符到 RS-232 I/O 等范围广泛的服务。在此后的十年或更长时间里,在 PC BIOS 的体系结构方面,或在提供给应用程序和 OS 开发人员的服务方面,基本上没有多少更改。由于 Intel 最近提供的“可扩展固件接口”(EFI),所有这些已经改变。

EFI 是 Intel 为全新类型的 PC 固件的体系结构、接口和服务提出的建议标准。其主要目的是为了提供一组在 OS 加载之前(启动前)在所有平台上一致的、正确指定的启动服务。可以从 http://developer.intel.com/technology/efi 中“Specifications”超级链接下面下载 EFI 标准文档本身。EFI 的参考实现(包含用于 x86 处理器的源代码)也可以在相同 URL 的“Tools”超级链接下面得到。该参考实现运行在使用标准 BIOS 的现有旧式 PC 上,实际上可以构建为在重新启动 PC 时直接从软盘加载。这样,即使您的 PC 不支持 EFI,也可以开始目前的 EFI 实验。

这对 Windows CE 开发人员意味着什么?

目前,当您决定让您的 Microsoft Windows CE 平台基于嵌入式 PC 时,您一定非常希望保持 BIOS 不变,或者通过许可使用一个可提供增强服务的替代 BIOS(可能需要额外的费用)。您可能还是会选择该替代 BIOS,因为它使您能够访问源代码并且/或者会得到知识丰富的开发人员团体的良好支持。例如,如果将自定义 Windows CE 内核映像从硬盘加载到 RAM 中,可能还需要通过许可使用一份 MS DOS 来维护文件系统,以便将内核映像文件存储在硬盘上。这是在 Platform Builder 附带的 LOADCEPC.EXE 示例应用程序的实现中所使用的模型。

让我们感激的是,仅仅是为了初始化基于 PC 的 Windows CE 平台和加载内核映像就需要许可新的 BIOS 和/或一份 MSDOS 的情况不再是必需的了。“可扩展固件接口”名称中的关键词是“可扩展”,正是有了这种可扩展性才为添加驱动程序和应用程序模块提供了经过广泛证明的机制。与在桌面操作系统中一样,EFI 驱动程序使用定义的接口(或用 EFI 的说法称为“协议”)提供了一个在硬件之间的抽象层。应用程序只是能够访问由 EFI 定义的范围广大的各种所选协议的模块。

EFI 还有另外一个不是很直观、但却非常有用的目标。以我的经验看,专用于平台初始化的代码(在 Windows CE 中通常称为启动加载程序)包含了最低限度的体系结构,大多数是为嵌入式系统匆忙制作的组件。随着日程的进展,它通常处在“关键途径”上,并且经常被后期指派负责通常在使用原型硬件时所需要的平台诊断、闪存更新和其他任何内容。通常,时间只够大体实现这些功能,它们很少可用在未来的平台上。这正是 EFI 真正发挥作用的地方。虽然只是简短的介绍,但您不久将会看到,EFI 的构造非常精良,并且它附带了在没有功能完整的 OS 时无法期望的那种调试工具。

这回避了问题的实质,如果从 EFI Web 站点得到的参考实现不直接支持 Windows CE 平台,那么如何使用 EFI?EFI 在发布时带有使您能够移植到您自己的平台和目标处理器的源代码。本文不讨论这样做的过程,但这种做法的优点应当是清楚的。您可以在创建 Windows CE 启动加载程序时立即继承 C 源代码的牢固基础,以及一组设备驱动程序和工具,而所有这些在很大程度上都是独立于平台的。这意味着,随着平台连续升级换代,您可以重复利用以前的启动前软件投资,同时又能隔离任何对 EFI 设备驱动程序必需的更改。本文随后将从编写一个 EFI Windows CE OS 加载程序出发,将其作为一个示例,以用来探讨关于如何在 EFI 下面创建一个独立于平台、启动前、可重用的软件组件这一问题。

EFI 概述

EFI 服务划分为两个不同的阵营,一些只在 OS 加载之前可用,称为“启动服务”,另一些还会在 EFI 已经采用它的最低占用配置之后可用,这称为“运行时服务”。启动服务为平台配置、初始化、诊断、OS 内核映像加载和其他功能(数量太多,无法全部列出)提供了由 EFI 实现的范围广泛的各种功能。但是,运行时服务只是一组最少的服务,主要用来查询和更新永久性 EFI 设置。启动服务会消耗更多的资源,这取决于所选择的构建选项;而运行时服务消耗的资源非常少,这是为了尽可能地保持运行中的 OS 的透明性。

在 EFI 规范中,EFI 内的服务正式指定为核心服务和协议接口。用于访问多种启动设备的多种协议接口已经定义,其中很多接口都能在 EFI 参考实现中找到。其他协议接口为应用程序级函数提供服务,例如内存分配和获得对指定协议接口的访问权。

EFI 模块通常被定义为应用程序或驱动程序。驱动程序遵守在 EFI 规范中定义的模型,并被用于实现特定的协议接口。很多情况下,实现一个协议接口可能要使用或增强现有协议接口的功能,从而为称为包含和聚合的面向对象设计实践提供了一个机制。

对特定于平台的功能进行抽象的 EFI 驱动程序存在于 COREFW\fw\platform 源代码树中,并且链接到核心 EFI 映像。用于实现对某些硬件功能的功能性抽象的其他驱动程序则与特定于平台的驱动程序分开,位于 EDK 子目录中。它们遵守在 1.10 EFI 规范中定义的驱动程序模型(到编写本文时为止,仍在审阅中),并且应用于诸如 SCSI、视频、IDE、LAN、USB 等启动设备硬件类别。EDK 驱动程序被构建为扩展名为 .efi 的、独立的可执行文件,并且可以在 EFI 启动序列期间动态加载。

EFI 应用程序通常由启动管理器加载,并与它们在启动设备介质上的位置一起列在一系列永久变量中。也可以从参考实现中提供的 EFI 命令外壳来加载应用程序。因为应用程序没有通过协议接口公开它们的服务,所以应用程序模型略微简单一些。EFI 外壳和启动管理器是 Web 站点的参考实现中与源代码一起提供的两个 EFI 应用程序示例。

EFI 规范还定义了一个调试支持协议,以便允许在远程调试控制台上用 C 进行源代码级别调试。它支持实现第三方调试器。

在当前 EFI 规范草案 1.10 版中用超过 1000 页的篇幅,定义了本文无法详尽描述的很多功能。请访问 Intel Web 站点以下载该规范和当前的参考实现。表 1 提供了在开发应用程序和设备驱动程序模块时可用的函数列表的一部分。

表 1. 可用于实现 EFI 应用程序和设备驱动程序模块的函数(部分列表)

外壳服务

数学函数

Platform initialization functions

旋转锁定函数

Linked list support macros

处理和协议支持函数

String functions

文件 I/O 支持函数

Memory support functions

设备路径支持函数

CRC support functions

其他函数和宏

Text I/O functions

 

 

EFI 体系结构

图  1 描述了在使用 EFI 之前和之后的 Windows CE 启动加载程序的体系结构。目前的 Windows CE 启动加载程序是全部通过各种对象模块链接在一起的各种服务的整体的合成,每个服务均有唯一的一组 API。使用这些库创建的启动加载程序大部分是依赖于平台的,并且必须针对每个新的或修订后的平台进行更新或重写。很多情况下,不提供处理某些启动设备时所需的功能,或必须在制造商提供的参考源代码的基础上进行改写。

After 映像将 OS 加载程序描写成单独的模块,该模块能够访问 EFI 启动服务和库函数,以下载 Windows CE 内核映像、执行平台诊断以及任何特定于 Windows CE 内核启动序列(在 EFI 启动期间还没有执行)的必需的平台配置。驱动程序是用单独的协议接口很好地定义的,从而允许您在 EFI 参考实现中使用(或移植)现有的驱动程序,或者从硬件厂商那里获得一个支持 EFI 规范的、带有用于目标 CPU 的驱动程序的协议接口。

EFI 规范甚至定义了“字节代码虚拟机”,以便允许驱动程序和应用程序同时支持多个 CPU。Intel 随后将把称为 EFI Byte Code 编译器的特殊编译器作为产品提供,它允许以标准 C 源代码的形式提供的 EFI 驱动程序被编译为一种伪代码的形式,这样的代码将被为特定处理器实现的核心启动服务驱动程序解释。这实际上允许一次编写和编译的驱动程序能够在具有不同目标处理器的很多不同平台上运行。

 

图  1.  在使用  EFI  之前和之后的  Windows CE  启动加载程序体系结构

EFI OS 加载程序

EFI 中的 OS 加载程序是特殊类型的应用程序,简单地说,其目的是加载和激活 OS 映像。具体涉及哪些步骤将取决于内核映像是存储在闪存(或 ROM)中还是文件系统中,以及它是位于本地还是远程位置。图 2 包含的活动图描绘了从 PC 硬盘加载 Windows CE 内核映像时所需的步骤。我们将首先针对该图中的每个步骤来探讨示例 Windows CE OS 加载程序的示例源代码。源代码中对所有类型的 EFI 开发都通用的方面也将被注意到。

 

图  2. EFI OS  加载程序活动图

激活启动管理器

在自定义平台启动序列的某些点上,您可能想要向最终用户显示菜单。这将允许他们脱离立即加载 OS 映像的标准启动过程,以便允许执行其他 EFI 应用程序。在一个 EFI 映像的开发构建过程中,该菜单可以用来从一个可用内核映像的列表中进行选择,让列表中的每一项都有唯一的一组选项。还可以提供激活内部诊断或执行下载和重新刷新新的内核映像选项。在任何情况下,EFI 的“控制台 I/O 协议”(它是简单输入和简单文本输出协议的组合)都为基本用户界面提供了字符模式的文本输出和键盘输入。该接口具有足够的灵活性,允许使用远程屏幕和键盘,或作为 headless 设备的基本要求的物理按钮(如同 Windows CE .NET 设备上的 jog dial(一指通)或应用程序按钮)。(译者注:Headless 设备是指在本地不包含显示器、鼠标和键盘的设备。)

参考实现中提供的默认 Boot Manager 菜单是用存储在 NVRAM 中的数据来填充的。EFI 规范的“Firmware Boot Manager”节定义了该数据的格式,该格式允许指定应用程序、驱动程序、甚至 OS 映像。启动管理器使用“Variable Services”(一个接口,用于存储由唯一的 GUID 标识的配置数据块)访问 NVRAM 中的数据。Variable Services 的实现控制了在哪里实际存储数据,在EFI 参考实现中创建的是硬盘文件,而不是直接写入闪存或 EEROM。

要将示例 Windows CE OS 加载程序添加到默认的 Boot Manager,一个容易的方式是使用 EFI 命令外壳中提供的“bcfg”命令。示例 1 包含该命令的示例,其中 Windows CE OS 加载程序文件 Celoader.efi(位于第一个软盘驱动器的根目录“fs0:”)的条目将添加到 NVRAM(EFI 参考实现中的硬盘文件)。该命令中的“boot”选项指示应当将条目添加到启动管理器的选项菜单,同时数字 1 指定它应当作为菜单中的第一个条目出现。“Windows CE OS Loader”文本字符串指定该条目在菜单中应当如何显示。

示例  1.  将条目添加到  Boot Manager  的  Option  菜单

bcfg boot add 1 fs0:celoader.efi "Windows CE OS Loader"
 

命令行参数的语法分析

作为一种 EFI 应用程序的类型,OS 加载程序可以检查命令行,以确定调用方是否已经传递了可以用来修改加载内核映像的方式的任何参数。在这种情况下,调用方可以是启动管理器,也可以是从 EFI 命令外壳手动运行 Windows CE OS 加载程序 (Celoader.efi) 最终用户。您公开的参数可以包括任何或所有下列内容:

在进入 Windows CE 内核之前应当配置的初始视频模式。

用于 Windows CE 内核调试消息的 COM 端口及其比特率。

将 Windows CE 内核映像文件从外部介质(例如,硬盘分区和目录、压缩闪存卡等)加载到 RAM 中时该文件的位置。

位于示例 Windows CE OS 加载程序的 CELoaderParms.c 模块中的 ProcessCommandLineArgs 函数专门用于处理命令行参数和更新称为 BootArgs 的全局结构。该结构在 RAM 中的地址随后会复制到一个保留内存位置,并由 Windows CE 内核在其启动序列的早期进行处理。ProcessCommandLineArgs 函数的当前实现只是将 BootArgs 结构初始化为一系列硬编码的默认值,并且不会处理任何命令行参数。但是,示例 2 演示了如何使用“外壳接口协议”(Shell Interface Protocol) 来获得包含在全局变量 SI 中的 argv 和 argc 的值。InitializeShellApplication 函数执行对 SI 的初始化。

示例 2. 处理命令行参数
CHAR16 **gArgv;
UINTN    gArgc;

EFI_STATUS
InitializeApplication (
    IN EFI_HANDLE       ImageHandle,
    IN EFI_SYSTEM_TABLE *SystemTable
    )
{
    InitializeLib(ImageHandle, SystemTable);
    InitializeShellApplication(ImageHandle, SystemTable);

    gArgv = SI->Argv;
    gArgc = SI->Argc;

    // Begin parsing each of the arguments contained in the string array
  gArgv.
    // The number of arguments, if any, is contained in gArgc.
    .
    .
    .
 

定位内核映像

如果将 Windows CE 内核映像构建为在 RAM 中执行,则它必须存储在某种形式的永久介质中。该介质可以包括闪存(主板自带或来自压缩闪存卡)或通过网络连接下载的旋转介质(或 IDE 硬盘或 USB 和 IEEE 硬盘)。到编写本文时为止,EFI 参考实现通过指定的协议接口提供了对访问 IDE 和 SCSI 硬盘介质、闪存设备和 USB 设备的支持。在大多数情况下,您只需要在您的 OS 加载程序中关注将确定使用什么介质然后找到内核映像的代码的开发。

示例 3 假设内核映像位于某个硬盘分区的根目录。该代码首先获得所有受支持的固定介质设备的列表,并检查根目录中是否有 PathFileName 参数中所指定的内核映像文件。LibLocateHandle 库函数用来检索所有这些支持 BlockIoProtocol 协议的驱动程序的句柄。返回值存储在 HandleList 数组中,然后在随后的 for 循环中检查该数组是否支持 FileSystemProtocol 协议。当对 FileSystemProtocol 的请求最后成功时,将使用关联的 OpenVolume 服务获得文件 I/O 服务,随后用该服务尝试打开 PathFileName 变量中的文件。如果成功找到该文件,则把文件 I/O 服务的句柄返回给调用方。

注意,这里使用了 BS 变量来调用 HandleProcotol 函数。该变量是全局变量,其值先前在 OS 加载程序主入口点 InitializeOSLoader 中进行了初始化(通过调用 CeLoaderMain.c 模块中的 InitializeLib 函数),并且包含共同描述可用启动服务的函数的地址。EFI 使用全局变量和句柄来存储为每个协议定义的函数。使用一个句柄所引用的服务来访问由另一个句柄引用的服务是常见的操作。虽然术语句柄的使用不一致,但重要的是应当记住句柄既用作标识特定资源的键,也用作指向其中包含所指定服务(协议接口)的地址的结构的指针。

示例  3.  使用  EFI“BlockIoProtocol” 定位内核映像 

CELOADER_ERROR_CODES
  LocateKernelImage(IN CHAR16 *PathFileName, IN BOOLEAN Verbose, OUT
    EFI_FILE_HANDLE
    *OutFileIOHandle)
{
    Certain code omitted for brevity.  See original source for more 
      details...

    Status = LibLocateHandle(ByProtocol,
                             &BlockIoProtocol,
                             NULL,
                             &MaxHandles,
                             &HandleList

                             );

    if (EFI_ERROR(Status)) {
        Print(L"Error: Unable to obtain an array of BLOCK_IO handles.\n");
        return (CeLoaderErrorFileIOError);
    }

    for (HandleListIdx = 0; HandleListIdx < MaxHandles; HandleListIdx++) 
    {
        Status = BS->HandleProtocol(HandleList[HandleListIdx],
                                    &FileSystemProtocol,
                                    &FileSystem

                                    );

        if (EFI_ERROR(Status)) 
        {
            continue;
        }

        Status = FileSystem->OpenVolume(FileSystem, &FileIOHandle);

        if (EFI_ERROR(Status)) 
        {
            continue;
        }
        Status = FileIOHandle->Open(FileIOHandle,
                                    &FileHandle,
                                    PathFileName,
                                    EFI_FILE_MODE_READ,
                                    0
                                    );
        if (EFI_ERROR(Status))
        {
            continue;
        }
        else
        {
            *OutFileIOHandle = FileIOHandle;

            return (CeLoaderSuccessful);
        }
    }

如果 Windows CE 内核映像被构建为从闪存或 ROM 现场执行,那么 Windows CE OS 加载程序的角色将受到更多限制。在内核映像已经加载并且做好执行准备之后,您将首先执行 Windows CE 内核所要求的任何最后平台初始化。然后,您会确定内核映像的入口点地址,并跳到该地址继续执行(本文随后演示该过程)。

分配内存资源

如果正在开发一个与 EFI Windows CE OS 加载程序相比更传统的 Windows CE 启动加载程序,您会希望能够访问需要内存的任何内容而不会受到损失。例如,不会存在分配内存以便从硬盘文件加载 Windows CE 内核映像的需要,因为我们希望启动加载程序是唯一正在运行的程序。如果使用 EFI,则情况不再是这样。设备驱动程序和各种应用程序全都共享对内存资源的访问,这就需要使用内存分配函数。

在示例 4 中,内核映像所需要的内存大小是从 CeLoaderDiskImage 结构中检索得到的,该结构此前从位于内核映像文件开始的标头被初始化。在对 AllocatePages 的调用中使用了该值以及内核的物理开始地址。注意 EfiRuntimeServicesData 参数的使用。必须使用该参数才能确保当 EFI 为执行运行时操作而重新配置它自己时,内存不会 被释放和重新初始化(随后在“退出启动服务”节中讨论)。

示例  4.  为加载到  RAM  中的内核映像分配运行时内存 

static EFI_STATUS
AllocateImageMemory(IN BOOLEAN Verbose)
{
    EFI_STATUS Status;
    UINTN NumPages =
        (UINTN)
        ((CeLoaderDiskImage.PhysicalImageSize + GetSizeOfBootArgs()) 
            / EFI_PAGE_SIZE + 1);
    UINT64 LoadStartAddress = CeLoaderDiskImage.PhysicalStartAddress;

    //
    // Make sure the allocation uses the memory type of
      EfiRuntimeServicesData
    // to make sure that it is *not* reclaimed when calling
      ExitBootServices().
    //
    Status = 
        BS->AllocatePages(
                AllocateAddress, 
                EfiRuntimeServicesData,
                NumPages,
                &LoadStartAddress);

    if (EFI_ERROR(Status) || LoadStartAddress !=
      CeLoaderDiskImage.PhysicalStartAddress)
    {
        Print(L"Error in allocating memory at physical address 0x%X.");
    }

    //
    // Save the LoadStartAddress which in this case simply corresponds to
      the
    // physical address of the kernel image.  This might not be the case
    // if the kernel was loaded at one address then relocated later to its
    // physical address.
    //
    CeLoaderDiskImage.LoadStartAddress = (UINT32)LoadStartAddress;

    return (Status);
}  
 

加载内核映像

如果 Windows CE 内核映像文件被构建为从 RAM 运行,那么您必须从某种形式的外部介质加载映像文件。EFI 为访问 SCSI 和 IDE 硬盘、以太网、RS-232 串行端口、闪存和 USB 设备提供了驱动程序。提供这些驱动程序节约了大量开发时间,并允许您为在多种类型的介质上定位内核映像提供支持。这不仅对在重复的编译/生成/调试周期中需要频繁修改内核映像的开发期间有用,而且允许您的最终用户拥有更方便的内核映像字段更新机制。

示例 5 使用了调用方(通过 LocateKernelImage 函数)提供的文件 I/O 协议接口的句柄来打开内核映像文件。然后,签名、物理开始地址和物理映像的长度被读入 CeLoaderDiskImage 结构中,该结构随后将在 OsDiskImage 输出参数中返回给调用方。然后,在随后调用上一节讨论的 AllocateImageMemory 函数时将使用这些值。AllocateImageMemory 函数将请求在指定的地址为整个内核映像分配足够的 RAM。此后的 while 循环将把内核映像文件的每一节读入 RAM,直到到达地址为 0 的节(它表示内核入口点的地址)为止。同时,该节也表示已经读取所有节。

该函数不是特别复杂,但它真的有效地展示了 EFI 在简化以其他方式就会很复杂的过程方面所具有的强大功能。在表象的后面,我们实际上通过硬盘的 IDE 接口访问了硬盘,并从硬盘介质的正确扇区读取数据,而这些操作由基本文件系统进行管理。这些操作实际上在 Windows CE 启动加载程序中发生,这样您就不需要通过数月的工作从头、甚至从公共域中的源代码库开发该功能。

示例  5.  从文件系统加载  Windows CE  内核映像 

CELOADER_ERROR_CODES
LoadDiskImage(
    IN EFI_HANDLE ImageHandle,
    IN EFI_FILE_HANDLE FileIOHandle,
    IN CHAR16 *FileName, 
    IN BOOLEAN Verbose,
    OUT CELOADER_OS_DISK_IMAGE **OsDiskImage
    )
{
    Certain code omitted for brevity.  See original source for more 
      details...

    CeLoaderDiskImage.Verbose = Verbose;
    CeLoaderDiskImage.FileName = FileName;

    // 
    // Open the file using the File I/O interface
    // passed in by the caller.
    //
    Status = FileIOHandle->Open(FileIOHandle,
                                &FileHandle,
                                FileName,
                                EFI_FILE_MODE_READ,
                                0
                                );
    //
    // Read initial signature and physical start and size of CE OS kernel 
      image.
    //
    ReadSize = sizeof(Signature) + 2 * sizeof(UINT32);

    Status = FileHandle->Read(FileHandle,
                              &ReadSize,
                              &Buffer

                              );

    // 
    // Initialize the physical starting address and the total size
    // of the image from the OS kernel image signature bytes.
    //
    CeLoaderDiskImage.PhysicalStartAddress = 
        *(UINT32 *)&Buffer[sizeof(Signature)];
    CeLoaderDiskImage.PhysicalImageSize = 
        *(UINT32 *)&Buffer[sizeof(Signature) + sizeof(UINT32)];
    //
    // Now that the CeLoaderDiskImage structure contains sufficient 
      information to 
    // describe the physical memory requirements of the kernel image, 
      allocate the 
    // required memory as an EFI runtime memory region.
    //
    Status = AllocateImageMemory(Verbose);

    // 
    // Initialize the memory region occupied by the OS kernel image.
    //
    SetMem((VOID *)CeLoaderDiskImage.PhysicalStartAddress, 
        CeLoaderDiskImage.PhysicalImageSize, 0);

    //
    // Copy OS kernel image one section at a time into physical memory.
    //
    while (TRUE)
    {
        Status = FileHandle->Read(FileHandle,
                                  &SectionHeaderSize,
                                  Buffer);

        SectionAddress = *(UINT32 *)&Buffer[0];
        SectionSize = *(UINT32 *)&Buffer[sizeof(UINT32)];
        SectionChecksum = *(UINT32 *)&Buffer[sizeof(UINT32) + 
          sizeof(UINT32)];

        //
        // If the section address is 0 then consider this the last section
          and
        // use the section size as the entry point address for the OS
          kernel image.
        //
        if (SectionAddress == 0)
        {
            CeLoaderDiskImage.EntryPoint = (UINT32)SectionSize;

            break;
        }

        //
        // Verify that the section address is valid by comparing it to the
        // upper and lower bounds of the physical memory region.
        //
        if (SectionAddress < CeLoaderDiskImage.PhysicalStartAddress ||
               (SectionAddress + SectionSize) >
               (CeLoaderDiskImage.PhysicalStartAddress + 
                CeLoaderDiskImage.PhysicalImageSize))
        {
            Print(
                L"Image section doesn't fit physical image memory.\n"
                L"OS kernel image memory at 0x%X, size = %d\n"
                L"Section physical start = 0x%X, size = %d\n",
                CeLoaderDiskImage.PhysicalStartAddress, 
                CeLoaderDiskImage.PhysicalImageSize,
                SectionAddress, SectionSize);

            return (CeLoaderErrorImageSectionOutOfBounds);
        }

        //
        // We have already verified that physical memory is available so
          go
        // ahead and read the section of the OS kernel image file directly
        // into its destination address.  
        //
        Status = FileHandle->Read(FileHandle,
                                  (UINT32 *)&SectionSize,
                                  (VOID *)SectionAddress);
    }

    //
    // Close OS kernel image file. 
    //
    Status = FileHandle->Close(FileHandle);

    // 
    // Provide the caller with a reference to the OsDiskImage structure
    // in the OUT parm, which is now populated with information on the
    // kernel image.
    //
    *OsDiskImage = &CeLoaderDiskImage;

    return (CeLoaderSuccessfulLoad);
}
 

重新分配内核参数并为内核配置平台

启动时,Windows CE 内核会检查一个使用预定义格式的结构(在示例 Windows CE OS 加载程序源代码中表示为 BOOT_ARGS)中的值。该结构中的地址必须被复制到 RAM 中的保留位置,Microsoft 为每个平台定义了该位置。对于 CEPC 平台,该地址的值包含在 BOOT_ARG_PTR_LOCATION 常量中,该常量与在示例 Windows CE OS 加载程序中使用的常量具有相同的名称。

在进入 Windows CE 内核之前,您可能还需要对平台执行某些类型的最后配置。对于 CEPC 平台来说,这包括将视频控制器设置到将被 Windows CE 内核使用的模式。考虑到每个平台的最后配置需求是不同的,用来更改视频模式的代码被放置在称为 CeHal 的驱动程序中。该驱动程序实现了一个协议接口,该协议接口是为本文演示 Windows CE OS 加载程序应当如何执行最后的平台配置而专门定义的。同样,它只由一个入口点组成,该入口点用于使用旧式 Int10 BIOS 调用来更改当前的视频模式。

示例 6 包含示例 OS 加载程序源代码中的 CeLoaderVideoInit.c 模块的摘录内容。该函数用来设置调用方所请求的视频模式。函数 LibLocateProtocol 用来获得保存在 CeHal 变量中的协议接口的指针。然后调用 cehal 驱动程序中的 SetVideoMode 函数,调用时第一个参数是协议接口指针;该指针充当了正在使用的驱动程序实例的句柄,这类似于 C++ 编译器透明地使用“this”指针。

示例  6.  从  Windows CE OS  加载程序 “Celoader” 调用 “Cehal” 协议接口

CELOADER_ERROR_CODES
CeLoaderSetVideoMode(INTN Mode)
{
    EFI_CE_HAL_PROTOCOL *CeHal;     
    EFI_STATUS Status;

    Status = LibLocateProtocol(&gEfiCeHalProtocolGuid, &CeHal);

    if (EFI_ERROR(Status)) 
    {
        Print(L"Unable to retrieve the CeHalProtocol for ImageHandle\n");
        return (CeLoaderErrorGeneral);
    }

    CeHal->SetVideoMode(CeHal, Mode);

    return (CeLoaderSuccessful);
}

应当注意 int10 调用的使用肯定是特定于平台的。这是创建单独模块的主要原因。正如与示例 OS 加载程序源代码一起提供的 ReadMe.txt 文件中的描述,cehal 模块将作为 corefw\fw\platform\cepc\driversEFI 源代码树(而不是 celoader 本身所在的 apps 目录树)中特定于平台的驱动程序被添加到 EFI 内部版本中。另一个选择可能是使用不同的 .efi 可执行文件将 cehal 驱动程序作为独立的模块进行开发(与 EDK 源代码树中的驱动程序一样)。而这是不可能的,因为用来进行 int10 调用的服务是特定于平台的,这些服务只能被链接到主 EFI 映像的驱动程序使用。

在某些 EFI 初始化函数中只需要很少的代码更改即可加载 cehal 驱动程序。让 EFI 正确加载驱动程序的较好方法是使用“bcfg driver”命令,但由于 cehal 是平台驱动程序,因此这是不可能的。示例 7 显示了对 EFI 源文件的修改。

示例  7.  加载平台驱动程序所必需的  EFI  源文件修改 

Major portions of code omitted for brevity.  See original source for more 
  details...

File: COREFW\fw\platform\BuildTip\bios32\init.c

POST_CODE(0x1b);
// JYW-CELoader:  Load Windows CE Hal Services driver.
    LOAD_INTERNAL_BS_DRIVER (L"CeHal",
      CeHalDriverEntryPoint);

File: COREFW\fw\platform\BuildTip\inc\drivers.h

// JYW-CeLoader: Add prototype of the CeHal driver entry point to allow
  the EFI
// core firmware to load this driver through the LOAD_INTERNAL_BS_DRIVER
  macro.

EFI_STATUS
CeHalDriverEntryPoint(
  IN EFI_HANDLE         ImageHandle,
  IN EFI_SYSTEM_TABLE   *SystemTable
  );
 

退出启动服务和启动内核

退出启动服务几乎是 Windows CE OS 加载程序所需的最后步骤。正如示例 8 的描写,调用 ExitBootServices 之前必须先调用 LibMemoryMap 来更新内存映射,然后 EFI 才能进入内存占用量减少的运行时配置。调用 ExitBootServices 之后,只能调用 EFI 运行时服务,这意味着您可能很想插入的、用于声明即将激活 Windows CE 内核的任何诊断消息都必须在调用 ExitBootServices 之前插入。最后,调用 LaunchKernel 以便进入 Windows CE 内核。

示例  8.  调用  ExitBootServices  和进入  Windows CE  内核 

Portions of code omitted for brevity.  See original source for more
  details...
    //
    // Get the most current memory map.
    //
    MemoryMap = LibMemoryMap(&NoEntries, &MapKey, &DescriptorSize, 
  &DescriptorVersion);

    //
    // Transition from Boot Services to Run Time Services.
    //
    BS->ExitBootServices(ImageHandle, MapKey);

    //
    // Vector to the entry point for the CE kernel.
    //
    LaunchKernel(VerboseStatus);
 

小结

当我第一次遇到 EFI 时,我对所见内容就非常兴奋。在编写 Windows CE 启动加载程序之前,我在很多年里一直在开发应用程序,我总是忘记那种小心地施加在为使用综合库(例如,Microsoft 基础类 (MFC) 库或活动模板库 (ATL))而编写的源代码上的顺序。虽然 EFI 与应用程序级别的库有很大的不同,但可以将它比作软件工程中的一项进步。虽然这听起来可能有点过于夸大的成分,但我还是喜欢将 EFI 当作为启动前的混乱建立秩序的有力工具。

转到原英文页面

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值