操作系统还原真相 第六章 内存容量检测

一、相关知识

BIOS的0x15中断有3个子功能:0xe820h、0xe801h、0x88h,这三个子功能都可以用来获取物理内存

这三个子功能的区别是:

  • e820h返回内存布局,信息量大
  • e801h返回内存容量
  • 88h最简单,功能也最弱
1、0xe820h子功能
  1. 地址范围描述符

e820将物理内存探测的结果以地址范围描述符的格式放在内存中。地址范围描述符共计20字节,格式是:

字节范围描述
0~7内存块基地址
8~15这块内存的大小
16~20这块内存的类型
  1. e820h的调用参数

调用前输入:

  • eax:子功能编号,这里我们填入0xe820

  • edx:534D4150h(ascii字符”SMAP”),签名,约定填”SMAP”

  • ebx:每调用一次int $0x15,ebx会加1。当ebx为0表示所有内存块检测完毕。(重要!看后面的案例会明白如何使用)

  • ecx:存放地址范围描述符的内存大小,至少要设置为20。

  • es:di:告诉BIOS要把地址描述符写到这个地址。

返回值输出:

  • CF标志位:若中断执行失败,则置为1。

  • eax:值是534D4150h(“SMAP”)

  • es:di:中断不改变该值,值与参数传入的值一致

  • ebx:下一个中断描述符的计数值(见后面的案例)

  • ecx:返回BIOS写到cs:di处的地址描述符的大小(应该就是20吧?)

  • ah:若发生错误,表示错误码

2、0xe801h子功能

调用前输入:

  • ax:子功能号,这里填入0xe801。

返回值输出:

  • CF标志位:若中断执行失败,则置为1。

  • ax:以1KB为单位,只显示15MB以下的内存容量,故最大值为0x3c00,即ax表示的最大内存容量为0x3c00 * 1024=15MB。

  • bx:以64KB为单位,内存空间16MB~4GB中连续的单位数量,即内存大小为bx * 1024 * 0x3c00。

  • cx:同ax

  • dx:同bx

3、0x88子功能

调用前输入:

  • ah:子功能号,这里填入0x88。

返回值输出:

  • CF标志位:若中断执行失败,则置为1。

  • ax:以1KB为单位,内存空间1MB以上的连续的单位数量,不包括低端的1MB内存,故内存大小为ax*1024+1MB。

二、实验记录

1、实验目的

在进入保护模式之前,我们应该在实模式下获取硬盘的容量大小,方便进入保护模式后的编程。

2、实验步骤

我们通过BIOS的0x15中断的三个子功能0xe820h、0xe801h、0x88h来获取内存的容量。

  1. 先使用0xe820获取内存容量:
  • 由于e820将物理内存探测的结果以地址范围描述符的格式放在内存中,因此我们需要定义一个缓冲区用来装0xe820子功能返回的ARDS结构体以及用于记录 ARDS 结构体数量的变量。
;ards_buf为缓冲区地址,缓冲区用来装0xe820子功能返回的ARDS结构体
ards_buf times 244 db 0 
ards_nr dw 0 ;大小2个字节,用于记录 ARDS 结构体数量
  • 循环调用0xe820子功能,获取每个 ARDS 内存范围描述结构
;;;;;;先使用0xe820子功能;;;;;;;;;
;;;;;先进行相关寄存器的输入;;;;;;;;;;
xor ebx, ebx   ; 异或运算,第一次使用0xe820子功能ebx要清0
mov edx, 0x534d4150 ;edx 只赋值一次,循环体中不会改变
mov di, ards_buf  ;ES:DI是ards结构缓冲区的指针,所以将ards缓冲区首地址赋给di
		  ;es已在mbr.s赋值完毕了,这里不用再赋值.
