windows无盘启动技术开发之UEFI(新一代BIOS)引导程序开发之一

                                                       by fanxiushu 2023-04-04 转载或引用请注明原始作者。
UEFI 是Unified Extensible Firmware Interface(统一可扩展接口)的简称。
UEFI这个名称比较陌生,但是提到BIOS,应该很熟悉,它其实就是BIOS的升级,
只是升级幅度一下从脚踏车窜升到了喷气式飞机。
但它的核心功能都一样,都是作为引导一个独立的大型的操作系统正常运行的一个前期基本系统。

BIOS(或者作为区别称作传统BIOS),是建立在很早前的硬件基础之上的,
那个时候基本都是8位,16位微机,各种硬件资源有限,但随着芯片技术的发展,尤其是64位芯片进入主流。
建立在16位芯片基础上的传统BIOS的各种限制就很明显的暴露出来。
比如支持硬盘容量有限,最大到2T,这是目前来说非常严重的缺陷,因为现在的硬盘容量早就超过这个限制。
还有就是开发困难,这或许在早期不算大问题,因为当时就是16位机器的天下,需求也不像现在那么多,
而现在如果再回到16位模式去开发代码,
去做大量的汇编代码工作,以及16位模式下的内存,CPU,各种外设硬件打交道,
同时还面临各种升级硬件的要求,这对BIOS开发人员来说,肯定是个噩梦。
(当然如果现在还得去维护两套代码,也是个噩梦)

UEFI的出现一定程度上解决了传统BIOS面临的问题。
传统BIOS一开机,进入的是 16位实模式,
而UEFI一开机进入的就是CPU当前的位模式。
比如64位的CPU,就直接进入64位模式,32位CPU,直接进入32模式。
不再出现传统BIOS那样的从16位实模式到保护模式的转换。

这显然给UEFI的开发带来极大的好处,我们可以像开发windows,linux那样的普通程序那样开发UEFI程序。
而实际上,也确实如此。
甚至在UEFI环境下运行的程序,有PE头,
而且还是windows使用的exe执行文件的PE头文件格式的一个子集:PE32+,
PE32+ 是windows系统64位程序专用的头格式。
而且我们可以使用 gcc 或者windows的 cl.exe 直接编译能在UEFI环境运行的程序。
而我开发UEFI无盘引启动导程序,确实是使用的 VS2015 编译的UEFI引导程序。
下面会进一步说明。

当然,我们别忘了UEFI和传统BIOS的核心目的:其实都是为了引导高级系统而存在的一个基本系统。
而UEFI的这个引导过程,却要丰富了许多。
除了传统的从本地硬盘引导,从U盘引导等内容外,网络引导的内容就更加丰富。
除了基本的PXE引导,UEFI还可以实现多种网络引导,甚至HTTP、HTTPS引导。
而 http/https引导,则让电脑直接从互联网中去远程引导系统变成可能。
虽然网络引导对网络带宽需求之高,让人大概率会止步,而如果网络带宽跟不上,系统基本也启动不了或十分缓慢。
但是如果作为引导一个小型系统,比如维护系统,像WINPE之类的,或者linux下的维护系统,或者其他小型系统。
这样的需求还是非常适合从internet互联网通过http等协议进行远程引导的。

本文讲述的引导程序,并不是为了引导某些小型系统的,而是引导windows这种系统的,
这里依然使用PXE引导,一是因为PXE肯定兼容各种情况,因为越简单发展越久的,兼容越好。
二是远程引导和运行windows这样的无盘系统,目前来看,也只能在局域网,而且要求带宽很高的环境中(千兆以上)。
并不适合上面提到的internet互联网环境,除非它提供的带宽确实足够高。

为了正常引导和运行无盘的windows系统,就如前面两篇关于传统BIOS引导程序开发的文章所阐述的那样。
我们不单需要开发UEFI环境下的引导程序,
还得开发windows的虚拟磁盘驱动。
因为windows从UEFI环境加载基本内核和驱动之后,就会全盘接管各种硬件,windows有自己的驱动来管理和使用这些硬件。
而这个时候UEFI环境下的绝大部分功能都会失效,无法再使用(除了Runtime Service),这个跟传统BIOS效果是一样的。
因此我们必须在windows的boot阶段,
实现虚拟磁盘驱动,同时保证网络通信正常,
这个 时候的网络通信也不再是UEFI环境下的网络,而是windows自己的NDIS驱动下的网络环境。

