欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

6.检测物理内存的容量

程序员文章站 2024-03-21 16:28:04
...

操作系统是管理硬件的大管家,所以它要知道有哪些硬件资源啊。比如要检测内存,知道内存的容量。

1.学习 Linux 中的获取内存的方法

学习 Linux 中的获取内存的方法,调用 BIOS 中断 0x15 来实现内存容量的检测。
0x15 中断一共有 3 个子功能:

  1. EAX=0xe820 ;遍历主机上全部内存
  2. AX=0xe801 ;分别检测低 15MB 和 16MB~4GB 的内存,最大支持 4GB。
  3. AH=0x88 ;最多检测 64MB 内存,实际内存超过64MB 也按64MB返回。

这三个功能是由强到弱的,首先使用第一个功能,如果不出错,就结束,否则就使用第二个功能。如果第二个功能也出错,使用第三个功能来检测。如果都出错则机器挂起,停止运行。

BIOS 中断是实模式下的方法,所以要在进入保护模式之前检测内存。

2. BIOS 中断 0x15 的 0xe820 子功能

功能最强大的一个,能过获取内存布局。

调用前往寄存器里输入一些值,调用后就会在寄存器中返回一些值,来代表内存布局的一些信息。

6.检测物理内存的容量
6.检测物理内存的容量
使用步骤:

1.填好“调用前输入”中列出的寄存器
2.执行中断调用 int 0x15
3.在 CF 位为 0 的情况下,“返回后输出”中对应的寄存器会有对应的结果

每次调用返回一个 ARDS(地址范围描述符),所以使用这个子功能要多次调用int 0x15 中断来获取所有的内存部分的信息。
因为内存不只是DRAM,还有ROM、设备内存等。

6.检测物理内存的容量

3. BIOS 中断 0x15 的 0xe801 子功能

第二种方法,0xe801 子功能。

6.检测物理内存的容量
使用步骤:

1.将 AX 寄存器写入0xe801
2.执行中断调用 int 0x15
3.在 CF 位为 0 的情况下,“返回后输出”中对应的寄存器便会有对应的结果。

4. BIOS 中断 0x15 的 0x88 子功能

6.检测物理内存的容量
使用步骤:

1.将 AX 寄存器写入 0x88
2.执行中断调用 int 0x15
3. 在 CF 位为 0 的情况下,“返回后输出” 中对应的寄存器便会有对应的结果。

5. 实验

前面提到,BIOS 中断是实模式下的方法,所以要在进入保护模式之前检测内存。

直接改进上一篇博文中的程序。
一共 3 个程序。boot.inc、mbr.S、loader.S
boot.inc 不改动。mbr.S改动最终的跳转指令。
在 loader.S 中先检测内存容量,然后再进入保护模式。

loader.S

   %include "boot.inc"
   section loader vstart=LOADER_BASE_ADDR		; 0x900
   LOADER_STACK_TOP equ LOADER_BASE_ADDR		;栈底也定位 0x900
 ;  jmp loader_start					;跳转到下面 loader_start 处执行;;注释掉,不需要这行,从mbr.S直接跳到loader_start执行。
   
