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

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

接上文,

这篇文章其实主要就是讲述上文中 Int13HookEntry 这个C函数的实现过程,

看起来就一个函数,可实现起来一点也不轻松。

首先得准备编译环境,因为是16位程序,所以得去找以前那种比较老的编译器,比如turboC,

当然,如果在linux下找到合适的16位编译环境,也是可以的。

传统BIOS的引导程序并不是windows的一部分,它可以在其他任何能支持16位交叉编译的操作系统上编译。

从上文的asm汇编代码片段,在调用 Int13HookEntry 函数之前,压栈了一堆的寄存器,ax,bx。。。cs,ds等等。

而在C代码中,总归是需要换算成对应的结构体,

如下,就是上文汇编代码压栈对应的结构体描述:

///INT13 的入口参数

struct int13_entry

{

/// AX,BX,CX,DX register

union{

struct {

UINT8 al;

UINT8 ah;

};

UINT16 ax;

};

union{

struct {

UINT8 bl;

UINT8 bh;

};

UINT16 bx;

};

union{

struct {

UINT8 cl;

UINT8 ch;

};

UINT16 cx;

};

union{

struct {

UINT8 dl;

UINT8 dh;

};

UINT16 dx;

};

///SI,DI, CS, DS,ES SS

UINT16 si;

UINT16 di;

//

UINT16 cs;

UINT16 ds;

UINT16 es;

UINT16 ss;

///

UINT16 flags;//标志寄存器

};

看起来似乎有点多,其实就是各种寄存器,还包括段寄存器。

而C函数的声明对应如下所示:

extern "C" void Int13HookEntry( int13_entry far* reg );

是 far 指针,也就是UINT32来描述地址,高16位是段地址,低16位是偏移地址。

在讲述 Int13HookEntry之前,我们先来看看另外一个在上文中出现的C函数x7c00_Init,

它其实就是初始化 PXE,为 Int13HookEntry 函数进行网络通信建立基础。

如何初始化PXE?具体的代码细节,可以去参阅 ipxe等开源代码的关于PXE接口调用的过程。

这里只是抛砖引玉的简单阐述。

其实PXE提供的也是一组接口函数调用,我们只要获取到了对应接口函数地址,调用它们,就可以实现各种功能。

因此这里简述从获取接口地址,到封装成一个通用的C函数接口的过程。

如下C代码:

static BOOL find_pxe_api_entry( HPXE far* pxe)

{

UINT32 pxe_env = 0;

///

__asm{

mov ax,5650h

int 1Ah

cmp ax, 564Eh

jnz exit

mov word ptr [pxe_env + 2], es

mov word ptr [pxe_env], bx

exit:

}

if( !pxe_env ){

return FALSE;

}

t_PXENV far* env = (t_PXENV far*)pxe_env;

t_PXE far* pxe_e = (t_PXE far*)env->PXEPtr;

pxe->pxe_env = env;

pxe->pxe_pxe = pxe_e;

///

if( env->Version < 0x0201 ){

pxe->api_entry = env->RMEntry;

}else{

pxe->api_entry = pxe_e->EntryPointSP;

}

return TRUE;

}

其中HPXE结构是方便我这边封装定义的结构体

类似:

struct HPXE

{

UINT32 api_entry; //

UINT8 is_localinfo;

UINT8 is_udpopen;

t_PXENV far* pxe_env;

t_PXE far* pxe_pxe;

UINT16 irq; //中断号

UINT32 old_isr;

volatile UINT8 isr_trigger; //中断ISR用来判断是否发生了中断

int isr_processing; ///正在处理的 ISR, Currently processing ISR

UINT32 local_ip;

UINT16 local_port; 2023-01-31

UINT8 local_mac[6]; ///

///

UINT32 r_ip;

UINT8 r_mac[6];

///

};

而t_PXENV和t_PXE结构的定义则是PXE规范提供的,我记得这个结构体的来源好像是从ipxe代码中复制过来的,

/** The PXENV+ structure */

typedef struct s_PXENV {

UINT8 Signature[6]; // 'PXENV+'

UINT16 Version; // 0x0201

UINT8 Length;

UINT8 Checksum;

UINT32 RMEntry; /**< Real-mode PXENV+ entry point */

UINT32 PMOffset; //Protected-mode PXENV+ entry point segment selector

SEGSEL PMSelector;

SEGSEL StackSeg; /**< Stack segment selector */

UINT16 StackSize; /**< Stack segment size */

SEGSEL BC_CodeSeg; /**< Base-code code segment selector */

UINT16 BC_CodeSize; /**< Base-code code segment size */

SEGSEL BC_DataSeg; /**< Base-code data segment selector */

UINT16 BC_DataSize; /**< Base-code data segment size */

SEGSEL UNDIDataSeg; /**< UNDI data segment selector */

UINT16 UNDIDataSize; /**< UNDI data segment size */

SEGSEL UNDICodeSeg; /**< UNDI code segment selector */

UINT16 UNDICodeSize; /**< UNDI code segment size */

///

UINT32 PXEPtr;

} t_PXENV;

/** The !PXE structure */

