当前位置: 首页 > news >正文

《操作系统真象还原》第五章(1)——获取内存容量

文章目录

    • 前言
    • 获取物理内存容量
      • 利用 BIOS 中断 0x15 子功能 0xe820 获取内存
      • 利用 BIOS 中断 0x15 子功能 0xe801 获取内存
      • 利用 BIOS 中断 0x15 子功能 0x88 获取内存
      • 内存容量检测代码
      • 编译运行
    • 结语

前言

经过上一章的学习实践,目前我们已经进入32位保护模式了。本章的标题是保护模式进阶,向内核迈进,那么就是要在保护模式下进一步编程。

郑钢老师在上一章提到,之前的四章偏向于理论,代码量相对较小,接下来的章节代码量会更多,这对我而言会是一个挑战。

这章内容较多,我打算花2-3天时间完成学习,博客也会分成若干部分,每部分完成一节内容。

第四章知识笔记博客链接:《操作系统真象还原》第四章(1)-CSDN博客

第四章实践部分博客链接:《操作系统真象还原》第四章(2)-CSDN博客

本章参考博客链接:《操作系统真象还原》第五章 ---- 轻取物理内存容量 启用分页畅游虚拟空间 力斧直斩内核先劈一角 闲庭信步摸谈特权级_真象还原 分页 info tab-CSDN博客


获取物理内存容量

保护模式可以访问4GB内存,为了维护这么大的内存空间,就要有相应的内存管理体系。想要管理内存,首先就要先获取内存容量,bios可以通过0x15中断获取内存信息,bios是在实模式下运行的,我们要在进入保护模式之前获取内存容量。

利用 BIOS 中断 0x15 子功能 0xe820 获取内存

子功能 0xE820 的强大之处是返回的内存信息较丰富,包括多个属性字段,所以需要一种格式结构来组织这些数据。内存信息的内容是用地址范围描述符来描述的,用于存储这种描述符的结构称之为地址范围描述符(Address Range Descriptor Structure,ARDS)。

ards长20字节,分为5段,每段4字节,每次调用0x15就会返回一个这样结构的数据。

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

关于type字段

Type 值名 称描 述
1AddressRangeMemory这段内存可以被操作系统使用
2AddressRangeReserved内存使用中或者被系统保留,操作系统不可以用此内存
其他未定义未定义,将来会用到,目前保留。但是需要操作系统一样将其视为ARR(AddressRangeReserved)

关于0x15中断的0xe820子功能的参数

调用或返回寄存器或状态位参 数 用 途
输入EAX子功能号:EAX 寄存器用来指定子功能号,此处输入为 0xE820
EBXARDS 后续值:内存信息需要按类型分多次返回,由于每次执行一次中断都只返回一种类型内存的 ARDS 结构,所以要记录下一个待返回的内存 ARDS,在下一次中断调用时通过此值告诉 BIOS 该返回哪个 ARDS,这就是后续值的作用。第一次调用时一定要置为 0,EBX具体值我们不用关注,字取决于具体 BIOS 的实现。每次中断返回后,BIOS 会更新此值
ES:DIARDS 缓冲区:BIOS 将获取到的内存信息写入此寄存器指向的内存,每次都以 ARDS 格式返回
ECXARDS 结构的字节大小:用来指示 BIOS 写入的字节数。调用者和 BIOS 都同时支持的大小是 20 字节,将来也许会扩展此结构
EDX固定为签名标记 0x534d4150,此十六进制数字是字符串 SMAP 的 ASCII 码:BIOS 将调用者正在请求的内存信息写入 ES:DI 寄存器所指向的 ARDS 缓冲区后,再用此签名校验其中的信息
输出CF 位若 CF 位为 0 表示调用未出错,CF 为 1,表示调用出错
EAX字符串 SMAP 的 ASCII 码 0x534d4150
ES:DIARDS 缓冲区地址,同输入值是一样的,返回时此结构中已经被BIOS 填充了内存信息
ECXBIOS 写入到 ES:DI 所指向的 ARDS 结构中的字节数,BIOS 最小写入 20 字节
EBX后续值:下一个 ARDS 的位置。每次中断返回后,BIOS 会更新此值,BIOS 通过此值可以找到下一个待返回的 ARDS 结构,咱们不需要改变 EBX 的值,下一次中断调用时还会用到它。在 CF 位为 0 的情况下,若返回后的 EBX 值为 0,表示这是最后一个 ARDS 结构