再次回到UEFI环境下的这个引导程序上来。
我们在前面两篇介绍传统BIOS引导程序的时候,讲述了开发传统BIOS引导程序的关键之处:
HOOK BIOS  INT13H 中断,替换中断服务函数。
文章中还说过,这等于是16位模式下的另类的虚拟磁盘驱动。

于是,在UEFI开发引导程序,基本上也是同样的开发思路:
在UEFI环境下,实现一个虚拟磁盘驱动,这个虚拟磁盘驱动通过网络传输定位到服务器上的系统镜像。
UEFI下的这个虚拟磁盘,会被当成一个正常的硬件,
再然后,从这个虚拟磁盘找到ESP分区,找到 \EFI\Boot\bootx64.efi引导文件,加载并且运行。
之后的事情,自然不用再插手,整个windows系统引导和运行进入正轨。

是不是很简单,比起windows下的驱动开发简单得多了。
当然前提是的熟悉UEFI,包括各种概念,什么protocol,BS,RS等内容。这些可以去阅读 UEFI的规范,
了解UEFI的各种接口和运行模式。

下面我们简单阐述UEFI开发环境。
(注:我这里是直接在VS2015中开发的,直接建立的sln工程,而并不是使用EDK2,当然得使用edk2的各种头文件)

各种资料都说的是如何在edk2环境下进行开发,
我折腾了一段时间之后,发现好像得把自己的工程和edk2合并到一起,
怎么说呢,就好像是以前那种嵌入式开发一样,需要把自己的代码和整个系统的源代码整合到一起。
而我习惯是把源码和自己创建的工程严格分开的,甚至都不会放到同一个盘符下。

有了这个强迫症,也就是不再继续折腾edk2,而且还有一堆的inf,dsc等配置文件,所以最后还不如拿vs2015开刀呢。
因为以前偶然发现vs2015工程配置还支持 EFI程序的配置,如下图那样:
windows无盘启动技术开发之UEFI(新一代BIOS)引导程序开发之一_第1张图片

当然,除了修改subsystem之外,还得修改好几处,比如 Entry Point 函数入口,不再是默认的 main或WinMain,需要自己指定入口函数。
反正是有好几处需要修改,具体可以根据编译efi程序报错情况进行纠正。
而且不能使用跟windows程序相关的任何库文件,必须忽略默认库,windows包含的默认头文件也不能使用,
简单的说,就是不能使用任何跟windows的exe程序相关的配置选项。

比如入口函数名为 EfiMain,而EfiMain的声明和参数如下:
EFI_STATUS EfiMain( EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable);

其中 EFI_SYSTEM_TABLE 结构的声明从哪里获取,自然得从edk2的头文件中查找,
因此可以直接从github下载edk2,然后解压到某个目录,直接把 edk2\MedPkt\Include 等目录添加进vs2015工程中。
这样头文件的问题解决了,但是edk2的lib文件我们无法使用,因此一些基本函数得自己去实现。
比如memcpy, strXXX函数,printf函数,等等。
况且我的目的是开发bootloader,并不是去开发非常复杂的efi程序,也用不着太多额外lib库文件。
这些都不难解决,比如上图的中string.c和vsprintf.c就是这些基本函数的实现代码,当然,是从别的地方copy的。
其中memcpy函数可以直接使用 BS中 CopyMem函数,封装一下,就成了 C代码库中的memcpy函数。
类似的 memset 也一样。

搭建好了vs2015工程环境,有了这些基础函数,以及关于UEFI基本知识,我们就能正常开发UEFI的程序了。