;;;;;循环调用0xe820子功能,获取每个 ARDS 内存范围描述结构;;;;;;
.e820_mem_get_loop: 
mov eax, 0x0000e820 ;执行int Oxl5 后,eax 值变为 Ox534d4150,
; 所以每次执行int前都要更新为子功能号
mov ecx, 20  ; 1个ards结构体大小为20字节
int 0x15;

jc .e820_failed_so_try_e801 
;cf 位为 1则 有错误发生,尝试 Oxe801子功能 
;;;;;cf为0,收集输出寄存器的值;
add di, cx ;使di 增加 20 字节指向缓冲区中新的 ARDS 结构位置
inc word [ards_nr] ;记录 ARDS 数量

;若ebx为0且cf不为1,这说明ards全部返回,退出循环
;若ebx不等于0,代码将会返回.e820_mem_get_loop,继续调用e820子功能返回ards结构体
cmp ebx, 0 
jnz .e820_mem_get_loop
  • 在所有 ards 结构中 ,找出(base_add low + length_low)的最大值,即内存的容量,在每个adrs结构中,0~7为是这块内存块基地址base_add low,8~15为是内存的大小length_low
;在所有 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,移了32位 
add eax, [ebx+8] ;base_add_low+length_low 
add ebx, 20 ;指向缓冲区中下-个 ARDS 结构
cmp edx, eax 
;简单选择排序,找出最大,edx寄存器始终记录最大的内存容量
jge .next_ards  ;edx大于eax就访问下一个ards结构体 
mov edx, eax    ;小于就记录在edx中
.next_ards: 
   loop .find_max_mem_area ;cx记录了ards结构体数量,cx为0说明已经循环了20次,最大的内存
;已经在edx中了,则退出循环。
   jmp .mem_get_ok ;将最大内存容量放入total_mem_byte中
  1. 如果0xe820失败,尝试0xe801获取内存容量:
  • 先算出低15MB的内存,将其转换为以 byte 为单位
;;;;;;;;int 15h ax = EBOlh 获取内存大小,最大支持4G
;返回后, ax ex 一样,以 KB 为单位, bx dx 值一样,以 64KB 为单位
;在 ax ex 寄存器中为低16MB ,在 bx dx 寄存器中为16MB到4GB

.e820_failed_so_try_e801:
   mov ax,0xe801 
   int 0x15 ;调用0xe810子功能
   jc .e801_failed_so_try88   ;cf为1表示出错
 
;若成功,先算出低 15MB 的内存
;ax ex 中是以 KB 为单位的内存数量,将其转换为以 byte 为单位
   mov cx, 0x400;  cx ax 值一样, cx 用作乘数 0x400字节=1KB
   mul cx        ;cx*ax 16位*16位 乘积32位,高16位dx保存,低16位ax保存
   shl edx,16 
   and eax,0x0000FFFF 
   or edx,eax  ;;;获得乘积,即获得内存容量字节数-1MB
   add edx, 0x100000   ;ax只是15MB,故要加1MB
   mov esi,edx  ;低15MB内存容量存入esi
  • 再将 16MB 以上的内存转换为 byte 为单位
; 再将 16MB 以上的内存转换为 byte 为单位
;寄存器 bx dx 中是以 64KB 为单位的内存数量
   xor eax, eax 
   mov ax, bx 
   mov ecx, 0x10000 
   mul ecx 
;0x10000 十进制为64KB
; 32 位乘法,默认的被乘数是 eax ,积为 64位
;高 32 位存入 edx ,低 32 位存入 eax
   add esi, eax 
;由于此方法只能测出 4GB 以内的内存,故 32位eax 足够了
; edx 肯定为0,只加 eax 便可
   mov edx, esi ;edx 为总内存大小
   jmp .mem_get_ok 
  1. 如果0xe801失败,尝试0x88获取内存容量:
;;;;;;;int 15h ah = Ox88 获取内存大小,只能获取 64MB 之内
.e801_failed_so_try88: 
;int 15 后, ax 存入的是以 KB 为单位的内存容量
   mov ah, Ox88 
   int 0x15 
   jc .error_hlt 
   and eax,0x0000FFFF 
   mov cx, 0x400 
