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

news/2024/12/2 20:36:22/

by fanxiushu 2023-04-14/18 转载或引用请注明原始作者。
接上文。

上文提到了至少有两种办法制作UEFI的虚拟磁盘驱动。
本文只接收利用BlockIO的方式来构造一个临时用的虚拟磁盘。
之所以说是临时使用,是因为在作为引导程序中,一旦进入到 \EFI\boot\bootx64.efi之后,
控制权就交给了bootx64.efi以及之后的引导程序,我们的引导程序唯一需要在幕后扮演的就是提供虚拟磁盘的IO功能。
这跟传统BIOS下实现大型系统引导的基本规则是一样的。

所以在开发引导程序的时候,不必按照UEFI规范文档那样阐述的,制作符合UEFI规范的UEFI驱动程序,
我们只需要在程序入口里调用 InstallMultipleProtocolInterfaces 函数安装新的BlockIO的PROTOCOL,
并且之后调用ConnectController 函数,让我们新安装的PROTOCOL能被连接上,
能被别的引导程序(其实就是 EFI\Boot\bootx64.efi 等其他引导程序)识别到。

所以在UEFI中制作这么一个临时的BlockIO接口就会显得比较轻松:
定义:
    EFI_BLOCK_IO_MEDIA              Media;
    EFI_BLOCK_IO_PROTOCOL     BlkIo;
    EFI_BLOCK_DEVICE_PATH     DevPath;
EFI_BLOCK_DEVICE_PATH是我们自己定义的继承自 EFI_DEVICE_PATH 的路径,可直接定义成 MSG_VENDOR_DP 的方式。
设备路径(EFI_DEVICE_PATH)是必须要的,否则其他efi程序无法正确定位。

定义以上结构之后,然后填写磁盘相关的信息,当然得首先从服务器获取磁盘相关的信息。
具体做法其实在以前阐述开发传统BIOS的引导程序的时候就介绍过,这里不再啰嗦,有兴趣可以去查阅。
填写  EFI_BLOCK_IO_PROTOCOL相关的回调函数,其中两个核心的函数:
    ReadBlocks,读磁盘扇区数据
    WriteBlocks,写磁盘扇区数据。
这两个回调函数就是我们需要实现的。
我们需要在回调函数中,把对磁盘的读写请求通过网络传输,定位到服务器端的磁盘镜像中。

填写好相关参数,就可以调用BootSErvice的InstallMultipleProtocolInterfaces 函数安装这个PROTOCOL了。
再然后做些其他相关的初始化操作。
完成之后,调用 BootService的ConnectController函数,让我们的PROTOCOL正常运作起来。
再之后,就需要通过 BootService的LocateHandleBuffer函数查询
gEfiSimpleFileSystemProtocolGuid(EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_GUID),
并进一步定位到属于我们刚才制作的BlockIO虚拟磁盘驱动里边的FAT文件系统。
然后调用 BS(BootService,以下都简称BS或bs)的 LoadImage 加载我们的虚拟磁盘驱动的 EFI\Boot\bootx64.efi
再然后,调用 BS的StartImage函数,将控制权交给bootx64.efi。
之后如果一切正常,StartImage 将不会返回,到UEFI退出舞台,被引导的系统运行起来。

接着我们来简单阐述一下网络通信部分。
本来按照正常思路,uefi是支持tcp,udp这些直接传输的。
也就是我们可以像使用类似bsd socket那样创建UDP的“套接口”。
但是实现了之后,才发现一个很大的问题,速度太慢。
举个例子吧,我是使用vmware虚拟机作为主要测试对象的。
以前开发的传统BIOS下的引导程序,16位下的引导程序,读取速度能达到 20-30MB(字节每秒),
也就相当于利用了千兆网卡 四分之一还多的带宽,其实在千兆网环境中,
单步读取(因为传统BIOS,UEFI等引导系统都是单CPU的,单步运行的,不存在多线程概念),
能达到 20-30MB这个速度,我还是很满意的,毕竟加载win10 前期阶段的数据,只需要3,4秒就完成了。
可是到了UEFI,直接使用UEFI提供的UDP方式传输,结果只能达到每秒 8MB-9MB左右的速度,
实在是差了两三倍。

