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)引导程序开发之一的主要内容,如果未能解决你的问题,请参考以下文章