操作系统如何获取物理内存容量

在Linux中有很多方法获取内存容量,如果一种方法失败,就会调用其他方法,但是这些方法的共性是调用BIOS中断的0x15实现的,分别是 0x15 的三个子功能,子功能号要放在寄存器EAX或者AX中

BIOS中断可以返回已安装的硬件信息,由于BIOS及其中断也只是一组软件,它要访问硬件也要依靠硬件提供的接口,所以,获取内存信息,其内部是通过连续调用硬件的应用程序接口(Application Program Interface,API)来获取内存信息的。另外,由于每次调用BIOS中断都是有一定的代价的(比如至少要将程序的上下文保护起来以便从中断返回时可以回到原点继续向下执行),所以尽量在一次中断中返回足量的信息,由用户程序自己挑出重点内容。

中断 0x15 子功能 0xe820

这个子功能能够获取系统的内存布局,由于系统内存各部分的类型属性不同,BIOS就按照类型属性来划分这片系统内存,所以这种查询呈迭代式,每次BIOS只返回一种类型的内存信息,直到将所有内存类型返回完毕。子功能0xE820的强大之处是返回的内存信息较丰富,包括多个属性字段,所以需要一种格式结构来组织这些数据。

内存信息的内容是用地址范围描述符来描述的,用于存储这种描述符的结构称之为地址范围描述符(ARDS)

地址范围描述符结构如下表

字节偏移量 属性名称 描述
0 BaseAddrLow 基地址的低32位
4 BaseAddrHigh 基地址的高32位
8 LengthLow 内存长度的低32位,单位字节
12 LengthHigh 内存长度的高32位,单位字节
16 Type 本段内存的类型

此结构中的字段大小都是4字节,共5个字段,所以一共是20字节,每次执行这个中断,BIOS就返回这样一个数据结构,上面是4个字段很好理解,Type字段为1表示这段内存可以被操作系统使用,为2则表示内存使用中或者被系统保留,操作系统不可以用此内存,Type字段一共就两个定义。

为什么BIOS会按照类型来返回内存信息呢,原因是这段内存可能是系统的ROM,或者ROM用到了这部分内存,设备内存映射到了这部分内存,由于某种原因,这段内存不适合标准设备使用。

由于我们在32位环境下工作,所以在ARDS结构属性中,我们只用到了低32位属性,BaseAddrLow+LengthLow是一片内存区域上限,单位是字节。正常情况下,不会出现较大的内存区域不可用的情况,除非安装的物理内存极其小。

BIOS中断只是一段函数例程,调用它就要为其提供参数,现在介绍下BIOS中断0x15的0xe820子功能需要哪些参数

EAX: 子功能号,这里输入为 0xE820

EBX: ARDS后续值,内存信息需要多次返回,EBX告诉中断下一次调用时返回哪个ARDS,第一次置0,后续不需要关注

ES:DI : ARDS缓冲区:BIOS将获取到的内存信息写入此寄存器指向的内存,每次都以ARDS格式返回

ECX: ARDS结构的字节大小:用来指示BIOS写入的字节数,调用者和BIOS都同时支持的大小是20字节

EDX: 固定为签名标记0x534d4150,此十六进制数字是字符串SMAP的ASCII码:BIOS将调用者正在请求的内存信息写入ES:DI寄存器所指向的ARDS缓冲区后,再用此签名校验其中的信息

返回后

CF: 若CF位为0表示调用未出错,CF为1,表示调用出错

EAX: 字符串SMAP的ASCII码0x534d4150

ES:DI : ARDS缓冲区地址,同输入值是一样的,返回时此结构中已经被BIOS填充了内存信息

ECX:BIOS写入到ES:DI所指向的ARDS结构中的字节数,BIOS最小写入20字节

EBX :后续值:下一个ARDS的位置。每次中断返回后,BIOS会更新此值,不需要操作

所以该中断的调用方式为

1、填写好“调用前输入”中列出的寄存器。

2、执行中断调用int 0x15。

3、在CF位为0的情况下,“返回后输出”中对应的寄存器便会有对应的结果。

中断 0x15 子功能 0xe801

这个方法虽然获取内存的方式较为简单,但是功能也不强大,只能识别最大4GB内存,不过这对于32位的地址线也够用了。

此方法检测到的内存是分别存储到两个寄存器中的,低于15MB的内存以1KB为单位来记录,记录结果保存在AX和CX中,其中AX和CX中的数据是一样的,所以在15MB空间以下的实际内存容量为 A X ∗ 1024 AX * 1024 AX1024 。AX的最大值为 0x3c00,所以知道为什么最大只能15MB了吧,16MB~4GB的内存时以64KB为单位来记录的,单位数量在寄存器BX和DX中记录

所以这个中断子功能需要的参数

AX :子功能号 0xE801

返回后

CF:CF为0表示调用未出错,CF为1,表示调用出错

AX:1KB的容量

BX:64KB的容量

CX:1KB的容量