此中断的调用步骤如下。

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

利用 BIOS 中断 0x15 子功能 0xe801 获取内存

这种方法相对简单,但是只能识别4GB内存,而且数据放到两个寄存器里。低于 15MB 的内存以 1KB 为单位大小来记录,单位数量在寄存器 AX 和 CX 中记录,其中 AX 和 CX 的值是一样的,所以在 15MB 空间以下的实际内存容量=AX*1024。AX、CX 最大值为 0x3c00,即 0x3c00*1024=15MB。16MB~4GB 是以 64KB 为单位大小来记录的,单位数量在寄存器 BX 和 DX 中记录,其中 BX 和 DX 的值是一样的,所以 16MB 以上空间的内存实际大小=BX*64*1024

为什么要分成两部分,分界线是16mb?为了兼容有24根地址总线的80286cpu,此cpu寻址范围是16mb,当时有一些isa设备要利用15-16mb空间作为缓冲区,出现了内存空洞。后续cpu为了兼容286保留了这一特性。

相关参数如下

调用或返回寄存器或状态位描述
输入AX子功能号:0xE801
输出CF若 CF 位为 0 表示调用未出错,CF 为 1,表示调用出错
AX以 1KB 为单位,只显示 15MB 以下的内存容量,故最大值为 0x3c00,即AX 表示的最大内存为 0x3c00*1024=15MB
BX以 64KB 为单位,内存空间 16MB~4GB 中连续的单位数量,即内存大小为 BX*64x1024 字节
CX同AX
DX同BX

此中断的调用步骤如下。

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

利用 BIOS 中断 0x15 子功能 0x88 获取内存

最后一个获取内存的方法也同样是 BIOS 0x15 中断,子功能号是 0x88。该方法使用最简单,只能识别最大 64MB 的内存。使用的时候结构要加上1MB。

输入:AH 子功能号:0x88

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

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

此中断的调用步骤如下。

(1)将 AX 寄存器写入 0x88。

(2)执行中断调用 int 0x15。

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

内存容量检测代码

关于jxx组指令的参考博客:汇编跳转指令: JMP、JECXZ、JA、JB、JG、JL、JE、JZ、JS、JC、JO、JP 等_jg指令-CSDN博客

%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
	LOADER_STACK_TOP equ LOADER_BASE_ADDR
	jmp loader_start
;-------------------- 这部分完成GDT和4个段描述符的构建 --------------------------------
;编译后程序的地址是越来越高的,所以代码要先开辟低地址再开辟高地址。8字节64位是一个段描述符。dd命令开辟出一个4字节32位空间。
GDT_BASE:
	dd 0x00000000
	dd 0x00000000		;第一个段描述符无法使用,开辟出来空着
CODE_DESC:
	dd 0x0000ffff		;前四位是16位段基址,设置为0,后四位是16位段界限,设置为1。高位在前低位在后
	dd DESC_CODE_HIGH4	;提前在配置文件中写好的高32位
DATA_STACK_DESC:
	dd 0x0000ffff
	dd DESC_DATA_HIGH4	;数据和栈用相同的段描述符,具体说明看博客
VIDEO_DESC:
	dd 0x80000007		;这部分请看博客里的说明。
	dd DESC_VIDEO_HIGH4
;-------------------- 这部分完成GDT大小的计算,同时预留出60个段描述符的空间 --------------------------------
;这部分为加载gdt做准备。dq定义4字8字节64位。times是nasm提供的伪指令,作用是循环。以后我们还要向gdt里添加别的表符,这里提前留出位置。
	GDT_SIZE equ $-GDT_BASE
	GDT_LIMIT equ GDT_SIZE-1
	times 59 dq 0
	times 5 db 0
;-------------------- 这部分完成构建选择子 --------------------------------
;选择子放在段寄存器里,16位大小,高13位是gdt的索引,第2位是ti位,指示索引是gdt的还是ldt的,0、1两位是特权级位。
	SELECTOR_CODE equ (0x0001<<3)+TI_GDT+RPL0	;0001就是下标1,左移3位相当于*8,因为一个表项是8字节
	SELECTOR_DATA equ (0x0002<<3)+TI_GDT+RPL0
	SELECTOR_VIDEO equ (0x0003<<3)+TI_GDT+RPL0