;0x400 等于 1024 ,将 ax 中的内存容量换为以 byte 为单位
   mul cx   
;16 位乘法,被乘数是 ax ,积为 32 位。积的高 16 位在 dx
;积的低 16 位在 ax

   shl edx, 16 ;把 dx 移到高 16
   or edx, eax ;把积的低 16 位组合到 edx ,为 32 位的积
   add edx, 0x100000 ; Ox88 子功能只会返回 lMB 以上的内存
   ;故实际内存大小要加上 lMB
  1. 如果三种方式都失败,则悬停在此处

    .error_hlt:
       jmp $
    
  2. 最后将存在edx寄存器中的最大容量放入[total_mem_bytes]内存当中

total_mem_bytes的定义:

;total_mem_bytes用于保存内存容量,以字节为单位
;当前偏移loader.bin文件头0x200字节
;0x200字节=(60+4)*8=512字节
;loader.bin加载地址是0x900
;故total_mem_bytes代表的内存地址是0xb00
total_mem_bytes dd 0;  大小4个字节
.mem_get_ok: 
   mov [total_mem_bytes ], edx   ;3种子功能均是把最大容量放入edx
3、实验代码

现在的内存图如下所示:(下述单位都是以字节为单位的)

在这里插入图片描述

可见现在loader_start为0x900+64*8+(4+6+244+2)=0x900+0x300;

1、mbr.s

修改mbr.s

 jmp LOADER_BASE_ADDR;

 jmp LOADER_BASE_ADDR+0x300;
2、loader.s
;------------------------
%include "boot.inc" 
section loader vstart=LOADER_BASE_ADDR 
LOADER_STACK_TOP equ LOADER_BASE_ADDR  ;相同内存地址,地址之下便是栈


;构建 gdt 及其内部的描述符
GDT_BASE: dd 0x00000000       ;第0个段描述符不可用
		dd 0x00000000  

CODE_DESC: dd 0x0000FFFF   ;代码段描述符
		 dd DESC_CODE_HIGH4 
 
DATA_STACK_DESC: dd 0x0000FFFF ;栈段描述符 栈段和数据段共用一个描述符 均向上扩展
		 dd DESC_DATA_HIGH4  
 
VIDEO_DESC: dd 0x80000007   ; limit=(0xbffff-0xb8000)/4k=0x7  故段界限为7
			dd  DESC_VIDEO_HIGH4  ;此时dpl为0
		

	 
GDT_SIZE equ $ - GDT_BASE ; 先是通过地址差来获得 GDT的大小,进而用 GDT大小减1得到了段界限
GDT_LIMIT equ GDT_SIZE - 1   ;用于构建GDTR的段界限


times 60 dq 0 ;此处预留 60 个描述符的空位


;;;;;;;;;;;;;;;;;;新增一句代码;;;;;;;;;;;;;

;total_mem_bytes用于保存内存容量,以字节为单位
;当前偏移loader.bin文件头0x200字节
;0x200字节=(60+4)*8=512字节
;loader.bin加载地址是0x900
;故total_mem_bytes代表的内存地址是0xb00
total_mem_bytes dd 0;  大小4个字节

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;以下是构建代码段、数据段、显存段选择子
SELECTOR_CODE equ (0x0001 << 3) + TI_GDT + RPL0 
;相当于[(CODE_DESC - GDT_BASE) /8 ]<<3+ TI_GDT + RPL0 
;内存地址的编号是一个存储单元8比特,这里CODE_DESC - GDT_BASE应该等于8
;书里的备注应该写掉了"<<3"
SELECTOR_DATA equ (0x0002<< 3) + TI_GDT + RPL0 
SELECTOR_VIDEO equ (0x0003 << 3) + TI_GDT + RPL0 

