机器启动后有2种方式探测物理内存:直接探测、通过BIOS中断探测。
这里我们关注如何通过BIOS的0x15中断探测物理内存。
由于BIOS中断需要在实模式下调用,所以我们在bootloader中探测物理内存比较合适。
BIOS的0x15中断有3个子功能:e820h、e801h、88h。这三个子功能区别是:
- e820h返回内存布局,信息量大
- e801h返回内存容量
- 88h最简单,功能也最弱
e820h子功能
这是功能最强大的子功能,使用也最复杂。
地址范围描述符
系统内存分为很多内存块,这些内存块加起来才构成系统的总内存。而这些内存块有不同的类型:有的OS可以使用、有的OS不能使用。
每次调用这个中断都会得到一块内存的描述,要通过多次调用才能得到系统所有内存块的描述。
e820将物理内存探测的结果以地址范围描述符的格式放在内存中。地址范围描述符共计20字节,格式是:
字节范围 | 描述 |
---|---|
0~7 | 内存块基地址 |
8~15 | 这块内存的大小 |
16~20 | 这块内存的类型 |
我们将在内存中开辟一个数组来存放所有地址范围描述符。
在c语言中可以这样定义:
struct {
uint64_t addr;
uint64_t size;
uint32_t type;
};
e820h的调用参数
显然在保护模式下用int $0x15
来调用15h中断。
但在这之前我们要将参数放置在寄存器中:
- eax:子功能编号,这里我们填入0xe820
- edx:534D4150h(ascii字符”SMAP”),签名,约定填”SMAP”
- ebx:每调用一次
int $0x15
,ebx会加1。当ebx为0表示所有内存块检测完毕。(重要!看后面的案例会明白如何使用) - ecx:存放地址范围描述符的内存大小,至少要设置为20。
- es:di:告诉BIOS要把地址描述符写到这个地址。
中断的返回值如下:
- CF标志位:若中断执行失败,则置为1。
- eax:值是534D4150h(“SMAP”)
- es:di:中断不改变该值,值与参数传入的值一致
- ebx:下一个中断描述符的计数值(见后面的案例)
- ecx:返回BIOS写到cs:di处的地址描述符的大小(应该就是20吧?)
- ah:若发生错误,表示错误码
案例:实现内存探测
定义数据存放结构,约定探测结果存放于0x8000处
约定在bootloader中将内存探测结果放到0x8000地址处。
数据存放的格式在c中定义了:
struct e820map {
int nr_map; // 表示数组元素个数,该字段是为了方便后续OS,不是BIOS访问的
struct {
uint64_t addr;
uint64_t size;
uint32_t type;
} map[E820MAX];
};
调用中断,并判断是否发生错误、是否探测完所有内存块
通过int $0x15
调用中断。如果发生错误,CF位为1。那么可以尝试使用其它子功能进行探测,或者就直接关机(连内存容量都没探测肯定无法启动OS了)
根据ebx判断是否完成所有内存块的探测,如果没有则继续int $0x15
探测下一个内存块,如此反复直到所有内存块探测完毕。