先简单阐述UEFI下如何实现UDP通信,
正如前面在开发传统BIOS引导程序的时候说过的,整个过程都是使用UDP通信的,
因为在局域网环境中,丢包小的环境中,使用UDP反而更快,数据吞吐量更大,尤其是这种简单的一来一往的磁盘读写请求。
而且每个读写请求都包括偏移,数据长度等信息,根据这些信息,可以天然的保障读写数据不会丢失乱序(丢失了大不了重新发送请求)。

在UEFI中,首先创建一个UDP的句柄,用于通信:
调用bs的LocateProtocol定位gEfiUdp4ServiceBindingProtocolGuid(EFI_UDP4_SERVICE_BINDING_PROTOCOL_GUID),
获得 EFI_SERVICE_BINDING_PROTOCOL 之后,调用里边的CreateChild创建一个handle。
接着根据这个handle,再次调用LocateProtocol获取到 EFI_UDP4_PROTOCOL 指针,
这个EFI_UDP4_PROTOCOL里边存储的就是我们接下进行UDP传输的函数集合。

当然首先是调用 里边的Configure回调函数配置通信的本地地址和远端地址。
因为默认是么有配置的,所以很多时候,我们得手动配置本地地址。
而本地地址,我们可以再次使用DHCP协议发送请求,或者因为是PXE启动的,所以可以直接从缓存中查找。
显然从本地缓存中查找会更简单。
接着就是调用 Transmit 发送UDP数据包,调用Receive接收UDP数据包。

这就是简单的的UDP通信过程,但是正如上面所阐述的,实现了之后,才发现速度太慢。
一开始还以为是vmware虚拟机的问题,然后后来尝试在真机中测试,好像速度也快不到哪去。

也不太想去查找具体原因,反正调用UDP的办法UEFI规范中就是这么提供的,也没存在哪里优化不够的问题。
当然也不排除我上层定义的协议比较繁琐,但是在传统BIOS下都能达到20MB-30M的速度,
在windows甚至能达到100MB的速度,而在UEFI中表现惨淡,这就显得不太对了。

到后来回想起传统BIOS开发的引导程序速度为何比较快。那是因为在传统BIOS下,直接调用的底层接口,
直接自己封装的UDP数据包。
所以最后放弃使用UEFI的上层的UDP接口,转而寻找底层的接口。
结果找到了SNP(EFI_SIMPLE_NETWORK_PROTOCOL)。

SNP是直接对 UNDI的封装,但是已经足够接近底层。
于是利用这个接口,既然是底层接口,什么都得自己实现,包括ARP处理,如何封包UDP,
还好在开发传统BIOS引导程序的时候,就已经实现了一遍。所以算是在UEFI环境下重新开发了一遍。
然后这么折腾之后,速度终于达到了传统BIOS下的速度。

所以。。。UEFI的网络堆栈虽然灵活了许多,但是速度好像也打了折扣啊。。。
因为 UEFI的网络堆栈大致如下:
先是 UNDI,这个是网卡驱动接口,接着UNDI套上了唯一的SNP, SNP套上了唯一的NMP,
从NMP才开始分开各种协议的处理细节。比如 UDP归于UDP Protocol处理,TCP归于TCP PROTOCOL处理。
然后一层层往上,反正在整个过程中,速度有所降低也正常。毕竟套了这么多层,而且还是UEFI只能单步使用CPU情况下,
不存在多线程,只能依靠EVENT事件来达到异步的效果。

