动手学操作系统(六、获取物理内存容量)
在上一节中,我们介绍了保护模式和实模式的区别,保护模式的最大特点是“大”,“大”是指寻址空间大,在进入保护模式之后,我们还将要接触虚拟内存、内存管理等,但这些和内存有关的概念都是建立在物理内存之上的,所以无论理论上的概念有多高大上,最终还是要落实到物理内存上才行,所以这一节我们来学习如何获取物理内存容量。
文章目录
- 动手学操作系统(六、获取物理内存容量)
- 1. 利用BIOS中断0x15子功能0xe820获取内存
- 2. 利用BIOS中断0x15子功能0xe801获取内存
- 3. 利用BIOS中断0x15子功能0x88获取内存
- 4. 代码
- Reference
1. 利用BIOS中断0x15子功能0xe820获取内存
在Linux中有许多中方式来获取内存,如果一种方式失败,那么会使用别的方式来获取,如果所有方式都失败了,就会导致无法获取内存信息,后续程序无法加载,只好将机器挂起停止运行。
BIOS中断提供了丰富的功能,具体要调用的功能,需要在寄存器AX
或EAX
中指定,如下:
EAX=0xE820
:遍历主机上全部内存AX=0xE801
:分别检测15MB和16MB~4GB的内存,最大支持4GBAH=0x88
:最多检测出64MB内存,实际内存超过此容量也按照64MB返回。
其中中断0x15
的子功能0xE820
的强大之处在于其返回内存的信息比较丰富,包括多个属性字段,所以需要一种格式结构来组织这些数据,内存信息的内容是用地址范围描述符来进行描述,用于存储这种描述符的结构称之为地址范围描述符(Address Range Descriptor Structure, ARDS)
BIOS中断只是一段函数例程,现在介绍一下BIOS中断0x15
的0xe820
子功能需要哪些参数
调用步骤如下:
- 填写好“调用前输入”中列出的寄存器
- 执行中断调用
int 0x15
- 在
CF
位为0的情况下,“返回后输出”中对应的寄存器便会有对应的结果
2. 利用BIOS中断0x15子功能0xe801获取内存
另一种获取内存容量的方法是BIOS0x15
中断的子功能0xE801
,这个方法虽然简单,但是这个方法只能最大检测4GB内存,且有点不方便的是此方法检测的内存是分别存放在两组寄存器中。
调用步骤如下:
- 将
AX
寄存器写入0xE801
- 执行中断调用
int 0x15
- 在
CF
位为0的情况下,“返回后输出”中对应的寄存器便会有对应的结果
3. 利用BIOS中断0x15子功能0x88获取内存
最后一种获取内存的方法同样是BIOS0x15
中断,子功能号0x88
,该方法使用最简单,但功能也最简单,简单到只能识别最大64MB的内存。
调用步骤如下:
- 将
AX
寄存器写入0x88
- 执行中断调用
int 0x15
- 在
CF
位为0的情况下,“返回后输出”中对应的寄存器便会有对应的结果
4. 代码
目录结构如下:
.
├── bin
│ ├── loader.bin
│ └── mbr.bin
├── bochs_out.log
├── command_compile.sh
├── command_run.sh
├── src
│ └── boot
│ ├── lib
│ │ └── boot.inc
│ ├── loader.S
│ └── mbr.S
└── test
5 directories, 8 files
修改Loader.S
; ~/d2los/src/boot/loader.s
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR ; 程序开始的地址
jmp loader_start
LOADER_STACK_TOP equ LOADER_BASE_ADDR ; 栈顶地址
;构建gdt及其内部的描述符
GDT_BASE: dd 0x00000000
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
dd DESC_VIDEO_HIGH4 ; 此时dpl为0
GDT_SIZE equ $ - GDT_BASE
GDT_LIMIT equ GDT_SIZE - 1
times 60 dq 0 ; 此处预留60个描述符的slot
SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0 ; 第一个选择子
SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0 ; 第二个选择子
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0 ; 第三个选择子
; 以下是定义gdt的指针,前2字节是gdt界限,后4字节是gdt起始地址
gdt_ptr dw GDT_LIMIT
dd GDT_BASE
total_mem_bytes dd 0 ; 保存内存容量,以字节为单位
ards_buf times 244 db 0 ; 人工对齐:total_mem_bytes4字节+gdt_ptr6字节+ards_buf244字节+ards_nr2,共256字节
ards_nr dw 0 ; 用于记录ards结构体数量
loader_start:
mov byte [gs:160],'L'
mov byte [gs:161],0x0F
mov byte [gs:162],'O'
mov byte [gs:163],0x0F
mov byte [gs:164],'A'
mov byte [gs:165],0x0F
mov byte [gs:166],'D'
mov byte [gs:167],0x0F
mov byte [gs:168],'E'
mov byte [gs:169],0x0F
mov byte [gs:170],'R'
mov byte [gs:171],0x0F
; 获取内存容量,int 15, ax = E820h
.get_total_mem_bytes:
xor ebx, ebx ;第一次调用时,ebx值要为0
mov edx, 0x534d4150 ;edx只赋值一次,循环体中不会改变
mov di, ards_buf ;ards结构缓冲区
.e820_mem_get_loop: ;循环获取每个ARDS内存范围描述结构
mov eax, 0x0000e820 ;执行int 0x15后,eax值变为0x534d4150,所以每次执行int前都要更新为子功能号。
mov ecx, 20 ;ARDS地址范围描述符结构大小是20字节
int 0x15
jc .failed_so_try_e801 ;若cf位为1则有错误发生,尝试0xe801子功能
add di, cx ;使di增加20字节指向缓冲区中新的ARDS结构位置
inc word [ards_nr] ;记录ARDS数量
cmp ebx, 0 ;若ebx为0且cf不为1,这说明ards全部返回,当前已是最后一个
jnz .e820_mem_get_loop
;在所有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
add eax, [ebx+8] ;length_low
add ebx, 20 ;指向缓冲区中下一个ARDS结构
cmp edx, eax ;冒泡排序,找出最大,edx寄存器始终是最大的内存容量
jge .next_ards
mov edx, eax ;edx为总内存大小
.next_ards:
loop .find_max_mem_area
jmp .mem_get_ok
; 获取内存容量,int 15, ax = E801h
.failed_so_try_e801:
mov ax,0xe801
int 0x15
jc .failed_so_try88 ;若当前e801方法失败,就尝试0x88方法
; 先算出低15M的内存,ax和cx中是以KB为单位的内存数量,将其转换为以byte为单位
mov cx,0x400 ;cx和ax值一样,cx用做乘数
mul cx
shl edx,16
and eax,0x0000FFFF
or edx,eax
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 ;0x10000十进制为64KB
mul ecx ;32位乘法,默认的被乘数是eax,积为64位,高32位存入edx,低32位存入eax.
add esi,eax ;由于此方法只能测出4G以内的内存,故32位eax足够了,edx肯定为0,只加eax便可
mov edx,esi ;edx为总内存大小
jmp .mem_get_ok
; 获取内存容量,int 15, ah = 0x88
.failed_so_try88:
;int 15后,ax存入的是以kb为单位的内存容量
mov ah, 0x88
int 0x15
jc .error_hlt
and eax,0x0000FFFF
;16位乘法,被乘数是ax,积为32位.积的高16位在dx中,积的低16位在ax中
mov cx, 0x400 ;0x400等于1024,将ax中的内存容量换为以byte为单位
mul cx
shl edx, 16 ;把dx移到高16位
or edx, eax ;把积的低16位组合到edx,为32位的积
add edx,0x100000 ;0x88子功能只会返回1MB以上的内存,故实际内存大小要加上1MB
jmp .mem_get_ok
;将内存换为byte单位后存入total_mem_bytes处。
.mem_get_ok:
mov [total_mem_bytes], edx
; 打开A20地址线
.open_A20:
in al,0x92
or al,0000_0010B
out 0x92,al
; 加载gdt描述符
.load_gdt:
lgdt [gdt_ptr]
; 修改cr0标志寄存器的PE位
.change_cr0_PE:
mov eax, cr0
or eax, 0x00000001
mov cr0, eax
.jmp_bit_32
jmp SELECTOR_CODE:p_mode_start ; 刷新流水线,避免分支预测的影响
; 远跳将导致之前做的预测失效,从而起到了刷新的作用。
.error_hlt: ;出错则挂起
hlt
; 下面就是保护模式下的程序了
[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:320], 'M'
mov byte [gs:321],0x0F
mov byte [gs:322], 'A'
mov byte [gs:323],0x0F
mov byte [gs:324], 'I'
mov byte [gs:325],0x0F
mov byte [gs:326], 'N'
mov byte [gs:327],0x0F
jmp $
使用上一节中编写的command_compile.sh
、command_run.sh
进行编译、运行
command_compile.sh
nasm -I src/boot/lib -o bin/mbr.bin src/boot/mbr.S
nasm -I src/boot/lib -o bin/loader.bin src/boot/loader.S
dd if=./bin/mbr.bin of=/home/sjh/bochs/bin/hardisk60MB.img bs=512 count=1 seek=0 conv=notrunc
dd if=./bin/loader.bin of=/home/sjh/bochs/bin/hardisk60MB.img bs=512 count=2 seek=1 conv=notrunc
command_run.sh
~/bochs/bin/bochs -f ~/bochs/bin/bochsrc.disk
编译
cd ~/d2los
sh command_compile.sh
sh command_run.sh
结果如下:
在运行的过程中依次按下6->c->ctrl+c->r
可以看到检测的内存是33554432
33554432 byte ÷ 1024 ÷ 1024 = 32 MB 33554432 \text{byte} \div 1024 \div 1024 = 32\text{MB} 33554432byte÷1024÷1024=32MB
Reference
[1]《一个64位操作系统的设计与实现》
[2]《操作系统真象还原》