Gos —— 获取物理内存容量

文章目录

写在前面:自制操作系统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不断的将内存信息返回。这个返回的本质起始时一个地址范围描述符,其结构如下:
Gos —— 获取物理内存容量
其中每个字段的含义如下:

  • 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为单位大小来记录,单位数量在寄存器axcx中记录,当然,这两个值一样的。16MB~4GB是以64KB为单位大小来记录的,单位数量在寄存器bxdx中记录。

而我们想使用这个功能。只需要简单的填写一个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] 操作系统真相还原
上一篇:保护模式篇——中断与异常和控制寄存器


下一篇:10.任务门(Task Gate)