然后,这两天,有人提到一件事,说是类似无盘启动一类的系统,但具体又不严格是。
就是UEFI中虚拟磁盘驱动不从网络读取,而是读取本地磁盘中的镜像文件,
然后启动到windows或linux,也是读写对应的镜像文件。
也就是相当于本地磁盘不是用来安装操作系统的,而是存储一个一个的磁盘镜像文件的。
磁盘镜像文件里的才是操作系统。
也就是相当于从网络方式的无盘启动,转到本地磁盘的一个一个镜像文件中。

以前到没想到还能这么用,而且比起开发网络方式的无盘启动,还简单的多了。
当然,windows的虚拟磁盘驱动还是得自己开发,只不过读取数据不再是网络读取,
也不用考虑非常麻烦的各类网卡驱动在boot阶段运行的问题,
在boot阶段直接访问真实的硬盘镜像文件就可以了,这个时候NTFS等文件系统已经建立了。

然后开发UEFI的引导程序,替换掉 原来的 EFI\boot\bootx64.efi就可以,之后就能让整个系统按照读取镜像文件的方式运行。

这个应该是很早前就在使用的一类无盘启动了,只是最近才知道有这么一个应用。
虽然不知道这么个应用究竟有啥实际用处。
但是无疑让我对UEFI的引导类型又开了一次眼界。
这玩意还能这么用,感觉挺好玩的。故此简单写了出来。
 


http://www.ppmy.cn/news/45097.html

相关文章

双榜加冕!加速科技荣登2023准独角兽中国未来独角兽双榜单

4月10日至11日,由杭州市人民政府、民建浙江省委会、中国投资发展促进会主办的第7届万物生长大会在杭州国际博览中心隆重举行。会上,中国投资发展促进会创投专委会、杭州市创业投资协会联合微链共同发布2023杭州市独角兽(准独角兽)…

【架构设计】如何设计一个几十万在线用户弹幕系统

文章目录 一、前言二、项目介绍客户端轮询WebSocket主动推送 三、弹幕初始架构四、弹幕架构演进五、弹幕存储六、弹幕查询七、总结 一、前言 现在无论是直播还是电视剧,我们都可以看到上面慢慢的弹幕,满足十几万用户在线的弹幕系统,我们该如…

装饰模式的C++实现——设计模式学习(1)

编程目的:理解装饰模式及其用法。 装饰模式(Decorator),动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更为灵活。[DP] 装饰模式是一种结构型设计模式,它允许在运行时动态地给对…

【实用】解决.gitignore文件不生效

文章目录 背景原因解决方法 背景 针对:.gitignore文件不生效,哪怕是将某个目录、文件加入到忽略规则里 原因 .gitignore只能忽略那些原来没有被追踪的文件如果某些文件已经被纳入了版本管理中,那么修改.gitignore是无效的。 解决方法 先把本…

python实现将给定列表划分为元素和大致相等的两个子列表

假设现有列表[300,150,75,38,19,9,5,2],我想把它划分为两个子列表,并要求两个列表的元素和大致相等,应该如何做? 这是一个很有意思的问题,我们期待的答案应该是: list1: [300] list2: [150,75,38,19,9,5,…

如何让心情保持平静?100多条禅修心法

静的层次和阶段 静首先是不要去争,没有任何争的心,没有任何杂念心。静有几个层次阶段: ⒈. 自己的心情相对于自己平静,是平静的第一个阶段。 ⒉. 第二个平静的阶段是:别人觉得你很静,自己也很静&#xf…

面试题整理

1、new关键字干了什么? let a new Object() let a Object() let a {} 都会创建一个新对象 new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。 function Person(name) { // 构造函数Person()this.name name;// this.prototype Obj…

Python中的@cache巧妙用法

缓存是一种空间换时间的策略,缓存的设置可以提高计算机系统的性能,这篇文章主要介绍了Python中的cache巧妙用法,需要的朋友可以参考下 Python中的cache有什么妙用? 缓存是一种空间换时间的策略,缓存的设置可以提高计算机系统的性…