16位装载程序 - 第一阶段装载之探测内存
正常情况下,在MBR程序和分区启动程序运行完后,就开始执行位于0800:0100的16位装载程序,这个程序就是被分区启动程序加载到这个位置的。
这一阶段要完成的工作比较多,因此相应的源代码也比较多了。所以我就分成几个部分来写,
这个第一阶段装载要完成内存探测、加载配置文件、加载第二阶段的32位装载程序、加载各种必须的系统文件(主要是各种驱动,因为进入32位后,就没有BIOS可用了),最后给硬盘做标记。
1. 装载程序
我的装载程序分成两个阶段执行,第一阶段是在实模式下进行装载,主要是探测内存和加载必要的文件,第二阶段是在32位不分页的保护模式下装载,主要是装载核心、初始化物理内存布局、初始化分页,完成这些后,才开始运行真正的系统。
2. 16位装载程序的作用
16位装载程序起着非常关键的作用,因为有很多事情要在实模式完成,比如把各种文件加载进内存。为什么要在实模式下加载,而不是到32位保护模式下进行加载呢?因为进入32位保护模式后,就没有BIOS可用了(不考虑V86模式,因为编程比在实模式下麻烦,还不如直接在实模式下完成),要完成探测内存、读取磁盘什么的工作,就要自己先写好这些硬件的驱动,至少是实现部分功能的。但是这些功能在BIOS中提供了,所以,我就选择在实模式下把需要的文件先加载进内存。
3. 获取内存容量
获取内存容量有几种方法,不能只用一种方法,因为BIOS不一定支持这种方法,如果都不支持,那就只能用最原始的读写比较来探测内存了。 int 15h的具体使用方法放到书里。
3.1 EAX=0E820H调用INT 15H
这个BIOS功能的调用规范就留到书里了,其实下面的代码已经把它封装成C函数,调用的方法也在里面,输出结果也贴在后面了。
#include "stdio.h"
typedef struct _reg_t
{
unsigned short _DI, _SI, _BP, _SP, _BX, _DX, _CX, _AX;
unsigned long _EDI, _ESI, _EBP, _ESP, _EBX, _EDX, _ECX, _EAX;
unsigned short _CS, _DS, _ES, _SS, _FS, _GS;
}reg_t;
/*Values for System Memory Map address type:
01h memory, available to OS
02h reserved, not available (e.g. system ROM, memory-mapped device)
03h ACPI Reclaim Memory (usable by OS after reading ACPI tables)
04h ACPI NVS Memory (OS is required to save this memory between NVS
sessions)
other not defined yet -- treat as Reserved
*/
typedef struct _address_range_map_t
{
unsigned long _BaseLow,_BaseHigh,_SizeLow,_SizeHigh,_Type;
}arm_t;
arm_t arm;
char * strType[] =
{
"undefined",
"memory, available to OS",
"reserved, not available",
"ACPI Reclaim Memory",
"ACPI NVS Memory"
};
#define LOW_WORD(dw) ((unsigned short)(dw))
#define HIGH_WORD(dw) ((unsigned short)((dw)>>16))
#define SEGMENT(farptr) HIGH_WORD((unsigned long)(farptr))
#define OFFSET(farptr) LOW_WORD((unsigned long)(farptr))
int int_15h_E820(void * far pARD, unsigned int nSizeARD, unsigned long * pLeftCnt)
{
reg_t reg;
;
reg._EAX = 0xE820;
reg._EBX = *pLeftCnt;
reg._ECX = nSizeARD;
reg._EDX = 0x534D4150;
reg._ES = SEGMENT(pARD);
reg._DI = OFFSET(pARD);
asm{
push si
push di
lea si, reg
mov di, [si]._DI
mov ax, [si]._ES
mov es, ax
db 066h
mov ax, [si]._EAX ;// mov eax, reg._EAX
db 066h
mov bx, [si]._EBX ;// mov ebx, reg._EBX
db 066h
mov cx, [si]._ECX ;// mov ecx, reg._ECX
db 066h
mov dx, [si]._EDX ;// mov edx, reg._EDX
int 015h
mov si, pLeftCnt
db 066h
mov [si], bx ;// mov [pLeftCnt], ebx
pop di
pop si
jc error
}
return 1;
error:
return 0;
}
int main(void)
{
unsigned long nLeftCnt = 0;
int i = 0;
printf(" ## base address size type\n");
printf(" -- ----------------- ----------------- --------------------------\n");
do{
int_15h_E820(&arm, sizeof(arm_t), &nLeftCnt);
printf(" %2d %08lX-%08lX %08lX-%08lX %s\n", ++i,
arm._BaseHigh, arm._BaseLow,
arm._SizeHigh, arm._SizeLow,strType[arm._Type % 5]);
}while( nLeftCnt );
return 0;
}
上图是一个8G内存虚拟机的探测情况,可以看到有几个地方是需要保留的,有一些地址是断开的
0x000A0000 - 0x000DFFFF 这里有256K
0xF8000000 - 0xFFFFFFFF 这里有128M
然后看有效地址上限,给虚拟机设定的内存是8G,但是可以访问的上限却是8G + 128M,把0xF8000000 - 0xFFFFFFFF这一段给补回来了
3.2 AX=0E801H调用INT 15H
这个方法也算不错了,但是上限是4G内存,而且数量会少256K。如果内存容量超过4G,他的返回结果有些不同。
/*
AX = E801h
Return:
CF clear if successful
AX = extended memory between 1M and 16M, in K (max 3C00h = 15MB)
BX = extended memory above 16M, in 64K blocks
CX = configured memory 1M to 16M, in K
DX = configured memory above 16M, in 64K blocks
CF set on error
*/
int int_15h_E801(unsigned short * pExtMem16M, unsigned short * pExtMem4G,
unsigned short * pCfgMem16M, unsigned short * pCfgMem4G)
{
asm{
mov ax, 0E801h
int 015h
jc error
push si
mov si, pExtMem16M
mov [si], ax
mov si, pExtMem4G
mov [si], bx
mov si, pCfgMem16M
mov [si], cx
mov si, pCfgMem4G
mov [si], dx
pop si
}
return 1;
error:
return 0;
}
3.3 AX = 088H调用INT 15H
这个是BIOS最早提供的方法,只能得到64M的内存。
在Hyper-V里已经不支持方法,如果用这个方法,得到的结果是0。
unsigned short int_15h_88(void)
{
unsigned short nTotal = 0;
asm{
mov ah, 088h
int 015h
jc error
mov nTotal, ax
}
return nTotal;
error:
return 0;
}
3.4 读写比较探测
如果以上方法都不行,只能先写入数据,然后读出来比较的方法了。这个方法以“如果这个地址能够访问,那写入后在读出的数据应该一致”为基础的。肯定会有人问,如果碰巧一样呢?那你就写入4个字节,碰巧一样的几率是四十亿分之一,如果还一样,好吧,那就在想别的办法,我懒得想了,而且我觉得在虚拟机下也没得想了。其实,经过测试,发现如果地址不能访问,读出的数据都是0。
这个方法比较麻烦,先要实现在实模式下访问4G地址空间,然后才能访问4G空间,利用在不分页的情况下,线性地址就是物理地址的情况,来测试内存是否可以使用。实际上就是在段寄存器装入一个能访问4G空间的段描述符,然后不要修改这个段寄存器,直接使用段寄存器访问内存就行了。
下面就不贴完整的代码了,贴出关键的部分
;
;void WriteByte4G(byte_t nData, uint32_t nPhyAddr)
;
_WriteByte4G PROC
mov bx, sp
mov al, [bx + 2]
mov ecx, [bx + 4]
mov ebx, ecx
mov fs:[ebx], al
ret
_WriteByte4G ENDP
;
;byte_t ReadByte4G(uint32_t nPhyAddr)
;
_ReadByte4G PROC
mov bx, sp
xor ax, ax
mov ecx, [bx + 2]
mov ebx, ecx
mov al, fs:[ebx]
ret
_ReadByte4G ENDP
// test memory
nSize32 = 0x100000l;
while( nSize32 < 0xF8000000 ){
_printf(" testing memory %lu ...\r",nSize32);
nTmp = ReadByte4G(nSize32);
WriteByte4G(0xA5, nSize32);
if( ReadByte4G(nSize32) != 0xA5 )
break;
WriteByte4G(nTmp, nSize32);
nSize32 += 0x400000l; /* detection setp is 1M */
}
4 综合比较
用这三种方法对同一个分配了8G内存的虚拟机进行测试,代码和运行结果放在下面
int main(void)
{
unsigned long nLeftCnt = 0;
unsigned short nExtMem16M, nExtMem4G, nCfgMem16M, nCfgMem4G;
int i = 0;
printf("EAX = E8020:\n");
printf(" ## base address size type\n");
printf(" -- ----------------- ----------------- -------------------------\n");
do{
if( !int_15h_E820(&arm, sizeof(arm_t), &nLeftCnt) )
break;
printf(" %2d %08lX-%08lX %08lX-%08lX %s\n", ++i,
arm._BaseHigh, arm._BaseLow,
arm._SizeHigh, arm._SizeLow,strType[arm._Type % 5]);
}while( nLeftCnt );
printf("\nAX = E801\n");
if( int_15h_E801(&nExtMem16M, &nExtMem4G, &nCfgMem16M, &nCfgMem4G) ){
printf("Extend memory: %u %u. total: %lu\n", nExtMem16M, nExtMem4G,
(unsigned long)nExtMem16M + (unsigned long)nExtMem4G * 64);
printf("Config memory: %u %u. total: %lu\n", nCfgMem16M, nCfgMem4G,
(unsigned long)nCfgMem16M + (unsigned long)nCfgMem4G * 64);
}
printf("\nAH = 0x88\n");
printf("Extend memory: %u\n", int_15h_88());
return 0;
}
从这测试结果来看,我是不使用AH=088h了,如果EAX=0E820和AX=0E801都不行的话,我是采用读写比较的方法了。