;-------------------- 这部分设置保存内存容量的标号--------------------------------
;关于这个标号的地址,loader.S的起始地址是0x900,这行前面有64个8字节的段描述符,所以这里是0x900+0x200=0xb00
	total_mem_bytes dd 0	;初始为0,最终变成总内存容量
;-------------------- 这部分完成定义GDT指针 --------------------------------
gdt_ptr:
	dw GDT_LIMIT	;前2字节是gdt的界限
	dd GDT_BASE		;后4字节是gdt的起始位置
;-------------------- 这部分实现三种获取内存容量的方法 --------------------------------
;注意我们这次删除了实模式下打印字符串的功能
	ards_buf times 244 db 0	;设置ards缓冲区,存放ards
	ards_nr dw 0		;用于记录ards结构体的数量
				;这两行开辟256字节的空间,人工对齐

loader_start:
;-------------------- 方法1:利用0xe820获取内存 --------------------------------
;以下部分,通过0xe801获取所有的ards
	xor ebx,ebx		;用异或置零,初始ebx设置为0,后续我们不需要再处理
	mov edx,0x534d4150	;固定值
	mov di,ards_buf		;es:di指向缓冲区,es在mbr设置,这里修改di即可
.e820_mem_get_loop:
	mov eax,0x0000e820	;因为执行完int 0x15后eax,ebx,ecx会变化,所以每次循环都要重新设置
	mov ecx,20		;返回的字节数,固定为20
	int 0x15
	jc .e820_failed_so_try_e801 ;如果e820失败,尝试e801
	add di,cx		;+20字节指向下一个ards
	inc word [ards_nr]	;记录ards数量
	cmp ebx,0		;如果ebx=0且cf=0,所有ards全部返回
	jnz .e820_mem_get_loop 	;如果ebx!=0,继续循环
;以下部分,遍历ards,找到最大的32位基地址+内存长度,即为最大内存容量
	mov cx,[ards_nr]
	mov ebx,ards_buf
	xor edx,edx		;edx保存最大内存容量,初始置0
.find_max_mem_area:
	mov eax,[ebx]		;32位基地址
	add eax,[ebx+8]		;内存长度
	add ebx,20		;指向下一个ards
	cmp edx,eax
	jge .next_ards		;如果edx>=eax,跳转到下一个ards,否则让edx=eax,最终效果是找到最大的ards
	mov edx,eax
.next_ards:
	loop .find_max_mem_area
	jmp .mem_get_ok
;-------------------- 方法2:利用0xe801获取内存 --------------------------------
;返回后,ax=cx单位是1kb,里面是小于16mb的单位数,bx=dx单位是64kb,里面是大于16mb的单位数。最终需要转化为字节数。
.e820_failed_so_try_e801:
	mov ax,0x0000e801
	int 0x15
	jc .e801_failed_so_try88	;如果e801失败,尝试88方法
;以下计算出低于16mb的内存容量大小
	mov cx,0x400		;1024
	mul cx			;16位乘法,结果是32位,低16在ax,高16在dx
	shl edx,16		;左移16位
	
	and eax,0x0000ffff	;保留低16位
	or edx,eax		;拼接edx的高16位和eax低16位,放到edx中
	add edx,0x100000	;+1mb,原因是获取的内存比实际内存少1mb
	mov esi,edx		;备份edx
;以下计算16mb以上内存容量大小
	xor eax,eax
	mov ax,bx		;大于16mb的单位数存在bx、dx里
	mov ecx,0x10000		;单位是64kb
	mul ecx			;32位乘法,结果是64位,低32位在eax,高32位在edx
	add esi,eax		;这种方法的上限就是4gb,所以不必理会高32位,只需要把低32位加进结果即可
	
	mov edx,esi
	jmp .mem_get_ok
;-------------------- 方法3:利用0x88获取内存 --------------------------------
;这部分是方法2的简化版,代码参考2,不再写注释
.e801_failed_so_try88:
	mov ah,0x88
	int 0x15
	jc .error_hlt
	and eax,0x0000ffff
	
	mov cx,0x400
	mul cx
	shl edx,16
	or edx,eax
	add edx,0x100000
