这里写个往期推荐,这样可以来回跳跃(狗头
0x00-环境准备
0x01-BIOS以及MBR
0x02-MBR支持显卡
0x03-MBR操作硬盘以及Loader
0x04-进入保护模式
0x05-内存容量检测
0x00 基础知识
今天本来是准备进入内核知识,但在这之前我们先简单了解下如何获取物理内存容量,这是因为考虑到把这个讲了再加入内核分页的只是会显得很长,所以获取内存容量单独写一篇。为了在后期做好内存管理的工作,我们首先得知道自己有多少物理内存才行。
0.Linux获取内存基本嫩方法
在linux 2,6内核中,使用detect_memory 函数来获取内存容量的,其函数本质上是通过调用BIOS中断0x15实现的,分别是BIOS中断0x15的3个子功能,子功能号需要存放到寄存器EAX或AX中,以下是这三种模式的介绍:
- EAX=0xE820:遍历主机上全部内存
- AX=0xE801:分别检测低15MB和16MB~4GB内存,最大支持4GB
- AH=0x88:最多检测64MB内存,实际内存超过此容量也按照64MB返回
这里大伙可能会奇怪,咱们不是进入保护模式了吗,我还怎么用BIOS中断呢,不是你最开始说BIOS中断只能在实模式下才能用吗,你是不是不懂装懂,你是不是根本不会啊,这个博主就是个××

这里我们并不是说要在保护模式之下进行BIOS中断操作,今天我们所讲的只是一个小demo而已,所以我们先放下上一篇的知识,咱们先在实模式下检测了物理内存再进入保护模式。这里我们将这三个方法依次介绍(如果其中一个用不了就用别的)
1.利用BIOS中断0x15子功能0xe820获取内存
据说这是最灵活的内存获取方式,说他比较灵活是因为他返回的信息比较丰富,而返回丰富的信息就表示我们需要用一种格式结构来组织这些数据。而内存信息的内容是用地址范围描述符来描述的,用于存储这种描述符的结构称之为地址范围描述符(Address Range Descriptor Structure,ARDS),格式如下:

上述的字段从偏移也可以看出每个占4字节,其中含义大家可以有表得知,这里详细介绍其中的TYPE字段,具体意义如下:

而BIOS按照上述类型来返回内存信息是因为这段内存可能为一下几种情况:
- 系统的ROM
- ROM用到了这部分内存
- 设备内存映射到了这部分内存
- 由于某种原因,这段内存不适合标准设备使用
而由于我们是在32位环境下,所以我么只需要用到低32位属性属性,也就是BaseAddrLow 和 LengthLow就可以了,当然我们在调用BIOS中断不仅仅使得EAX或AX里面有相应的功能号,我们还需要通过其他寄存器传入一系列参数,下面给出具体示例:


这里值得注意的参数寄存器有ECX和ES:DI,其中ECX是指缓冲区大小,ES:DI是指缓冲区指针,被调用函数将所写内容写入该缓冲区,然后记录写入内容大小然后记录在缓冲区大小寄存器中。注意这里调用者是传入的期待BIOS写入大小,而被调用者则是往ECX写入实际大小。此中断的调用步骤如下:
- 填写好调用前函数寄存器
- 执行中断调用int 0x15
- 在CF为0的情况下,“返回后输出”的寄存器便会有相应的结果
2.利用BIOS中断0x15子功能0xe801获取内存
这个子功能整体来说不算强,但也值得我们来学习,他虽然说最大只能识别4GB内存,但是对咱们32位地址总线足够了,但是有点特殊的就是这种方法检测到的内存是分别放到两组寄存器中的。低于15MB的内存以1KB为单位来记录,单位数量在AX和CX中记录,其中AX和CX的值是一样的,所以在15MB空间以下的实际内存容量=AX×1024.AX,CX最大值为0x3c00,即0x3c00**** 1024 =15MB.而16MB~4GB是以64KB为单位大小来记录的,单位数量在BX、DX中存储,其中这俩内容一样,跟上述AX、CX类似。下面给出输入时寄存器以及输出时寄存器的功能和作用:

这里我们大多数人都会意识到这两个问题:
- 为什么要分“前15MB”和“16MB~4GB”。
- 为什么要设两个内容相同的单位量寄存器,就是说AX=CX,BX=DX.
为了解释地一个问题,我们在这里给出经过测试后的结果,这里如何测试不重要,我们会在最后实战,所以这里我们只关注结果即可

这里我们观看表头,发现实际物理内存和检测到的内存大小总是相差1MB,这是为什么呢
这里实际上是万恶的历史遗留问题,这是由于在80286版本由于有24位地址线,即可表示16MB的内存空间,其中低15MB用来正常作为内存使用,而高1MB是留给一些ISA设备作为缓冲区使用,到了现在由于为了向前兼容,所以这1MB还是被空了出来,造成一种内存空洞的现象。
所以说但我们检查内存大小大于等于16MB时,其中AX×1024必然小于等于15MB,而BX×64K必然大于0,所以我们在这种情况下是可以检查出这个历史遗留的1MB的内存空洞,但若是我们检查内存小于16MB的时候,我们所检查的内容范围就会小于实际内存1MB。
至于第二个问题,为什么要用两个内容相同的问题,我们在上面的输入寄存器的图片中可以看到,AX与CX,BX与DX这两组寄存器中,都是一个充当Extended和Configured.但这里我们暂时不去区别他们之间的不同,我怎么感觉这第二个问题我说了个寂寞。大伙别见怪,书上就这么说的。
这里再给出第二个方法的调用步骤:
- 将AX寄存器写入0xE801
- 执行中断调用
- 在CF为0的情况下,“返回后输出”的寄存器便会有相应的结果
3. 利用BIOS中断Ox15子功能Ox88获取内存
这里就是最后一个子功能了,他使用简单,获取的东西也简单,他只能识别到最大64MB的内存,即使内存容量大于64MB,也只会显示63MB,这里为啥又少了1MB呢,这是因为此中断只能显示1MB之上的内存,所以我们在检测之后需要加上1MB,现在懂了为啥说第一种灵活了吧,这第二种第三种都有点毛病,这里像之前一样给出传递参数

调用步骤如下:
- 将AH寄存器写入0x88
- 执行中断调用 int 0x15
- 在CF为0的情况下,“返回后输出”的寄存器便会有相应的结果
0x01 实战内存容量检测
按照我们上面介绍的基础知识,这里我们直接上代码,大伙也可以自己先尝试一下:
ards_buf times 244 db 0
ards_nr dw 0
loader_start:
xor ebx, ebx
mov edx, 0x534d4150
mov di, ards_buf
.e820_mem_get_loop:
mov eax, 0x0000e820
mov ecx, 20
int 0x15
jc .e820_failed_so_try_e801
add di, cx
inc word[ards_nr]
cmp ebx,0
jnz .e820_mem_get_loop
mov cx, [ards_nr]
mov ebx, ards_buf
xor edx, edx
.find_max_mem_area:
mov eax, [ebx]
add eax, [ebx+8]
add ebx, 20
cmp edx, eax
jge .next_ards
mov edx, eax
.next_ards:
loop .find_max_mem_area
jmp .mem_get_ok
.e820_failed_so_try_e801:
mov ax, 0xe801
int 0x15
jc .e801_failed_so_try88
mov cx,0x400
mul cx
shl edx, 16
and eax, 0x0000FFFF
or edx, eax
add edx, 0x100000
mov esi, edx
xor eax, eax
mov ax, bx
mov ecx, 0x10000
mul ecx
add esi, eax
mov edx, esi
jmp .mem_get_ok
.e801_failed_so_try88:
mov ah, 0x88
int 0x15
jc .error_hlt
and eax, 0x0000FFFF
mov cx, 0x400
mul cx
shl edx, 16
or edx, eax
add edx,0x100000
.error_hlt:
jmp $
.mem_get_ok:
mov [total_mem_bytes], edx
可以看到我们上面有个代码是人工对齐,其实这个人工对其不是必要的,但是方便咱们调试以及查看,所以我们将其变为0x100的整数倍,我将上面代码简单介绍下,因为我们loader是存放在内存0x900的地址,然而我们在这个地址上又加上了四个段描述符和60个段描述符预留空位,此时已经用了0x200,然后我们还需要划出一点来作为ards的存放缓冲,存放最大内存,还有gdt指针,这些统统加起来为了满足0x100的整数倍,我们在此选择缓冲区大小申请244字节,这里大家认真查看代码然后计算即可,
注意还有个需要修改的地方就是mbr.S,因为我们在loader.S上去掉了jmp loader_start(占3字节),而loader_start在loader中的偏移为我们精心准备好的0x300,所以我们在mbr跳转到loader_start时就要加上0x300,修改部分如下:
jmp LOADER_BASE_ADDR + 0x300
然后我们进行汇编,这里建议大家用个脚本一套完成算了,免得每次还的一行行打
#!/bin/sh
nasm -I include/ -o mbr.bin mbr.S
dd if=./mbr.bin of=../bochs/hd60M.img bs=512 count=1 conv=notrunc
nams -I include -o loader.bin loader.S
dd if=./loader.bin of=../bochs/hd60M.img bs=512 count=4 seek=2 conv=notrunc
这样在我们mbr运行完毕后就会直接跳转到loader_start开始内存检测了。这里我给出我们bochs的配置:
megs :512
romimage: file=/home/dawn/repos/OS_learning/bochs/share/bochs/BIOS-bochs-latest
vgaromimage: file=/home/dawn/repos/OS_learning/bochs/share/bochs/VGABIOS-lgpl-latest
boot: disk
log: bochs.out
mouse: enabled=0
keyboard:keymap=/home/dawn/repos/OS_learning/bochs/share/bochs/keymaps/x11-pc-us.map
ata0: enabled=1, ioaddr1=0x1f0, ioaddr2=0x3f0, irq=14
ata0-master: type=disk, path="hd60M.img", mode=flat
可以看到内存设置为512MB,我们在开启bochs查看是否如此。

发现果然如此,这个0x20000000大家用十进制表示会发现确实为512MB。
0x02 总结
本篇作为一个额外篇是为下一篇分担点篇幅,不然会显得十分冗长,今天的检测还是十分简单的,大家仔细看代码就会看懂。
本次我的所有源码已在github上成功上传,分支名定为Checkout,欢迎各位指教
传送门