DX:64KB的容量

功能我们介绍完了其实怎么用就很好说了,但是有没有一些疑问,比如说,为什么小于15MB的内存和大于16MB的内存要分开记录,中间的1MB去哪儿了,比如为什么AX和CX一样,BX和DX一样?

我们先解决第一个问题,这是一个著名的历史遗留问题,80286拥有24位地址线,其寻址空间是16MB。当时有一些ISA设备要用到地址15MB以上的内存作为缓冲区,也就是此缓冲区为1MB大小,所以硬件系统就把这部分内存保留下来,操作系统不可以用此段内存空间。保留的这部分内存区域就像不可以访问的黑洞,这就成了内存空洞memory hole。现在虽然很少很少能碰到这些老ISA设备了,但为了兼容,这部分空间还是保留下来,只不过是通过BIOS选项的方式由用户自己选择是否开启。BIOS厂商不同,一般的菜单选项名称也不相同,不过大概意思都差不多。

起初这个 0x801的中断子功能也是为了支持ISA服务的,如果内存的容量大于16MB,那么 A X ∗ 1024 AX*1024 AX1024必然是小于等于15MB,而 B X ∗ 64 ∗ 1024 BX*64*1024 BX641024肯定大于0,所以很容易就检测出内存空洞。当然如果物理内存在16MB以下,此方法就不灵了,但检测到的内存依然会小于实际内存1MB。

至于第二个问题,我们可以理解为,BIOS就是这么设计的

总结,此中断的调用步骤为:

1、将AX寄存器写入0xE801。

2、执行中断调用int 0x15。

3、在CF位为0的情况下,“返回后输出”中对应的寄存器便会有对应的结果。

中断 0x15 子功能 0x88

这个方法最简单,得到的结果也最简单,简单到只能识别最大64MB的内存。即使内存容量大于64MB,也只会显示63MB,大家可以自己在bochs中试验下。为什么只显示到63MB呢?因为此中断只会显示1MB之上的内存,不包括这1MB,咱们在使用的时候记得加上1MB。

所以这个中断子功能需要的参数

AX :子功能号 0x88

返回后

CF:CF为0表示调用未出错,CF为1,表示调用出错

AX:1KB的容量,内存空间1MB之上的连续单位数量,不包括低端1MB内存。故内存大小为AX*1024字节+1MB

此中断的调用步骤为:

1、将AX寄存器写入0x88。

2、执行中断调用int 0x15。

3、在CF位为0的情况下,“返回后输出”中对应的寄存器便会有对应的结果。

实战

total_mem_bytes dd 0 

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;第一种获取内存的方式
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
ards_buf times 244 db 0
ards_hr  dw 0            ; 用于记录ARDS的数量

loader_start:
	xor ebx, ebx         ; ebx需要清零
	mov edx, 0x534d4150  ; edx需要赋值一次
	mov di,  ards_buf    ; di 需要给到保存ARDS的地址
.e820_mem_get_loop:      ; 循环获取每个ARDS内存范围描述结构
	mov eax, 0x0000e820  ; 执行int 0x15后,eax值变为0x534d4150,所以没次都需要更新子功能号
	mov ecx, 20          ; ARDS描述符结构大小是20字节
	int 0x15
	jc .e820_failed_so_try_e801 ;判断CF是否出错    
	add di,cx            ; 使di增加20字节指向缓冲区中新的ARDS结构位置
	inc word [ards_nr]   ; 记录ARDS数量
	cmp ebx,0            ; 若ebx为0说明ards全部返回
	jnz .e820_mem_get_loop ; 如果不为0则循环执行
	
	;在所有ards结构中,找出(base_add_low + length_low)的最大值,即内存的容量
	 mov cx, [ards_nr]  ; 将CX装入ARDS的数量
	 mov ebx, ards_buf  ; 将BX装入ARDS的头地址
	 xor edx, edx       ; 将edx清零,用来保存最长的内存大小
	 
	
	; 找出最大内存块,无需判断type是否为1,最大的内存块一定是可被使用的
	.find_max_mem_area:
		mov eax, [ebx]
		add eax, [ebx+8]
		add ebx, 20
		cmp edx, eax
		jge .next_ards   ; 大于等于则跳转
		mov eax,edx     ; 否则将 eax 赋值给 edx
	.next_ards:
		loop .find_max_mem_area
		jmp .mem_get_ok   ;当执行完这个循环后,跳转到拿到内存最大值的程序
        
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;若CF为1则表示有错误发生,执行第二种获取内存的方式
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
.e820_failed_so_try_e801:
	 mov ax,0xe801     
	 int 0x15     
	 jc .e801_failed_so_try88   ; 判断CF是否出错 
	 ; 先算出低15MB内存
	 mov cx,0x400               ; cx和ax值一样,cx用作乘数,即1KB 
     mul cx  				    ; 将cx乘以1k,结果保存在DX:CX中
     shl edx,16                 ; 
     and eax,0x0000FFFF         ; 
     or edx,eax                 ; 将 dx << 16 + ax 就是低15MB内存的大小
     add edx,0x100000           ; ax只是15MB,故要加1MB 
     mov esi,edx                ; 将edx的结果保存在esi中
     ; 再算出16MB以上的内存
     xor eax,eax
	 mov ax,bx
	 mov ecx,0x10000
	 mul ecx                    ; 和上面的方法一样 
	 add esi,eax                ; 将高位和低位相加,由于此方法只能测出4GB以内的内存,故32位eax足够了
	 mov edx,esi                ; edx为总内存大小
     jmp .mem_get_ok 
	 
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;若CF为1则表示有错误发生,执行第三种获取内存的方式
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
.e801_failed_so_try88:
	 mov  ah, 0x88     
	 int  0x15      
	 jc .error_hlt               ;判断CF是否出错 
	 and eax,0x0000FFFF
     mov cx,0x400
	 mul cx
	 shl edx,16
	 or edx,eax                  ; 这一部分的计算和上面一样
	 add edx,0x100000            ; 加1MB的内存

