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更加臃肿有些关系,
毕竟没有在真实机器上试过,真机上应该会更快。