;-------------------- 这部分记录内存容量 --------------------------------
;如果三种方法都失败,跳转到这里,进行一个死循环
.error_hlt:
     jmp $
;不管使用了哪种子命令,只要成功,都要跳转到这里记录,单位是1字节
.mem_get_ok:
	mov [total_mem_bytes],edx	;在total_mem_bytes地址记录总内存容量
;-------------------- 这部分完成进入保护模式的三个步骤 --------------------------------
	in al,0x92 
	or al,0000_0010B 
	out 0x92,al 		;打开 A20

	lgdt [gdt_ptr] 		;加载 GDT

	mov eax, cr0 
	or eax, 0x00000001 
	mov cr0, eax		;cr0 第 0 位置 1
;-------------------- 验证是否进入保护模式 --------------------------------
	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:160], 'P' ;通过显卡打印一个字符,验证是否进入保护模式
	
	jmp $

编译运行

这里我顺手调整了一下mbr,所以先编译写入mbr

nasm -I include/ -o mbr.bin mbr.S
dd if=/home/hongbai/bochs/mbr.bin of=/home/hongbai/bochs/bin/c.img bs=512 count=1 conv=notrunc

编译写入loader

nasm -I include/ -o loader.bin loader.S
dd if=/home/hongbai/bochs/loader.bin of=/home/hongbai/bochs/bin/c.img bs=512 count=4 seek=2 conv=notrunc

运行bochs

cd bin
./bochs -f bochsrc.disk

结果截图:

我们先打开bochsrc.disk配置文件,看一下我们给bochs配置了多少内存。内存大小是megs行,我设置的是512mb大小。

在控制台输入ctrl+c,中断bochs运行,再输入xp 0xb00,查看内存容量是否探测成功。结果如下:

0x20000000正是我设置的512MB内存大小,说明程序运行正常。


结语

这一部分没什么好说的,主要就是完成获取内存容量的功能,新增了一些代码。后续还有内存分页,加载内核,特权级几部分内容,我计划在清明假期把它们全部完成,最后写一个大总结。

http://www.dtcms.com/a/109642.html

相关文章:

  • Leetcode 1262 -- 动态规划
  • #window系统php-v提示错误#
  • 一周学会Pandas2 Python数据处理与分析-Pandas2简介
  • Node.js 与 MySQL:深入理解与高效实践
  • VisMin:视觉最小变化理解
  • 强化学习_Paper_1988_Learning to predict by the methods of temporal differences
  • 【Pandas】pandas DataFrame values
  • MacOS中配置完环境变量后执行source ~/.bash_profile后,只能在当前shell窗口中生效
  • 【eNSP实验】RIP协议
  • WHAT - JWT(JSON Web Token)
  • 颜色归一化操作
  • 设计心得——状态机
  • STM32单片机入门学习——第12节: [5-2]对射式红外传感器计次旋转编码器计次
  • 多模态学习(八):2022 TPAMI——U2Fusion: A Unified Unsupervised Image Fusion Network
  • MySQL数据库脱敏实战指南:从原理到企业级实现
  • torch.nn中的非线性激活介绍合集——Pytorch中的非线性激活
  • Webacy 利用 Walrus 技术构建链上风险分析决策层
  • 软考又将迎来新的改革?
  • c#和c++脚本解释器科学运算
  • 约瑟夫环的四种(数组,链表,递归,迭代)解决方案,与空间、时间复杂度分析
  • 【Linux】远程登录时,使用图形界面报错:MoTTY X11 proxy: Unsupported authorisation protocol
  • Vue 学习随笔系列二十二 —— 表格高度自适应
  • 一个完整的 HTTP/HTTPS 请求流程
  • 【电路笔记】-触发器的转换
  • ctfshow VIP题目限免 源码泄露
  • 【面试篇】Es
  • QTableWidget 中insertRow(0)(头插)和 insertRow(rowCount())(尾插)的性能差异
  • 服务器磁盘io性能监控和优化
  • c++中cin.ignore()的作用
  • Unirest:优雅的Java HTTP客户端库