windows无盘启动技术开发之传统BIOS(Legacy BIOS)引导程序开发之一

Posted 雨中风华

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了windows无盘启动技术开发之传统BIOS(Legacy BIOS)引导程序开发之一相关的知识,希望对你有一定的参考价值。

by fanxiushu 2023-03-01 转载或引用请注明原始作者。

这个话题可能有点老,UEFI Bios 已经大量存在,而Legacy BIOS最终会被取代。

但是也是作为无盘启动技术里不可或缺的,毕竟还有许多老型号的电脑存在,

而且为了兼容性,有些新的电脑主板还保留着Legacy BIOS。

开始之前,我们需要预备一些知识,

首先是对16位程序的开发知识,那是属于DOS时代的基本知识。

新一代程序员对这个应该并不清楚,我了解得也不多。就是很早以前接触过 TurboC,学习时候使用的。

因此也多多少少了解一些概念,但仅此而已。

想想有些感概,仅仅二三十年,从简陋的命令行,到复杂图像界面,到手机,平板,各种终端,影响和深入到各行各业,

从无联网的单机,到复杂的各种各样的网络,从0娱乐,到各种各样的极限娱乐。

简单的说,从无到有,都尽情显示着人类这个智慧生物无穷的创造力。

略微有些遗憾的是,这中间中国的参与度不高,大部分都是被动接受,当然被动的总比不接受的好。

当然有些超纲的,目前也无法解决。

比如无法达到超光速信号通信,更无法达到光速航行,或者超光速航行。

这应该是人类脱离地球,在宇宙间任意翱翔的基础,

而以现在的理论基础,好像几乎是不可行的,但愿有天能推翻现有理论,找到超光速航行的办法。

但也有可能在还没找到解决办法的时候,就会迎来地球生物的灭顶之灾,

比如环境恶化,地球冰期到来,大陨石撞向地球。。。,

地球迎来现有文明的灭绝,以及多个亿年后。。。可能的下一波新生文明。。。

扯远了,回到正题。

除了具备16位程序开发基础知识外,还需要了解 PXE开发知识,以及BIOS中断,其实主要是 INT 13H 磁盘中断。

PXE系统规范手册可以去intel官网搜寻。

作为开发,可以去借鉴现有的一些开源代码,比如最齐全的 ipxe,还有一些零散的。

查看开源代码,这是了解PXE接口的调用办法最快的途径。

无盘启动,顾名思义,就是系统在没有硬盘的情况下启动和运行。

其实并不是没有硬盘,而是硬盘不在本地,是通过网络连接,远在服务器上。

既然如此,那就更简单了,专门做这么一个硬件,在硬件底层上把硬盘延长了。

确实有这样的硬件,把网卡和硬盘SCSI HBA集成到一起,

对上表现为一块硬盘SCSI HBA硬盘管理器,对下则是通过以太网传输硬盘数据。

但是本文并不阐述这样的硬件设备,而是在普通网卡上,通过软件方式,实现硬盘的 “延长” 功能。

当然,软件实现也必须得到硬件支持:需要BIOS具备从网卡启动功能,需要网卡支持远程启动。

这种硬件支持,很早前就具备,BIOS有网卡启动选项,

网卡通过PXE规范支持常规系统还没运行起来时候的网络数据传输。

对于 Legacy BIOS启动过程(至于UEFI BIOS引导启动,以后有机会再阐述):

首先是BIOS上电自检,主要是查看各种连接上来的基础设备是否正常。

包括,CPU,640K基础内存,1M以上的扩展内存,显卡,键盘等等。

以及一些初始化工作后,

lagacy BIOS开始把引导权力交给引导程序,

BIOS根据配置选项,

如果是从本地硬盘引导的话,则读取磁盘的第一个扇区的MBR记录,

并把它放到内存 0000:7C00, 然后BIOS把CPU指令跳转到这个地址,之后就退居幕后,引导权交给MBR。

至于为何偏偏是这么个奇怪的地址,我想应该是跟最早的硬件设备有关,后来应该是为了兼容性性,保留这么一个地址。

