引言
- 前面章节中我们学习的内存有关的概念都建立在物理内存之上,无论理论概念说得多高大上,最终也要在物理内存上落实行动。为了在后期做好内存管理工作,咱们先得知道自己有多少物理内存才行
- 如何获取系统物理内存的大小呢?
BIOS 提供的中断(int 0x15)
- 来获取内存物理容量有 3 中方法,它们是 BIOS 中断 0x15 的 3 个子功能,子功能号要存放到寄存器 EAX 或 AX 中,如下:
- AH=0x88:最多检测出 64MB 内存,实际内存超过此容量也按照 64MB 返回
- AX=0xE801:分别检测低 15MB 和 16MB~4GB 的内存,最大支持 4GB
- EAX=0xE820:遍历主机上全部内存
- 注意:BIOS 中断是实模式下的方法,只能在进入保护模式前调用
方法 1:利用 BIOS 中断 0x15 子功能 0x88 获取物理内存容量
- 由于最多只能检测 64M 的内存,所以此方法我们不做说明
方法 2:利用 BIOS 中断 0x15 子功能 0xE801 获取物理内存容量
- 此中断的调用步骤如下:
- 将 AX 寄存器写入 0xE801
- 执行中断调用 int 0x15
- 在 CF 位为 0 的情况下,获取返回结果
- 返回值具体说明
寄存器或状态位 | 描述 |
CF 位 | CF 位为 0 表示调用未出错,CF 为 1,表示调用出错 |
AX | 以 1KB 为单位,只显示 15MB 以下的内存容量,故最大值为 0x3c00,即 AX 表示的最大内存为 0x3c00* 1024=15MB |
BX | 以 64KB 为单位,内存空间 16MB~4GB 中连续的单位数量,即内存大小为 BX* 64* 1024 字节 |
CX | 同 AX |
DX | 同 BX |
- 关于标志寄存器知识详见 标志寄存器,对于 32位处理器已经扩展成了 eflags,可自行查找学习
- 就在上一章节实现的代码的基础上进行实验吧 loader.asm
- 具体实现见 loader.asm ,注意:因为用到 BIOS 中断,所以读内存容量指令必须在是模式下执行
- 我们先定义 4 字节的空间用于读取内存容量
MEM_SIZE times 4 db 0
- int 0x15 中断使用如下:
xor eax, eax ; 用于清 CF 标志位 CF = 0 mov eax, 0xE801 int 0x15
- 接下来如果 CF标志位 = 0,则物理内存容量 = AX
*
1024+BX*
64*
1024
shl eax, 10 ; eax = eax * 1024 shl ebx, 16 ; ebx = ebx * 64 * 1024 add dword [MEM_SIZE], eax add dword [MEM_SIZE], ebx
- 程序到这里就已经得到 MEM_SIZE 了。不过我们没有办法显示出来内存容量,只能反汇编后断点调试了
ndisasm -o 0x900 loader.bin > loader.txt
- 找到实模式下最后一条指令 “jmp dword CODE32_SELECTOR:0” 处地址为 0x9D0,在这里打上断点
- 从反汇编后的文件中我们找到了 [MEM_SIZE] 地址为 [0x969]
- bochs 调试打印信息如下
<bochs:1> b 0x9d0 <bochs:2> c ... <bochs:3> xp 0x969 [bochs]: 0x00000969 <bogus+ 0>: 0x01f00000
- 从提示信息中可以得到获取的物理内存大小为:0x01f00000 = 31M
- 查看 bochs 配置文件 bochsrc.disk 中内存的配置大小配置为 32M
# how much memory the emulated machine will have
# 虚拟机内存大小
megs: 32
- 为什么少了 1M 呢?
- 我们再回过头来看看“返回值具体说明”中AX和BX寄存器的描述内容,发现,描述恰巧丢了 15~16 这 1M 空间,这又是为什么?
- 这个就只能说是历史遗留问题了,凡是不懂得,就归结到历史原因上,早期芯片将 15M~16M 这段内存映射给了 ISA 设备使用,也别管 ISA 是啥,反正就是内存没法用了,这种情况称之为内存黑洞。具体咱就不研究了
- 现在,我们人为增加 1M 内存容量
add dword [MEM_SIZE], eax add dword [MEM_SIZE], ebx mov ecx, 1 shl ecx, 20 ; ecx = 1MB add dword [MEM_SIZE], ecx ; 人为增加 1M
方法 3:利用 BIOS 中断 0x15 子功能 0xE820 获取物理内存容量
- BIOS 中断 0x15 的子功能 0xE820 能够获取系统的内存布局,这是最灵活的内存获取方式
- 操作系统根据需要把一整块物理内存分成多块不同属性的内存块,子功能 0xE820 就可以获得每一块内存的信息,想得到所有内存信息,就需要多次中断,来获得所有内存块的信息
- 通过子功能 0xE820 获得的内存信息被称作为地址范围描述符(Address Range Descriptor Structure, ARDS)
- 地址范围描述符结构 ARDS
字节偏移量 | 属性名称 | 描 述 |
0 | BaseAddrLow | 基地址的低 32 位 |
4 | BaseAddrHigh | 基地址的高 32 位 |
8 | LengthLow | 内存长度的低 32 位,以字节为单位 |
12 | LengthHigh | 内存长度的高 32 位,以字节为单位 |
16 | Type | 本段内存的类型 |
- 用 C 来描述这个结构
struct ARDS { unsigned int BaseAddrLow; unsigned int BaseAddrHigh unsigned int LengthLow unsigned int LengthHigh unsigned int Type }
- 此结构中的字段大小都是 4 字节,共 5 个字段,所以此结构大小为 20 字节。每次 int 0x15 之后,BIOS 就返回这样一个结构的数据。注意,ARDS 结构中用 64 位宽度的属性来描述这段内存基地址(起始地址)及其长度,所以表中的基地址和长度都分为低 32 位和高 32 位两部分
- Type 值:1,这段内存可以被操作系统使用;2,内存使用中或者被系统保留,操作系统不可以用此内存;其他,未定义,将来会用到,目前保留
- 中断调用前输入
寄存器或状态位 | 参数用途 |
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 结构 |
- 说了那么多 ARDS,就算我们得到了 ARDS 内存信息,又怎么知道实际物理内存容量有多大呢?
- 每次中断后都会得到一组 BaseAddrLow + LengthLow(基地址+内存长度),当所有内存信息得到后,其中 BaseAddrLow + LengtLow 的最大值就是我们要的实际物理内存容量(32 位操作系统只用到低 32 位)
- 注意:获取到的 ARDS 结构信息中只有 Type 为 1 时才是有效的
- 直接上代码:loader.asm
- 先把原先的 detect_memory 函数改名为 detect_memory_0xe801,区分本次新增的函数 detect_memory_0xe820
- 先来看一下获取 ARDS 的伪代码帮助理解:
struct ARDS ARDS[64] = {0}; // ARDS 缓冲区 int count = 0; // 内存块个数 es:di = ARDS; ebx = 0; do { // 固定参数,使用中断前需重新设置 eax = 0xE820 edx = 0x534d4150; ecx = 20; int 0x15; // 触发 0x15 中断 // int 0x15 中断执行完成后,返回一个 ARDS 信息填充到 es:di 执行的内存中 if(ARDS[i].Type == 1) // Type 为 1 时表示系统可以使用 { // 比较大小,把较大的赋值给 MEM_SIZE if(MEM_SIZE < ARDS[i].BaseAddrLow + ARDS[i].LengthLow) MEM_SIZE = ARDS[i].BaseAddrLow + ARDS[i].LengthLow } if(cf == 1) // 失败处理 { error(); break; } di += 20; count++; } while(ebx != 0) // 如果 ebx = 0,说明是最后一个 ARDS 结构
- 有了上面的伪代码,我们再来看汇编实现的 detect_memory_0xe820 函数就比较容易了
- 我们依旧需要使用断点调试的方法来查看获得的物理内存容量,反汇编后找到 MEM_SIZE 地址为 0x96a,MEM_ARDS 地址为 0x96e 在 “jmp dword CODE32_SELECTOR:0” 这条跳转指令处(0xed1)打断点,查看内存 MEM_SIZE 和 MEM_ARDS 中的值
- 在调试过程中发现获得的物理内存容量始终少了 0x10000,不知道啥原因,参考 detect_memory_0xe801 人为给它加上 0x10000 吧
<bochs:1> b 0xed1 <bochs:2> c ... <bochs:3> xp 0x96a [bochs]: 0x0000096a <bogus+ 0>: 0x02000000 <bochs:4> xp/5 0x96e [bochs]: 0x0000096e <bogus+ 0>: 0x00000000 0x00000000 0x0009f000 0x00000000 0x0000097e <bogus+ 16>: 0x00000001 <bochs:5> xp/5 0x96e+20 [bochs]: 0x00000982 <bogus+ 0>: 0x0009f000 0x00000000 0x00001000 0x00000000 0x00000992 <bogus+ 16>: 0x00000002 <bochs:6> xp/5 0x96e+40 [bochs]: 0x00000996 <bogus+ 0>: 0x000e8000 0x00000000 0x00018000 0x00000000 0x000009a6 <bogus+ 16>: 0x00000002 <bochs:7> xp/5 0x96e+60 [bochs]: 0x000009aa <bogus+ 0>: 0x00100000 0x00000000 0x01ef0000 0x00000000 0x000009ba <bogus+ 16>: 0x00000001 <bochs:8> xp/5 0x96e+80 [bochs]: 0x000009be <bogus+ 0>: 0x01ff0000 0x00000000 0x00010000 0x00000000 0x000009ce <bogus+ 16>: 0x00000003 <bochs:9> xp/5 0x96e+100 [bochs]: 0x000009d2 <bogus+ 0>: 0xfffc0000 0x00000000 0x00040000 0x00000000 0x000009e2 <bogus+ 16>: 0x00000002 <bochs:10> xp/5 0x96e+120 [bochs]: 0x000009e6 <bogus+ 0>: 0x00000000 0x00000000 0x00000000 0x00000000 0x000009f6 <bogus+ 16>: 0x00000000
获取物理内存容量逻辑
- 当前我们有两种方式都可以实现物理内存容量的检测,那么怎么处理这两者之间的关系呢?
- 先使用 0xe820 方式检测内存容量,如果失败了再使用 0xe801 方式检测内存容量,如果都失败了,死机,没有内存操作系统也没必要启动了
- 让我们用 C 语言的方式实现一下这个逻辑吧
void detect_memory(void) { int err = 0; err = detect_memory_0xe820(); if(err) { detect_memory_0xe801(); } else { printf("Get Memory Err"); for(;;) // 死机 } }
- 最终实现代码:loader.asm
保护模式下的数据保护
- 比如前面我们在实模式下得到的实际物理内存相关数据,需要将其传递到保护模式下以供使用,如果我们不对这些数据做任何保护措施,那么该数据就有可能被破坏。既然是保护模式,那对数据做保护措施不是轻轻松松的事吗
- 我们把需要保护的数据当成一个只读数据段不就可以了嘛
SYS_DATA_SEGMENT: MEM_SIZE times 4 db 0 ; 物理内存容量 MEM_SIZE_OFFSET equ MEM_SIZE - SYS_DATA_SEGMENT MEM_ARDS times 64 * 20 db 0 ; ARDS 缓冲区 MEM_ARDS_OFFSET equ MEM_ARDS - SYS_DATA_SEGMENT SYS_DATA_SEG_LEN equ $ - SYS_DATA_SEGMENT
- 对应的段描述法和段选择符
; 段描述符,只读属性 SYS_DATA_DESC : Descriptor 0, SYS_DATA_SEG_LEN - 1, DA_DR + DA_32 ; 段选择符 SYS_DATA_SELECTOR equ (0x000B << 3) + SA_RPL0 + SA_TIG
- 段描述符中段基址别忘记初始化
mov esi, SYS_DATA_SEGMENT mov edi, SYS_DATA_DESC call InitDescItem
- 代码如下:loader.asm