;以下是 gdt 的指针即GDTR,前2字节是gdt界限,后4字节是gdt起始地址 后面代码使用lgdt指令时会用上
gdt_ptr dw GDT_LIMIT 
dd GDT_BASE 


;ards_buf为缓冲区地址,缓冲区用来装0xe820子功能返回的ARDS结构体
;一个ARDS结构体20字节,本次实验测试一共返回了6个。
;虽然ards_buf不需要244字节,定义244字节是为了下面代码的loader_start的地址长得好看
;人工对齐:total_mem_bytes+gdt_ptr+ards_buf+ards_nr=4+6+244+2=256,共256字节
;256字节=0x100字节
ards_buf times 244 db 0 
ards_nr dw 0 ;大小2个字节,用于记录 ARDS 结构体数量

; loader_start地址为0x900+0x300,偏移地址为0x300
;total_mem_bytes的文件内偏移为0x200,人工对齐又0x100,故0x300
   loader_start: 

; int 15h eax = 0000E820h ,edx = 534D4150h ('SMAP')获取内存布局

;;;;;;先使用0xe820子功能;;;;;;;;;
;;;;;先进行相关寄存器的输入;;;;;;;;;;
xor ebx, ebx   ; 异或运算,第一次使用0xe820子功能ebx要清0
mov edx, 0x534d4150 ;edx 只赋值一次,循环体中不会改变
mov di, ards_buf  ;ES:DI是ards结构缓冲区的指针,所以将ards缓冲区首地址赋给di
		  ;es已在mbr.s赋值完毕了,这里不用再赋值.
;;;;;循环调用0xe820子功能,获取每个 ARDS 内存范围描述结构;;;;;;
.e820_mem_get_loop: 
mov eax, 0x0000e820 ;执行int Oxl5 后,eax 值变为 Ox534d4150,
; 所以每次执行int前都要更新为子功能号
mov ecx, 20  ; 1个ards结构体大小为20字节
int 0x15;

jc .e820_failed_so_try_e801 
;cf 位为 1则 有错误发生,尝试 Oxe801子功能 
;;;;;cf为0,收集输出寄存器的值;
add di, cx ;使di 增加 20 字节指向缓冲区中新的 ARDS 结构位置
inc word [ards_nr] ;记录 ARDS 数量

;若ebx为0且cf不为1,这说明ards全部返回,退出循环
;若ebx不等于0,代码将会返回.e820_mem_get_loop,继续调用e820子功能返回ards结构体
cmp ebx, 0 
jnz .e820_mem_get_loop

;在所有 ards 结构中
;找出(base_add low + length_low )的最大值,即内存的容量
mov cx, [ards_nr] ;遍历每一个 ARDS 结构体,循环次数是 ARDS 的数量
mov ebx, ards_buf 
xor edx, edx   ;ebx为最大容量,在此先清0
.find_max_mem_area: 
;无需判断 type 是否为1,最大的内存块一定是可被使用的 
mov eax, [ebx] ;缓冲区首地址就是第一个结构体的base_add_low,移了32位 
add eax, [ebx+8] ;base_add_low+length_low 
add ebx, 20 ;指向缓冲区中下-个 ARDS 结构
cmp edx, eax 
;简单选择排序,找出最大,edx寄存器始终记录最大的内存容量
jge .next_ards  ;edx大于eax就访问下一个ards结构体 
mov edx, eax    ;小于就记录在edx中
.next_ards: 
   loop .find_max_mem_area ;cx记录了ards结构体数量,cx为0说明已经循环了20次,最大的内存
;已经在edx中了,则退出循环。
   jmp .mem_get_ok ;将最大内存容量放入total_mem_byte中

;;;;;;;;int 15h ax = EBOlh 获取内存大小,最大支持4G
;返回后, ax ex 一样,以 KB 为单位, bx dx 值一样,以 64KB 为单位
;在 ax ex 寄存器中为低16MB ,在 bx dx 寄存器中为16MB到4GB