后面我们会看到,引导程序基本都是放到这个地址去执行的。

MBR是一个扇区,总共512字节,前446字节是简单的引导程序,其后是4个16个字节的磁盘分区表,最后两个是结束标志 55AA。

前446字节里就是一段程序代码,它不同于我们linux,windows上的程序,它没有复杂的PE头,不需要额外解析器。

它简单到,把CPU指令指针放上去就能执行的那种裸机代码,纯粹的汇编形式的代码。

这也是我们接下来需要开发的引导程序的样子。

MBR引导程序检测4个分区表是否完善,接着找到活动分区,然后把活动分区的第一个扇区数据读入到 0000:7C00,

然后MBR把引导权交给活动分区的第一个扇区。

这个扇区的内容就跟具体的操作系统有关了,比如 winxp 操作系统,这个扇区会读取 ntldr 引导程序,

活动分区的第一个扇区也只有512字节,它也不大可能在这么小的内容上建立一个文件系统,

因此winxp操作系统应该是把 ntldr 文件复制到固定扇区中,这样这个引导扇区就能简单读取固定扇区,从而完整加载ntldr。

之后引导权再次转交给ntldr,ntldr首先读取boot.ini配置文件,建立起一个简单的文件系统,

NTFS或FAT32,为接下来读取各种配置文件和系统文件做准备。

ntldr做完各种其他操作之后,加载需要的基础组件,

更主要的就是加载驱动,属于boot阶段能启动的驱动,ntldr通过读取注册表各种服务,

找到start=0,也就是在boot阶段执行的驱动,按照注册表中指定的执行顺序加载这些驱动,

按照

HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\ServiceGroupOrder

HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\GroupOrderList

注册表中指定的group order和每个group里单个驱动的顺序。

直到这些驱动全加载到内存,ntldr整个过程都是在16位的实模式下运行,

根据我的测试,winxp会在实模式下读取至少30M多的数据, 而win7会读取 70多M,到了win10,则会读取80多M,

这个数据相对而言还是比较大,这也是考验我们即将开发的引导程序对PXE读写速度的优化情况。

ntldr把这些需要执行的基础组件,各种驱动,全部加载到内存之后,

接下来,就会把执行权力交给winxp系统,winxp会切换到64位或32位的保护模式,开始windows系统的真正启动过程。

这个时候,我们就会在显示器中看到四个花一样的图像慢慢出现,这个过程就是winxp系统执行基础组件,运行各种驱动的过程。

这其中,windows的磁盘驱动也在boot阶段启动,这样等于是windows接管了 BIOS INT13H对磁盘的读写,

因为后面的windows阶段需要大量的读写磁盘。

再然后,winxp执行完boot阶段的驱动之后,进入系统的IoInit系统初始化阶段,加载和执行的驱动更多。

系统初始化完成,接着进入windows的Logon登录阶段,至此,windows系统的启动过程顺利完成。

至于win7和win10在legacy BIOS下的启动过程基本类似,

除了有一点不同,

第一个扇区读取的是bootmgr,bootmgr再读取 system32\\Boot\\winload.exe,接着再来执行后面的驱动加载等过程。

以上讲了这些windows的启动过程,当然,这里阐述的是非常笼统的过程。

但是这对于我们开发引导程序以及后面windows下用于启动的虚拟磁盘驱动都有一定帮助。

讲了BIOS通过本地磁盘的启动方式,再来看看BIOS把引导权交给网卡的情况。

网卡本身有个ROM,存储的就是一些基本代码,包括引导代码,PXE相关等,

当BIOS把网卡的引导代码加载到内存,并交权给网卡引导代码之后,

网卡引导代码首先会发送DHCP广播到局域网。

这个DHCP协议与普通的DHCP稍微有些区别,但总体协议格式基本一样。

它主要不同之处,除了获取到动态IP地址之外,还需要获取引导文件名,以及TFTP服务器地址。

我们可以使用现成的dhcp服务器软件,也可以在掌握了DHCP协议规范之后,

自己实现,反正也不是难事,几百行C代码就能搞定。

