网站技术维护网站的搜索引擎
文章目录
- 前言
- 获取物理内存容量
- 利用 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就会返回一个这样结构的数据。
字节偏移量 | 属性名称 | 描述 |
---|---|---|
0 | BaseAddrLow | 第32位基地址 |
4 | BaseAddrHigh | 高32位基地址 |
8 | LengthLow | 低32位内存长度,以字节为单位 |
12 | LengthHigh | 高32位内存长度,以字节为单位 |
16 | Type | 本段内存类型 |
关于type字段
Type 值 | 名 称 | 描 述 |
---|---|---|
1 | AddressRangeMemory | 这段内存可以被操作系统使用 |
2 | AddressRangeReserved | 内存使用中或者被系统保留,操作系统不可以用此内存 |
其他 | 未定义 | 未定义,将来会用到,目前保留。但是需要操作系统一样将其视为ARR(AddressRangeReserved) |
关于0x15中断的0xe820子功能的参数
调用或返回 | 寄存器或状态位 | 参 数 用 途 |
---|---|---|
输入 | EAX | 子功能号:EAX 寄存器用来指定子功能号,此处输入为 0xE820 |
EBX | ARDS 后续值:内存信息需要按类型分多次返回,由于每次执行一次中断都只返回一种类型内存的 ARDS 结构,所以要记录下一个待返回的内存 ARDS,在下一次中断调用时通过此值告诉 BIOS 该返回哪个 ARDS,这就是后续值的作用。第一次调用时一定要置为 0,EBX具体值我们不用关注,字取决于具体 BIOS 的实现。每次中断返回后,BIOS 会更新此值 | |
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 会更新此值,BIOS 通过此值可以找到下一个待返回的 ARDS 结构,咱们不需要改变 EBX 的值,下一次中断调用时还会用到它。在 CF 位为 0 的情况下,若返回后的 EBX 值为 0,表示这是最后一个 ARDS 结构 |
此中断的调用步骤如下。
- 填写好“调用前输入”中列出的寄存器。
- 执行中断调用 int 0x15。
- 在 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 |
此中断的调用步骤如下。
- 将 AX 寄存器写入 0xE801。
- 执行中断调用 int 0x15。
- 在 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_ADDRLOADER_STACK_TOP equ LOADER_BASE_ADDRjmp loader_start
;-------------------- 这部分完成GDT和4个段描述符的构建 --------------------------------
;编译后程序的地址是越来越高的,所以代码要先开辟低地址再开辟高地址。8字节64位是一个段描述符。dd命令开辟出一个4字节32位空间。
GDT_BASE:dd 0x00000000dd 0x00000000 ;第一个段描述符无法使用,开辟出来空着
CODE_DESC:dd 0x0000ffff ;前四位是16位段基址,设置为0,后四位是16位段界限,设置为1。高位在前低位在后dd DESC_CODE_HIGH4 ;提前在配置文件中写好的高32位
DATA_STACK_DESC:dd 0x0000ffffdd 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_BASEGDT_LIMIT equ GDT_SIZE-1times 59 dq 0times 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+RPL0SELECTOR_VIDEO equ (0x0003<<3)+TI_GDT+RPL0
;-------------------- 这部分设置保存内存容量的标号--------------------------------
;关于这个标号的地址,loader.S的起始地址是0x900,这行前面有64个8字节的段描述符,所以这里是0x900+0x200=0xb00total_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缓冲区,存放ardsards_nr dw 0 ;用于记录ards结构体的数量;这两行开辟256字节的空间,人工对齐loader_start:
;-------------------- 方法1:利用0xe820获取内存 --------------------------------
;以下部分,通过0xe801获取所有的ardsxor 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 ;返回的字节数,固定为20int 0x15jc .e820_failed_so_try_e801 ;如果e820失败,尝试e801add di,cx ;+20字节指向下一个ardsinc 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_bufxor edx,edx ;edx保存最大内存容量,初始置0
.find_max_mem_area:mov eax,[ebx] ;32位基地址add eax,[ebx+8] ;内存长度add ebx,20 ;指向下一个ardscmp edx,eaxjge .next_ards ;如果edx>=eax,跳转到下一个ards,否则让edx=eax,最终效果是找到最大的ardsmov edx,eax
.next_ards:loop .find_max_mem_areajmp .mem_get_ok
;-------------------- 方法2:利用0xe801获取内存 --------------------------------
;返回后,ax=cx单位是1kb,里面是小于16mb的单位数,bx=dx单位是64kb,里面是大于16mb的单位数。最终需要转化为字节数。
.e820_failed_so_try_e801:mov ax,0x0000e801int 0x15jc .e801_failed_so_try88 ;如果e801失败,尝试88方法
;以下计算出低于16mb的内存容量大小mov cx,0x400 ;1024mul cx ;16位乘法,结果是32位,低16在ax,高16在dxshl edx,16 ;左移16位and eax,0x0000ffff ;保留低16位or edx,eax ;拼接edx的高16位和eax低16位,放到edx中add edx,0x100000 ;+1mb,原因是获取的内存比实际内存少1mbmov esi,edx ;备份edx
;以下计算16mb以上内存容量大小xor eax,eaxmov ax,bx ;大于16mb的单位数存在bx、dx里mov ecx,0x10000 ;单位是64kbmul ecx ;32位乘法,结果是64位,低32位在eax,高32位在edxadd esi,eax ;这种方法的上限就是4gb,所以不必理会高32位,只需要把低32位加进结果即可mov edx,esijmp .mem_get_ok
;-------------------- 方法3:利用0x88获取内存 --------------------------------
;这部分是方法2的简化版,代码参考2,不再写注释
.e801_failed_so_try88:mov ah,0x88int 0x15jc .error_hltand eax,0x0000ffffmov cx,0x400mul cxshl edx,16or edx,eaxadd 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 ;打开 A20lgdt [gdt_ptr] ;加载 GDTmov 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_VIDEOmov 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内存大小,说明程序运行正常。
结语
这一部分没什么好说的,主要就是完成获取内存容量的功能,新增了一些代码。后续还有内存分页,加载内核,特权级几部分内容,我计划在清明假期把它们全部完成,最后写一个大总结。