文章目录
写在前面:自制操作系统Gos 第二章第六篇:主要内容是如何获取内存信息
linux 中获取内存信息的方法
大家可能在想,我们获取内存信息多简单啊,直接top
命令就可以了。确实,在Linux中输入这个命令就可以了。但是在我们自己的操作系统上可没人帮我们实现top
命令,所以我们必须明白这个top
命令的底层原理实现。
注:top这个例子可能不是很准确,大家懂我啥意思就好了
在x86体系下,由三种方法可以查看系统的内存,但是其本质都是依托BIOS的0x15
中断的三个子功能:
- eax = 0xe820:遍历主机上全部内存
- ax = 0xe801:分别检测低15MB和16MB~4GB的内存
- ah = 0x88:最多检测出64MB内存
BIOS中断是实模式下的方法,其可以返回已安装的硬件信息。本质呢,则是通过连续调用硬件的应用程序接口API来获取内存信息的。但是由于我们之后就要进入保护模式了,所以我们需要把结果存到内存中,这个等会儿在说。
0xe820:遍历主机上全部内存
BIOS中断0x15的子功能号0xe820
能够获取系统的内存布局,由于系统内存各部分的类型属性不同,BIOS就按照类型属性来划分这片系统内存,BIOS不断的将内存信息返回。这个返回的本质起始时一个地址范围描述符,其结构如下:
其中每个字段的含义如下:
- BaseAddrLow:基地址的低32位
- BaseAddrHigh:基地址的高32位
- LengthLow:内存长度低32位
- LengthHigh:内存长度高32位
- Type:本段内存的属性
其他几个都没啥好讲的,主要是这个Type字段,其含义如下:
值 | 描述 |
---|---|
1 | 这段内存可以被操作系统使用 |
2 | 内存使用中或者被系统保留,操作系统不可用 |
其他 | 未定义 |
而获取地址范围描述符就要守规矩,基本规则就是我们输入什么,它们返回什么。
首先是调用前输入:
寄存器 | 用途 |
---|---|
eax | 输入子功能号,由于是要用0xe820,所以此处是0xe820,子功能号更改一下就好了 |
ebx | ards后续值:因为每次只能返回一个,所以它里面并保存的就是后续值的地址,可以想象为链表 |
es : di | ards缓冲区:BIOS将获取到的内存信息写入此寄存器执行的内存 |
ecx | ards结构的字节大小,目前输入20就可以了 |
edx | 固定签名标记0x534d4150 |
而检测主要有以下几个步骤:
- 填写好输入的寄存器
- 执行中断
- 在CF位为0的情况下,获取相应的结果
而用代码表示则是这样:
xor ebx, ebx ;第一次调用时,ebx值要为0
mov edx, 0x534d4150
mov di, ards_buf ;ards结构缓冲区
mov eax, 0x0000e820 ;执行int 0x15后,eax值变为0x534d4150,所以每次执行int前都要更新为子功能号。
mov ecx, 20 ;ARDS地址范围描述符结构大小是20字节
int 0x15
我们输入之后直接启用中断就好,之后等待返回就可以了,返回规则如下:
寄存器或状态位 | 用途 |
---|---|
CF位 | 若CF为0表示调用成功,为1表示调用出错 |
EAX | 字符串SMAP的ASCII码 0x534d4150 |
es : di | ards缓冲区地址 |
ecx | BIOS写入到缓冲区中的字节数 |
ebx | 后续值的位置 |
代码实现如下:
.e820_mem_get_loop: ;循环获取每个ARDS内存范围描述结构
mov eax, 0x0000e820 ;执行int 0x15后,eax值变为0x534d4150,所以每次执行int前都要更新为子功能号。
mov ecx, 20 ;ARDS地址范围描述符结构大小是20字节
int 0x15
jc .e820_failed_so_try_e801 ;若cf位为1则有错误发生,尝试0xe801子功能
add di, cx ;使di增加20字节指向缓冲区中新的ARDS结构位置
inc word [ards_nr] ;记录ARDS数量
cmp ebx, 0 ;若ebx为0且cf不为1,这说明ards全部返回,当前已是最后一个
jnz .e820_mem_get_loop
;在所有ards结构中,找出(base_add_low + length_low)的最大值,即内存的容量。
mov cx, [ards_nr] ;遍历每一个ARDS结构体,循环次数是ARDS的数量
mov ebx, ards_buf
xor edx, edx
0xe801:分别检测低15MB和16MB~4GB的内存
现在我们来介绍一下第二种方法,其也是0xe820失败后我们应该采取的第一个补救措施。
为什么他不是我们获取内存信息的首要方法呢?这是因为其检测到的内存是分别存放到两组寄存器中的。低于15MB的内存以1KB为单位大小来记录,单位数量在寄存器ax
和cx
中记录,当然,这两个值一样的。16MB~4GB是以64KB为单位大小来记录的,单位数量在寄存器bx
和dx
中记录。
而我们想使用这个功能。只需要简单的填写一个ax
中的功能号就好了。
寄存器 | 描述 |
---|---|
ax | 输入子功能号0xe801 |
而在触发中断之后,BIOS会给我们返回以下信息:
寄存器或状态位 | 描述 |
---|---|
CF位 | CF为0表示调用成功,为1表示调用出错 |
ax | 显示15MB以下的内存容量,以1KB为单位 |
bx | 显示16MB~4GB的内存容量,以64KB为单位 |
cx | 同ax |
dx | 同bx |
实现思路和上面的0xe820,所以代码上面我们也很好实现啦:
.e820_failed_so_try_e801:
mov ax,0xe801
int 0x15
jc .e801_failed_so_try88 ;若当前e801方法失败,就尝试0x88方法
;1 先算出低15M的内存,ax和cx中是以KB为单位的内存数量,将其转换为以byte为单位
mov cx,0x400 ;cx和ax值一样,cx用做乘数
mul cx
shl edx,16
and eax,0x0000FFFF
or edx,eax
add edx, 0x100000 ;ax只是15MB,故要加1MB
mov esi,edx ;先把低15MB的内存容量存入esi寄存器备份
;2 再将16MB以上的内存转换为byte为单位,寄存器bx和dx中是以64KB为单位的内存数量
xor eax,eax
mov ax,bx
mov ecx, 0x10000 ;0x10000十进制为64KB
mul ecx ;32位乘法,默认的被乘数是eax,积为64位,高32位存入edx,低32位存入eax.
add esi,eax ;由于此方法只能测出4G以内的内存,故32位eax足够了,edx肯定为0,只加eax便可
mov edx,esi ;edx为总内存大小
jmp .mem_get_ok
ah = 0x88:检测64MB内存
这个方法应该是功能性最局限的了。思路和上面两个基本一样,只是输入和输出的东西不同了。
先看一下输入:
寄存器 | 用途 |
---|---|
ah | 子功能号:0x88 |
而输出的东西也很简单:
CF位 | 若CF为0表示调用未出错,CF为1表示出错 |
ax | 内存空间1MB之上的连续单位数量,以1KB为单位 |
其代码实现如下:
.e801_failed_so_try88:
;int 15后,ax存入的是以kb为单位的内存容量
mov ah, 0x88
int 0x15
jc .error_hlt
and eax,0x0000FFFF
;16位乘法,被乘数是ax,积为32位.积的高16位在dx中,积的低16位在ax中
mov cx, 0x400 ;0x400等于1024,将ax中的内存容量换为以byte为单位
mul cx
shl edx, 16 ;把dx移到高16位
or edx, eax ;把积的低16位组合到edx,为32位的积
add edx,0x100000 ;0x88子功能只会返回1MB以上的内存,故实际内存大小要加上1MB
最终实现
这样我们就获得了内存信息,之后我们把这个值存储到0xb00
这个位置:
; total_mem_bytes用于保存内存容量,以字节为单位,此位置比较好记。
; 当前偏移loader.bin文件头0x200字节,loader.bin的加载地址是0x900,
; 故total_mem_bytes内存中的地址是0xb00.将来在内核中咱们会引用此地址
total_mem_bytes dd 0
loader_start:
;------- int 15h eax = 0000E820h ,edx = 534D4150h ('SMAP') 获取内存布局 -------
xor ebx, ebx ;第一次调用时,ebx值要为0
mov edx, 0x534d4150 ;edx只赋值一次,循环体中不会改变
mov di, ards_buf ;ards结构缓冲区
.e820_mem_get_loop: ;循环获取每个ARDS内存范围描述结构
mov eax, 0x0000e820 ;执行int 0x15后,eax值变为0x534d4150,所以每次执行int前都要更新为子功能号。
mov ecx, 20 ;ARDS地址范围描述符结构大小是20字节
int 0x15
jc .e820_failed_so_try_e801 ;若cf位为1则有错误发生,尝试0xe801子功能
add di, cx ;使di增加20字节指向缓冲区中新的ARDS结构位置
inc word [ards_nr] ;记录ARDS数量
cmp ebx, 0 ;若ebx为0且cf不为1,这说明ards全部返回,当前已是最后一个
jnz .e820_mem_get_loop
;在所有ards结构中,找出(base_add_low + length_low)的最大值,即内存的容量。
mov cx, [ards_nr] ;遍历每一个ARDS结构体,循环次数是ARDS的数量
mov ebx, ards_buf
xor edx, edx ;edx为最大的内存容量,在此先清0
.find_max_mem_area: ;无须判断type是否为1,最大的内存块一定是可被使用
mov eax, [ebx] ;base_add_low
add eax, [ebx+8] ;length_low
add ebx, 20 ;指向缓冲区中下一个ARDS结构
cmp edx, eax ;冒泡排序,找出最大,edx寄存器始终是最大的内存容量
jge .next_ards
mov edx, eax ;edx为总内存大小
.next_ards:
loop .find_max_mem_area
jmp .mem_get_ok
;------ int 15h ax = E801h 获取内存大小,最大支持4G ------
; 返回后, ax cx 值一样,以KB为单位,bx dx值一样,以64KB为单位
; 在ax和cx寄存器中为低16M,在bx和dx寄存器中为16MB到4G。
.e820_failed_so_try_e801:
mov ax,0xe801
int 0x15
jc .e801_failed_so_try88 ;若当前e801方法失败,就尝试0x88方法
;1 先算出低15M的内存,ax和cx中是以KB为单位的内存数量,将其转换为以byte为单位
mov cx,0x400 ;cx和ax值一样,cx用做乘数
mul cx
shl edx,16
and eax,0x0000FFFF
or edx,eax
add edx, 0x100000 ;ax只是15MB,故要加1MB
mov esi,edx ;先把低15MB的内存容量存入esi寄存器备份
;2 再将16MB以上的内存转换为byte为单位,寄存器bx和dx中是以64KB为单位的内存数量
xor eax,eax
mov ax,bx
mov ecx, 0x10000 ;0x10000十进制为64KB
mul ecx ;32位乘法,默认的被乘数是eax,积为64位,高32位存入edx,低32位存入eax.
add esi,eax ;由于此方法只能测出4G以内的内存,故32位eax足够了,edx肯定为0,只加eax便可
mov edx,esi ;edx为总内存大小
jmp .mem_get_ok
;----------------- int 15h ah = 0x88 获取内存大小,只能获取64M之内 ----------
.e801_failed_so_try88:
;int 15后,ax存入的是以kb为单位的内存容量
mov ah, 0x88
int 0x15
jc .error_hlt
and eax,0x0000FFFF
;16位乘法,被乘数是ax,积为32位.积的高16位在dx中,积的低16位在ax中
mov cx, 0x400 ;0x400等于1024,将ax中的内存容量换为以byte为单位
mul cx
shl edx, 16 ;把dx移到高16位
or edx, eax ;把积的低16位组合到edx,为32位的积
add edx,0x100000 ;0x88子功能只会返回1MB以上的内存,故实际内存大小要加上1MB
.mem_get_ok:
mov [total_mem_bytes], edx ;将内存换为byte单位后存入total_mem_bytes处。
参考文献
[1] 操作系统真相还原