在网卡获取到分配给自己的IP地址之后,同时获取到了TFTP地址,以及一个引导文件名。

在这之后,网卡开始通过 TFTP协议下载这个引导文件。

这里又要牵涉到另一个TFTP协议,一样的,在掌握了TFTP协议之后,

TFTP服务端同样可以自己实现,几百行C代码也能搞定。

我记得在CSDN上还专门记录过TFTP代码:

https://blog.csdn.net/fanxiushu/article/details/11900125?spm=1001.2014.3001.5501

网卡下载完引导文件,并且放到0000:7C00地址,之后CPU跳转到这个地址,开始执行这个引导程序。

而这个引导程序,就是我们即将需要开发的。

但是,如何让这个引导程序,和我们的无盘启动计划联系起来!

这就需要再次回顾上面阐述的从BIOS上电自检,到windows启动的全部过程。尤其是在16位的实模式下,

各种引导程序运行到交权给下一个引导程序,

不管是从MBR开始,到活动分区第一个扇区,再到ntldr亦或者是bootmgr,

这其中都会牵涉到磁盘扇区的读取,实模式下,不论是哪个程序,都是通过 BIOS 的 INT 13H中断读取的磁盘扇区数据。

如果熟悉16位编程,多少都会了解到,这些INT中断,当发生时候,CPU都会跑去特定的中断服务列表入口,执行特定的中断服务函数。

而中断服务列表,是BIOS初始化之后,就会在内存建立起来的一个地址列表。

而如果,我们把 13H中断的中断服务函数给替换掉,替换成我们的常驻内存的函数入口,

而这个函数则通过PXE通信,把磁盘读写请求重定向到服务器端。

这样不管是 MBR引导程序,ntldr,bootmgr等,通过INT 13H读写磁盘的时候,其实是通过我们的函数重定向到了服务器端。

这个就是我们开发引导程序的最主要和核心的功能。

其实,从另一个角度去理解,这就是一个在16位的实模式下另类的虚拟磁盘而已。

当然,光实现实模式下的虚拟磁盘还不够,上面windows启动过程中,当windows转到保护模式运行后,

windows就已经全部接管了所有硬件,包括磁盘硬件。因此还必须有对应的windows下的虚拟磁盘驱动,

而且根据启动过程分析,这个虚拟磁盘驱动,还必须在boot阶段就得启动起来,并且正确跟网络通信,获取服务器磁盘数据。

因此在这个windows虚拟磁盘加载之前,网卡的驱动必须先加载,并且需要网卡成功联网通信,

因为有些网卡驱动虽然加载了,但网卡硬件却处于准备状态。

这个也是最难搞的之一,因为不同网卡的windows驱动各具特色。

这些都是以后有机会再阐述的内容,

这里我们重点关注的是,16位实模式下的这个具有“虚拟磁盘”功能的引导程序的开发过程。

我们先来看一段汇编代码,

这是我大概10年前实现的,只是最近整理修改,主要是为了提高PXE通信速度。

而且是我整个引导程序中的唯一使用汇编写的代码,其他部分都是C/C++代码,

因为这是引导程序入口,没办法,只能使用汇编。

.model TINY

;------------------------------------------------------------

.386 ; CPU type

;------------------------------------------------------------

;.model TINY ; memory of model

;---------------------- C函数 -----------------------------

extrn _x7c00_Init:near ;

extrn _Int13HookEntry:near ; 申明C入口函数

extrn _PxeIsrEntry:near ;

;------------------------------------------------------------

;----------------

;------------------------------------------------------------

.code

org 0000h;; ; 0偏移对齐

;----------------------- CODE SEGMENT -----------------------

start:

cli

xor bx,bx

mov ss,bx ;初始化堆栈段

mov ss:[7C00h-2], sp ;把sp压入堆栈

mov sp, (7C00h-2)

push ds

pusha

mov ds, bx ;把DS段设置成 0

;;;调用C函数初始化

call _x7c00_Init

;;;;预留20k的数据到系统的保留内存,防止他被覆盖

mov ax, ds:[0413h]

and al, NOT 3

sub ax, 32 ;;;;修改BIOS数据区,单位KB,预留 32KB