首先来了解入口参数结构:EFI_SYSTEM_TABLE, 定义如下:
 typedef struct {
  ///
  /// The table header for the EFI System Table.
  ///
  EFI_TABLE_HEADER                   Hdr;
  ///
  /// A pointer to a null terminated string that identifies the vendor
  /// that produces the system firmware for the platform.
  ///
  CHAR16                             *FirmwareVendor;
  ///
  /// A firmware vendor specific value that identifies the revision
  /// of the system firmware for the platform.
  ///
  UINT32                             FirmwareRevision;
  ///
  /// The handle for the active console input device. This handle must support
  /// EFI_SIMPLE_TEXT_INPUT_PROTOCOL and EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL.
  ///
  EFI_HANDLE                         ConsoleInHandle;
  ///
  /// A pointer to the EFI_SIMPLE_TEXT_INPUT_PROTOCOL interface that is
  /// associated with ConsoleInHandle.
  ///
  EFI_SIMPLE_TEXT_INPUT_PROTOCOL     *ConIn;
  ///
  /// The handle for the active console output device.
  ///
  EFI_HANDLE                         ConsoleOutHandle;
  ///
  /// A pointer to the EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL interface
  /// that is associated with ConsoleOutHandle.
  ///
  EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL    *ConOut;
  ///
  /// The handle for the active standard error console device.
  /// This handle must support the EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL.
  ///
  EFI_HANDLE                         StandardErrorHandle;
  ///
  /// A pointer to the EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL interface
  /// that is associated with StandardErrorHandle.
  ///
  EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL    *StdErr;
  ///
  /// A pointer to the EFI Runtime Services Table.
  ///
  EFI_RUNTIME_SERVICES               *RuntimeServices;
  ///
  /// A pointer to the EFI Boot Services Table.
  ///
  EFI_BOOT_SERVICES                  *BootServices;
  ///
  /// The number of system configuration tables in the buffer ConfigurationTable.
  ///
  UINTN                              NumberOfTableEntries;
  ///
  /// A pointer to the system configuration tables.
  /// The number of entries in the table is NumberOfTableEntries.
  ///
  EFI_CONFIGURATION_TABLE            *ConfigurationTable;
} EFI_SYSTEM_TABLE;

里边包含了所有我们需要使用到的接口函数。
其中 BootService就是我们主要需要使用的接口。
还比如 ConOut,这个把字符打印到屏幕上,
实际上 printf 函数的实现,最终就是调用 ConOut->OutputString 把字符串打印到屏幕上。
这里边的每个接口和每个函数的使用说明,在 intel提供的UEFI规范中,都有详细说明,
因此这里也就不再啰嗦的继续重复说明。

我们再来简单看看 UEFI到处都会出现的 PROTOCOL这个玩意。
它其实就是一个接口,interface, 每个protocol定义了一组函数(或者叫函数指针)和一些变量参数,
这跟C和C++代码中的struct或class的定义一致的。
如果熟悉windows的COM组件,那也对UEFI的PROTOCOL也有相似的感觉。
每个PROTOCOL就是一组函数的集合,为UEFI内核和各个EFI程序提供沟通的桥梁。
也就是说,我们的EFI程序可以生成protocol给别的EFI程序调用,同样的,我们的efi程序也可以调用别的efi程序提供的protocol。
我们也可以直接调用UEFI内核提供的基本服务, 比如BOOTSERVICE 或 RUNTIMESERVICE等。
比如EFI_SYSTEM_TABLE 结构体里边提供的各种函数接口。

下一章我们会继续讲述,如何开发UEFI环境下的虚拟磁盘驱动,
而且从上图中,细心点,就会发现,里边至少实现了两种途径的虚拟磁盘驱动,
一种是基于blockio的,block_init.cpp 代码实现,一种是基于scsi的,里边的 scsi_init.cpp代码
实际使用中,使用其中一种就可以了,我估计是闲的蛋疼才会把两个玩意一起实现了。
其实也不算是,因为一开始摸索中,想到我的windows的虚拟磁盘驱动都是基于scsi实现的,
自然到了UEFI,首先想到肯定也是SCSI。只是到了后来发现blockio也能满足需求,
于是乎,就把两个一起实现了。

未完待续。
 

 

你可能感兴趣的:(磁盘驱动,UEFI,windows,uefi引导程序)