.e820_failed_so_try_e801:
   mov ax,0xe801 
   int 0x15 ;调用0xe810子功能
   jc .e801_failed_so_try88   ;cf为1表示出错
 
;若成功,先算出低 15MB 的内存
;ax ex 中是以 KB 为单位的内存数量,将其转换为以 byte 为单位
   mov cx, 0x400;  cx ax 值一样, cx 用作乘数 0x400字节=1KB
   mul cx        ;cx*ax 16位*16位 乘积32位,高16位dx保存,低16位ax保存
   shl edx,16 
   and eax,0x0000FFFF 
   or edx,eax  ;;;获得乘积,即获得内存容量字节数-1MB
   add edx, 0x100000   ;ax只是15MB,故要加1MB
   mov esi,edx  ;低15MB内存容量存入esi

; 再将 16MB 以上的内存转换为 byte 为单位
;寄存器 bx dx 中是以 64KB 为单位的内存数量
   xor eax, eax 
   mov ax, bx 
   mov ecx, 0x10000 
   mul ecx 
;0x10000 十进制为64KB
; 32 位乘法,默认的被乘数是 eax ,积为 64位
;高 32 位存入 edx ,低 32 位存入 eax
add esi, eax 
;由于此方法只能测出 4GB 以内的内存,故 32位eax 足够了
; edx 肯定为0,只加 eax 便可
   mov edx, esi ;edx 为总内存大小
   jmp .mem_get_ok 

;;;;;;;int 15h ah = Ox88 获取内存大小,只能获取 64MB 之内
.e801_failed_so_try88: 
;int 15 后, ax 存入的是以 KB 为单位的内存容量 100 mov ah, Ox88 
   int 0x15 
   jc .error_hlt 
   and eax,0x0000FFFF 
   mov cx, 0x400 
;0x400 等于 1024 ,将 ax 中的内存容量换为以 byte 为单位
   mul cx   
;16 位乘法,被乘数是 ax ,积为 32 位。积的高 16 位在 dx
;积的低 16 位在 ax

   shl edx, 16 ;把 dx 移到高 16
   or edx, eax ;把积的低 16 位组合到 edx ,为 32 位的积
   add edx, 0x100000 ; Ox88 子功能只会返回 lMB 以上的内存
;故实际内存大小要加上 lMB
.error_hlt:
   jmp $
.mem_get_ok: 
   mov [total_mem_bytes ], edx   ;3种子功能均是把最大容量放入edx
    ; 一一一一一一一一一一 准备进入保护模式 一一一一一一一一一一一一一一-
;1 打开 A20
;2 加载 gdt
;3 将cr0 的 pe 位置1 

;一一一一一一一-打开 A20 一一一一一
   in al,0x92
   or al, 0000_0010B 
   out 0x92,al 
 
;一一一一一一一一加载 GDT (也就是设置好gdtr,gdtr记录着gdt的起始地址)一一一一一一一-
   lgdt [gdt_ptr ] 

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

   jmp dword SELECTOR_CODE:p_mode_start  ;刷新流水线

[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:0xA0], 'P' 

	jmp $
4、实验记录
1、编译mbr.s文件
nasm -o mbr.bin mbr.s
2、编译loader.s文件
nasm -o loader.bin loader.s
3、将mbr.bin刻入第0扇区
dd if=/home/llh/bochs/mbr.bin of=/home/llh/bochs/hd60M.img bs=512 count=1 seek=0 conv=notrunc
4、将loader.bin刻入第2扇区
dd if=/home/llh/bochs/loader.bin of=/home/llh/bochs/hd60M.img bs=512 count=2 seek=2 conv=notrunc
5、模拟bochs
./bin/bochs -f bochsrc.disk
5、实验结果

效果展示图:

在这里插入图片描述

在这里插入图片描述

  • 27
    点赞
  • 52
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值