mov ds:[0413h], ax

;sub word ptr ds:[0413h], 32 ;;;修改BIOS数据区,单位KB,预留 32KB

;mov ax, ds:[0413h]

shl ax,(10-4) ;;;获得预留的目标段

mov es, ax ;;设置正确目标段所在位置

;;;;;把自己复制到 预留内存区

cld

mov si,7C00h

xor di,di

mov cx,8000h/2 ;;复制 32K大小的数据

rep movsw

;;;;;替换 13h 中断服务程序

mov word ptr ds:[13h*4 ], int13hook

mov word ptr ds:[(13h*4) +2 ], es

;;;;;;;;;;替换 15h 中断服务程序,主要是拦截处理 e820功能

mov eax, dword ptr ds:[ 15h*4 ]

mov dword ptr es:[old_int15hook], eax ;;保存原来的地址

mov word ptr ds:[ 15h*4 ], int15hook

mov word ptr ds:[ 15h*4 +2 ], es

;;;;跳转到新地址执行

push es

push bootfromnetdisk

retf

bootfromnetdisk:

sti

;;;清屏设置显示模式

mov ax,0002h

int 10h

mov es, cx ; CX=0 from rep stosw

mov ax, 0201h ; Al=number of sector Ah=2 read sector

inc cx ; CH = cylinder; CL = sector and high bits of cylinder

mov dx, 0080h ; DH = head; DL = drive number

mov bx, 7C00h ; ES:BX dest buffer

int 13h

jc bootfromnetdisk

popa

pop ds

pop sp

db 0EAh ; JMP FAR 0000:7C00H

dw 7C00h, 0000h

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

int13hook:

;;;按照 int13_entry结构压入参数

pushf ;;保存标志

cli ;;;此操作会影响标志

push ss

push es

push ds

push cs

push di

push si

push dx

push cx

push bx

push ax

;;参数far地址压入堆栈

mov ax, sp ; int13_entry参数offset地址

push ss ;参数段地址

push ax

sti

;;;

call _Int13HookEntry ;;调用C函数

;;;;

cli

add sp, 4 ;;弹出参数far地址

;;;;;;;;

pop ax

pop bx

pop cx

pop dx

pop si

pop di

add sp, 2 ;;;pop cs

pop ds

pop es

pop ss

popf

;;;;

retf 2 ;;返回这里不恢复flags,而是直接 sp +2

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

以上代码大概意思是,在引导程序被加载到0000:7C00之后,

执行 x7c00_Init 初始化,这是个C函数,在里边其实主要是初始化PXE,获取PXE接口等。

然后把整个引导程序复制到 BIOS的0413保留区,这样在防止被后面的程序覆盖,因为我们是常驻程序,

是需要提供13H中断服务的,代码中预留的保留区有点大,32KB,

这个根据自己的引导程序大小可以动态计算,但是为了简单,这里就这么粗暴的预留了32KB。

接着就是重头戏,替换13H中断服务函数,替换成 int13hook,

而int13hook在经过一些列压栈,最终调用C函数 Int13HookEntry,

Int13HookEntry则是整个的核心,它完成磁盘所有功能,通过PXE把磁盘读写发送到服务器端。

完成这些初始化之后,接着让程序直接转到 bootfromnetdisk,

这个bootfromnetdisk过程其实就是直接通过INT 13H,读取MBR到 0000:7C00,

但是这个时候13H中断已经被我们替换,它最终会进入到 Int13HookEntry函数,

因此最终读取服务器上的磁盘数据,

未完待续,下面会接着讲述这个引导程序的磁盘开发部分以及PXE通信过程。。。

以上是关于windows无盘启动技术开发之传统BIOS(Legacy BIOS)引导程序开发之一的主要内容,如果未能解决你的问题,请参考以下文章

win10安全引导是啥意思?

PXE网络装机

W10系统进BIOS怎么关闭UEFI启动方式,改为pe环境启动?

第四节:Windows系统安装时BIOS设置及注意

电脑bios只有efi模式怎么办

window BIOS设置硬盘启动模式