.mem_get_ok:     
	mov [total_mem_bytes],edx   ; 将获取到的内存保存到一段内存中 

.error_hlt:
	jmp $                        ; 获取内存失败的话就此卡死

一些指令的解释

inc

在x86汇编中,inc指令用于将一个操作数的值增加1。它可以作用于寄存器,内存变量和偏移量。inc指令通常用于循环计数器和计算地址偏移等场景。例如:

mov eax, 5      ; 将eax寄存器设置为5
inc eax         ; 将eax寄存器的值增加1,此时eax的值为6
mov dword ptr [ebx], 10     ; 将内存变量ebx赋值为10
inc dword ptr [ebx]         ; 增加ebx内存变量的值,此时ebx的值为11
lea edx, [eax+4]            ; 将eax寄存器值加4得到新地址并存储到edx寄存器
inc edx                     ; 增加edx寄存器的值,此时edx的值为eax+5

cmp

cmp指令用于比较两个操作数的大小或相等性,它不会修改任何寄存器或存储器中的数据。cmp指令将第一个操作数减去第二个操作数,并根据结果更新标志寄存器。如果结果为零,则说明两个操作数相等;如果结果为负数,则说明第一个操作数小于第二个操作数;如果结果为正数,则说明第一个操作数大于第二个操作数。cmp指令在条件跳转和条件执行指令中经常被使用。他影响的标志寄存器的位为

  1. Zero Flag (ZF) - 如果cmp操作数相等,则置位。
  2. Sign Flag (SF) - 如果cmp操作数的符号不同,则置位。
  3. Carry Flag (CF) - 如果无符号数的相减结果产生了进位,则置位。
  4. Overflow Flag (OF) - 如果有符号数的相减结果产生了溢出,则置位。

因此,cmp指令会影响这些标志寄存器位的值,而不是直接修改任何寄存器的位。

jnz

jnz指令是x86汇编中的一个条件分支指令,其全称为"jump if not zero",意思是如果标志位中的零标志(ZF)为0,则跳转到指定的代码地址,否则继续执行下一条指令。该指令通常用于实现"循环"和"条件分支"等功能。

jge

jge(jump if greater than or equal to)指令用于比较两个操作数的大小关系,并根据结果跳转到指定的代码位置,如果第一个操作数大于等于第二个操作数,则跳转到指定的代码位置。

例如,以下代码使用jge指令:

cmp eax, ebx   ; 比较eax和ebx的值
jge label      ; 如果eax大于等于ebx,跳转到标记为label的代码位置

jge指令通常用于流程控制和条件判断,可以根据特定的条件改变代码的执行顺序,实现不同的程序逻辑。

jc

JC(Jump if Carry)指令在x86汇编中用于根据进位标志(CF)的值进行条件跳转。如果CF被设置为1,那么JC指令将跳转到指定的标签或地址。否则,程序将继续执行下一条指令。JC指令通常用于无符号数的比较和操作,例如,检查两个无符号数是否存在溢出或进位。

mul

mul指令用于将两个数相乘。它是通用寄存器和内存操作数的,例如:

  • mul reg - 将AX和指定寄存器中的值相乘,结果存储在DX:AX中。
  • mul mem - 将AX和指定内存地址中的值相乘,结果存储在DX:AX中。
  • mul reg/mem8 - 将AL和指定寄存器或内存地址中的8位值相乘,结果存储在AX中。
  • mul reg/mem16 - 将AX和指定寄存器或内存地址中的16位值相乘,结果存储在DX:AX中。
  • mul reg/mem32 - 将EAX和指定寄存器或内存地址中的32位值相乘,结果存储在EDX:EAX中。

mul指令可以用于执行高精度乘法和除法运算,或者在计算机图形学、信号处理、加密算法等应用中使用。

你可能感兴趣的:(Linux,linux,运维,服务器)