typedef struct s_PXE {

UINT8 Signature[4]; // '!PXE'

UINT8 StructLength; /**< Length of this structure */

UINT8 StructCksum;

UINT8 StructRev;

UINT8 reserved_1; /**< Must be zero */

SEGOFF16 UNDIROMID;

SEGOFF16 BaseROMID; //

UINT32 EntryPointSP; //本来是 SEGOFF16改为 UINT32

SEGOFF16 EntryPointESP;

SEGOFF16 StatusCallout;

UINT8 reserved_2; /**< Must be zero */

UINT8 SegDescCnt;

SEGSEL FirstSelector;

SEGDESC Stack;/** Stack segment descriptor */

SEGDESC UNDIData;/** UNDI data segment descriptor */

SEGDESC UNDICode;/** UNDI code segment descriptor */

SEGDESC UNDICodeWrite;/** UNDI writable code segment descriptor */

SEGDESC BC_Data;/** Base-code data segment descriptor */

SEGDESC BC_Code;/** Base-code code segment descriptor */

SEGDESC BC_CodeWrite;/** Base-code writable code segment descriptor */

} t_PXE;

上面的find_pxe_api_entry函数找到了 接口地址 api_entry,而现在再把这个接口地址封装成C函数。

unsigned int pxe_call_api( HPXE far* pxe, WORD op_code, void far* param)

{

UINT16 rr = PXENV_STATUS_FAILURE ; ///

if( !pxe->api_entry ){

//查找入口地址

find_pxe_api_entry( pxe );

///

if( !pxe->api_entry )

return rr;

}

UINT32 pxe_api_entry = pxe->api_entry;

//以下调用兼容 老的和新的PXE API

__asm{

mov es, word ptr [param+2]

mov di, word ptr [param]

mov bx, op_code

push es

push di

push bx

call dword ptr [pxe_api_entry]

add sp,6

mov rr, ax

}

return rr;

}

这样,这个 pxe_call_api 函数,几乎在我的代码中承担了绝大部分跟PXE相关通信的调用。

而上面参数中op_code则对应着各种操作类型,有初始化的,也有ARP,以及原始链路层数据发送和接收的操作。

对应每个不同的op_code,param也对应着各种不同的结构体,这些都必须符合PXE规范。

而这些op_code操作码,以及param代表的结构体的具体函数,可以自己进一步去查询PXE规范手册,或者查询 各类PXE相关源代码。

在x7c00_Init初始化函数中,我们调用pxe_call_api函数,包括的操作码

PXENV_UNDI_STARTUP(0x01)

PXENV_UNDI_INITIALIZE(0x03)

PXENV_GET_CACHED_INFO(0x71),

因为我们需要获取PXE加载引导程序之前获取的的DHCP信息,

以方便 Int13HookEntry 进一步进行网络通信。

以及其他一些我们自己的初始化操作。

至此x7c00_Init的任务基本完成,接下来就是 Int13HookEntry 函数了。

Int13HookEntry 需要与服务器端通信,需要获取服务器端的磁盘镜像数据,因此我们得开发自己的服务端程序。

当然如果你想借用一些公共协议的磁盘镜像服务器端也行。

这里是我们自己开发的服务器端,实际上服务端也把DHCP,TFTP等也集成到一块了。

这里清一色使用UDP协议进行传输,

因为如果使用其他除开UDP,TCP之外的协议,服务端处理起来也特麻烦,

比如如果使用AOE协议(直接基于链路层的磁盘传输协议),windows服务端还得开发协议驱动来收发磁盘数据,

如果使用TCP,服务端倒没啥问题,而到了 PXE一端就挺痛苦了,因为没有现成的TCP,那意味着还得自己实现。

所以最终权衡之后,还是得使用UDP传输。

其实PXE提供的也没有UDP协议,但是因为UDP协议格式简单,组装UDP报文也不是什么难事。

MAC头+IP头+UDP头+数据内容,这就是UDP报文,而且每个报文都是独立的,不像TCP那样有非常复杂的超时重传等机制。

因此,我们可以使用上面的pxe_call_api函数再次封装UDP的收发函数,

当然,进行数据传输前,需要打开PXE设备,同样是调用 pxe_call_api 函数:

PXENV_UNDI_SET_STATION_ADDRESS(0xA),设置网卡MAC地址,

PXENV_UNDI_OPEN(0x06), 打开设备。

UDP发送函数,操作码是 PXENV_UNDI_TRANSMIT(0x08),发送的是包括MAC头在内的链路层数据,因此需要在发送前进行组包处理。

UDP接收函数,操作码PXENV_UNDI_ISR(0x14),采用轮询方式接收,其实就是设置死循环查询,当然需要设置一个超时时间,

收到UDP数据就返回,当然得去掉各种头信息。

UDP的收发函数,还必须同时处理ARP报文,因为还得管理MAC-IP的关联关系。

同时因为MTU的原因,以太网卡MTU一般都是1500,所以实际上每个UDP报文携带的数据不得超过1472,

如果要达到UDP报文的极限大小(64K),得进行IP分片,