;构建gdt及其内部的描述符
   GDT_BASE:   dd    0x00000000 			;第 0 个描述符,GDT的第 0 个描述符不可用,所以设为 0
	       		dd    0x00000000

   CODE_DESC:  dd    0x0000FFFF 			;第 1 个描述符的低 4 位,看名称是代码段的段描述符
	    		dd    DESC_CODE_HIGH4			;第 1 个描述符的低高 4 位

   DATA_STACK_DESC:  dd    0x0000FFFF		;数据段和栈段的描述符
		    		 dd    DESC_DATA_HIGH4

   VIDEO_DESC: dd    0x80000007	       ;limit=(0xbffff-0xb8000)/4k=0x7;指向显存的位置,根据之前实模式布局,其基址要位 0xb8000
	       		dd    DESC_VIDEO_HIGH4  ; 此时dpl已改为0

   GDT_SIZE   equ   $ - GDT_BASE			;GDT的大小
   GDT_LIMIT   equ   GDT_SIZE -	1 			; GDT的大小减一就是 GDT 的界限,就是填在GDTR中的低16位
   times 60 dq 0					 		; 此处预留60个描述符的slot
   SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0         ; 相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0; 宏定义的选择子
   SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0	 	 ; 同上; 宏定义的选择子
   SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0		 ; 同上 ; 宏定义的选择子

	total_mem_bytes dd 0		;定义一个变量,以后用来存放内存的大小。
	
   ;以下是定义gdt的指针,前2字节是gdt界限,后4字节是gdt起始地址
   gdt_ptr  dw  GDT_LIMIT 	;GDTR的低16位
	    	dd  GDT_BASE		; GDTR的高 32 位,GDT的基地址。;gdt_ptr相当于指针。因为gdt_ptr是存放这48位的地址。
	    	
	;人工对齐:total_mem_bytes4字节+gdt_ptr6字节+ards_buf244字节+ards_nr2,共256字节
   ards_buf times 244 db 0		;ards_buf 和 ards_nr 是第一个子功能需要用的,用来存放ARDS
   ards_nr dw 0		      ;用于记录ards结构体数量
       	
loader_start:		;从 mbr.S 直接跳到此处执行。要先算的此处的内存地址
	;不在屏幕随便显示一些字符了,(●ˇ∀ˇ●)。直接开始检测内存容量。
;-------  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 打开A20
									;2 加载gdt
									;3 将cr0的pe位置1

   ;-----------------  打开A20  ----------------
   in al,0x92
   or al,0000_0010B
   out 0x92,al

   ;-----------------  加载GDT  ----------------
   lgdt [gdt_ptr]			;注意!这里用的是 [] ,就是取地址为gdt_ptr的内存中的数据,就是那48位数据


   ;-----------------  cr0第0位置1  ----------------
   mov eax, cr0
   or eax, 0x00000001
   mov cr0, eax

   ;jmp dword SELECTOR_CODE:p_mode_start	     ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转,
   jmp  SELECTOR_CODE:p_mode_start	     ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转,
					     ; 这将导致之前做的预测失效,从而起到了刷新的作用。
					     
.error_hlt:		      ;出错则挂起
   hlt
   
[bits 32]
p_mode_start:
   mov ax, SELECTOR_DATA
   mov ds, ax
   mov es, ax
   mov ss, ax
   mov esp,LOADER_STACK_TOP
   mov ax, SELECTOR_VIDEO
   mov gs, ax

   mov byte [gs:160], 'P'

   jmp $

根据代码注释和上一篇博文中的代码,可以看出本博文就是添加了 3 个内存检测的子功能的检测方法。
最后计算出内存的大小。放到edx寄存器,又复制到total_mem_bytes地址处。

前面提到mbr.S要直接跳到 loader_start:处执行,要计算它的内存地址。
loader.bin 开头先是 4 个 8 字节的段描述符,然后 60 个预留的段描述符,一个 4 字节的 total_mem_bytes,6字节的gdt_ptr ,244字节的ards_buf,2字节的ards_nr。
一共是(48+608+4+6+244+2)=768字节,也就是0x300。

所以要mbs.S不要直接跳到loader的开头执行,要跳到 LOADER_BASE_ADDR+ 0x300 的地方。

最后编译、写道虚拟硬盘执行。

dd if=mbr.bin of=路径/hd60M.img bs=512 count=1 conv=notrunc
dd if=loader.bin of=路径/hd60M.img bs=512 count=4 seek=2 conv=notrunc

注意对于loader.bin 是count=4,占有了 4 个扇区,因为大于 512字节了。
实验结果:
先输入 c 运行,然后按 ctrl+c 中断,然后输入 xp 0b00,查看0x0b00位置处的数据,
0x0b00 就是 total_mem_bytes的地址。正好是 0x2000000,就是32MB。和之前bochs的配置文件中设置内存大小(megs: 32)的一样。
6.检测物理内存的容量