所以还不如自己在应用层,每个UDP包大小限制小于1472,同时进行分包处理。

这里只是大致描述了一下UDP收发函数的处理过程,

实际编程过程,可能没有想象的那么容易,同时还是得在16位环境下。

如果有兴趣,可以自行去实现相关功能。

在使用pxe_call_api 基础函数,进一步封装实现UDP收发函数,比如 udp_recv和udp_send 函数之后,

开始真正实现 Int13HookEntry 函数了。

从PXE的PXENV_GET_CACHED_INFO命令中,我们获取了DHCP信息,

其实就是本地网卡的MCA地址和分配到的一个本地IP地址。

也就是获取到了本地网卡的地址,但是我们还无法知道磁盘服务端的地址,

以及一些磁盘相关的信息,比如磁盘大小,扇区大小,磁盘编号等信息。

因此还得像DHCP广播那样,在局域网内使用UDP协议,广播一条查询磁盘服务端的报文。

我们的磁盘服务端收到这条广播报文之后,开始回复相关信息。

在PXE客户端,接收到这条信息之后,就已经正确获取到了磁盘相关信息,

这样接下来自然就可以和服务端正常通信了。

因此,Int13HookEntry函数的实现看起来大致如下:

void Int13HookEntry( int13_entry far* reg )

{

while ( !netdisk_init() ) { //死循环来获取磁盘服务端信息,如果已经获取了,直接返回TRUE

;

}

//

int status = int13_hook( reg );

if( status == INT13_STATUS_SUCCESS || status == INT13_STATUS_ALL_OK ) { //success

reg->flags &= ~1; //clear CF

///

if( status == INT13_STATUS_SUCCESS )

reg->ah = status;

///

}

else{

reg->flags |= 1 ; //set CF

reg->ah = status;

}

}

netdisk_init 函数就是上面讲的,通过UDP广播方式,获取磁盘服务端各种信息,

这里还补充一点,当正确获取服务端地址以及磁盘等信息之后,

还需要告诉BIOS,我们增加了一块磁盘,告诉BIOS可以正常使用这块磁盘了。

大致如下:

修改BIOS 数据区, 增加硬盘个数

UINT32 bios_addr = 0x00400075;

UINT8 disk_num = *((char far*)bios_addr);

disk_num ++;

*((char far*)bios_addr) = disk_num;

再接下来,就是 Int13HookEntry函数里边的int13_hook函数调用。

这个函数其实就是响应BIOS的关于磁盘的各种调用,读或写磁盘扇区数据,

这就是关于BIOS的13H中断的具体细节了。比如

https://en.wikipedia.org/wiki/INT_13H

这个连接比较详细的描述了INT13H中断的各种命令(国内可能无法直接访问上面的连接)。

13H中断的AH寄存器,也就是对应着 int13_entry结构体中的ah变量,

存放着磁盘各种命令,大致如下一些常用的命令:

#define INT13_RESET 0x00 // Reset disk system

#define INT13_GET_LAST_STATUS 0x01 /// Get status of last operation

#define INT13_READ_SECTORS 0x02 /// Read sectors

#define INT13_WRITE_SECTORS 0x03 /// Write sectors

#define INT13_VERIFY_SECTORS 0x04 /// 验证扇区

#define INT13_GET_PARAMETERS 0x08 /// Get drive parameters

#define INT13_GET_DISK_TYPE 0x15 /// Get disk type

#define INT13_EXTENSION_CHECK 0x41

#define INT13_EXTENDED_VERIFY 0x44 // verify sectors

#define INT13_EXTENSION_PARAM 0x48

#define INT13_EXTENDED_READ 0x42

#define INT13_EXTENDED_WRITE 0x43

其中 INT13_READ_SECTORS和INT13_WRITE_SECTORS, 以及

INT13_EXTENDED_READ和INT13_EXTENDED_WRITE

就是读和写扇区命令,

这个命令需要通过 UDP协议发送到服务端,完成真正的磁盘扇区数据的读写。

至于如何实现磁盘扇区数据传输过程,则是八仙过海,各显神通了。

需要保证尽可能快的完成数据通信,

而且在上文也提到过,windows7在实模式会传输70MB多的数据,而win10则传输80-90MB多的数据。

因此,在千兆网络环境下,起码得保证每秒 20-30MB的传输速率,(MB是兆字节,而不是兆位)

才能保证在 4-5秒内完成实模式下的数据传输过程。

下面是我开发的传统BIOS下引导程序 +

windows虚拟磁盘驱动一起形成的无盘启动程序的gif演示图。

在vmware虚拟机环境中的 win7的无盘启动过程,速度其实也挺快的,启动过程基本没超过30秒。

当然,换成win10,速度就有点勉强了,启动过程大概需要1分钟多些,

大概跟电脑配置,vmware虚拟机,以及win10更加臃肿有些关系,

毕竟没有在真实机器上试过,真机上应该会更快。

windows无盘启动技术开发之传统BIOS(Legacy BIOS)引导程序开发之二_第1张图片

你可能感兴趣的:(网络驱动,磁盘驱动,BIOS引导程序)