任务目标
由于os需要管理硬件,因此os首先需要知道一下硬件信息,而物理内存容量就是一个重要的信息。
本节的主要任务是在正式写内核之前,首先对物理内存进行检测,并获取最大的物理内存容量。
整体思路是使用BIOS中断来获取每一个段的ards内存结构体,再遍历这些结构体获取以获取内存容量。
ARDS
BIOS中断0x15的子功能 0xE820能够获取系统的内存布局,由于系统内存各部分的类型属性不同,BIOS 就按照类型属性来划分这片系统内存,所以这种查询呈迭代式,每次BIOS只返回一种类型的内存信息,直到将所有内存类型返回完毕。子功能0xE820的强大之处是返回的内存信息较丰富,包括多个属性字段,所以需要一种格式结构来组织这些数据。内存信息的内容是用地址范围描述符来描述的,用于存储这种描述符的结构称之为地址范围描述符(Address Range Descriptor Structure,ARDS)。
此结构中的字段大小都是4字节,共5个字段,所以此结构大小为 20字节。每次int0x15之后,BIOS就返回这样一个结构的数据。注意,ARDS结构中用64位宽度的属性来描述这段内存基地址(起始地址)及其长度,所以表中的基地址和长度都分为低32位和高32位两部分。其中的 Type 字段用来描述这段内存的类型,这里所谓的类型是说明这段内存的用途,即其是可以被操作系统使用,还是保留起来不能用。Type字段的具体意义见下。
0xe820调用说明
代码
调用步骤
- 填写好“调用前输入”的寄存器
- 执行中断调用int 0x15
- 在CF位为0的情况下,获取对应的返回结果
- 遍历获取到的ards结构体,计算最大内存容量
loader.S
注意,代码中loader_start的偏移量经作者手工设计为0x300,因此可以直接修改mbr中loader的加载地址,直接从loader_start处开始执行,即
jmp LOADER_BASE_ADDR+0x300;
%include "boot.inc"
SECTION loader vstart=LOADER_BASE_ADDR
;初始化栈指针地址
LOADER_STACK_TOP equ LOADER_BASE_ADDR
;------------- 构建gdt及其内部的描述符 -------------
GDT_BASE:
dd 0x00000000
dd 0x00000000
;代码段描述符的低4字节部分,其中高两个字节表示段基址的0~15位,在这里定义为0x0000
;低两个字节表示段界限的0~15位,由于使用的是平坦模型,因此是0xFFFF
CODE_DESC:
dd 0x0000FFFF
dd DESC_CODE_HIGH4;段描述符的高4字节部分
DATA_STACK_DESC:
dd 0x0000FFFF
dd DESC_DATA_HIGH4
;定义显存段的描述符
;文本模式下的适配器地址为0xb8000~0xbffff,为了方便显存操作,显存段不使用平坦模型
;因此段基址为0xb8000,段大小为0xbffff-0xb8000=0x7fff,
;段粒度位4k,因此段界限的值为0x7fff/4k=7
VIDEO_DESC:
dd 0x80000007
dd DESC_VIDEO_HIGH4
GDT_SIZE equ $-GDT_BASE
GDT_LIMIT equ GDT_SIZE-1
times 60 dq 0 ;此处预留60个描述符的空位
;------------- 构建选择子 -------------
SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0
SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0
total_mem_bytes dd 0 ;total_mem_bytes用于保存最终获取到的内存容量,为4个字节
;由于loader程序的加载地址为0x900,而loader.bin的文件头大小为0x200
;(4个gdt段描述符(8B)加上60个dp(8B)填充字,故64*8=512B),
;故total_mem_bytes在内存中的地址为0x900+0x200=0xc00
;该地址将来在内核中会被用到
;------------- 定义gdtr(指向GDT的寄存器) -------------
gdt_ptr dw GDT_LIMIT
dd GDT_BASE
ards_buf times 244 db 0 ;开辟一块缓冲区,用于记录返回的ARDS结构体,
;该定义语句事实上是定义了一个数组ards_buf[244]
;244是因为total_mem_bytes(4)+gdt_ptr(6)+244+ards_nr(2)=256,即0x100
;这样loader_start的在文件内的偏移地址就是0x100+0x200=0x300
ards_nr dw 0 ;用于记录ards结构体数量
;------------------------------------------
;INT 0x15 功能号:0xe820 功能描述:获取内存容量,检测内存
;------------------------------------------
;输入:
;EAX:功能号,0xE820,但调用返回时eax会被填入一串ASCII值
;EBX:ARDS后续值
;ES:di:ARDS缓冲区,BIOS将获取到的内存信息存到此寄存器指向的内存,每次都以ARDS格式返回
;ECX:ARDS结构的字节大小,20
;EDX:固定为签名标记,0x534d4150
;返回值
;CF:若cf为0,表示未出错,cf为1,表示调用出错
;EAX:字符串SMAP的ASCII码值,0x534d4150
;ES:di:ARDS缓冲区,BIOS将获取到的内存信息存到此寄存器指向的内存,每次都以ARDS格式返回
;ECX:ARDS结构的字节大小,20
;EBX:ARDS后续值,即下一个ARDS的位置。
;每次BIOS中断返回后,BIOS会更新此值,BIOS会通过此值找到下一个待返回的ARDS结构。
;在cf位为0的情况下,若返回后的EBX值为0,表示这是最后一个ARDS结构
loader_start:
xor ebx,ebx ;第一次调用时,要将ebx清空置为0,此处使用的是异或运算置0
mov edx,0x534d4150
mov di,ards_buf ;di存储缓冲区地址,即指向缓冲区首地址
.e820_mem_get_loop:
mov eax,0x0000e820
mov ecx,20 ;一个ards结构体的大小
int 0x15 ;调用0x15中断函数,返回的ards结构体被返回给di指向的缓冲区中
add di,cx ;使di增加20字节指向缓冲区中下一个的ARDS结构位置
inc word [ards_nr] ;inc(increment增加)指令表示将内存中的操作数增加一,此处用于记录返回的ARDS数量
cmp ebx,0 ;比较ebx中的值是否为0
jnz .e820_mem_get_loop ;若ebx不为0,则继续进行循环获取ARDS,
;若为0说明已经获取到最后一个ards,则退出循环
mov cx,[ards_nr] ;cx存储遍历到的ards结构体个数
mov ebx,ards_buf ;ebx指向缓冲区地址
xor edx,edx ;EDX用于保存BaseAddrLow+LengthLow最大值,此处初始化为0
.find_max_mem_area:
mov eax,[ebx] ;eax用于遍历缓冲区中的每一个ards的BaseAddrLow
add eax,[ebx+8] ;ebx+8获取的是LengthLow,故该代码计算的是BaseAddrLow+LengthLow
add ebx,20 ;遍历下一个ards
cmp edx,eax ;分支语句,如果edx大于等于eax,则跳转到.next_ards,也就是进入循环
jge .next_ards
mov edx,eax
.next_ards:
loop .find_max_mem_area
mov [total_mem_bytes],edx
;------------- 准备进入保护模式 -------------
;1.打开A20
;2.加载gdt
;3.置cr0的PE位为1
;------------- 打开A20 -------------
in al,0x92
or al,0000_0010B
out 0x92,al
;------------- 加载gdt -------------
lgdt [gdt_ptr]
;------------- 置cr0的PE位为1 -------------
mov eax,cr0
or eax,0x00000001
mov cr0,eax
jmp dword 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 ;初始化栈指针,将栈指针地址放入bsp寄存器
mov ax,SELECTOR_VIDEO ;初始化显存段寄存器,显存段的选择子放入gs寄存器
mov gs,ax
mov byte [gs:160],'p'
jmp $
创建build文件夹,专门存放汇编后的loader与mbr,整个目录结构如下:
编译loader.S并写入磁盘
nasm -o ./build/loader.bin -I ./include/ loader.S
dd if=./osCode/build/loader.bin of=./bochs/hd60M.img bs=512 count=4 seek=2 conv=notrunc
mbr.S
;主引导程序MBR,由BIOS通过jmp 0:0x7c00跳转
;------------------------------------------
%include "boot.inc"
;vstart=0x7c00表示本程序在编译时,起始地址编译为0x7c00
SECTION MBR vstart=0x7c00
;使用通用寄存器中的值(0)初始化其余寄存器
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
;初始化栈指针
mov sp,0x7c00
;存入显存的段基址
mov ax,0xb800
mov gs,ax
;------------------------------------------
;------------------------------------------
;利用0x06号功能进行清屏
;INT 0x10 功能号:0x06 功能描述:上卷窗口清屏
;输入:
;AH 功能号:0x06
;AL=上卷的行数(如果为0,则表示全部)
;BH=上卷的行属性
;(CL,CH)=窗口左上角(X,Y)位置
;(DL,DH)=窗口右下角(X,Y)位置
;返回值
;无返回值
mov ax,0x0600
mov bx,0x0700
mov cx,0 ;左上角(0,0)
mov dx,0x184f ;右下角(80,25)
;0x18=24,0x4f=79
int 0x10 ;调用BIOS中断函数
;------------------------------------------
;------------------------------------------
;将要显示的字符串写入到显存中
mov byte [gs:0x00],'1';在第一个字节的位置写入要显示的字符“1”
;在第二个字节的位置写入显示字符(也就是字符1)的属性,其中A表示绿色背景,4表示前景色为红色
mov byte [gs:0x01],0xA4
mov byte [gs:0x02],' '
mov byte [gs:0x03],0xA4
mov byte [gs:0x04],'M'
mov byte [gs:0x05],0xA4
mov byte [gs:0x06],'B'
mov byte [gs:0x07],0xA4
mov byte [gs:0x08],'R'
mov byte [gs:0x09],0xA4
mov eax,LOADER_START_SECTOR;起始扇区地址
mov bx,LOADER_BASE_ADDR;要写入的内存地址
mov cx,4;待读入的扇区数目
call rd_disk_m16;调用磁盘读取程序
jmp LOADER_BASE_ADDR+0x300;
;------------------------------------------
;函数功能:读取硬盘的n个扇区
;参数:
;eax:LBA扇区号
;bx:要写入的内存地址
;cx:待读入的扇区数目
rd_disk_m16:
;out命令,通过端口向外围设备发送数据
;其中目的操作数可以是8为立即数或者寄存器DX,源操作数必须是寄存器AL或者AX
;因此需要将之前ax中保存的值进行备份
mov esi,eax
mov di,cx
;正式读写硬盘
;第一步:设置要读取的扇区数目
mov dx,0x1f2
mov ax,cx
out dx,ax
mov eax,esi;恢复eax的值
;第二步:将LBA的地址存入0x1f3~0x1f6
;LBA的0~7位写入端口0x1f3
mov dx,0x1f3
out dx,al
;LBA的8~15位写入端口0x1f4
mov cl,8
shr eax,cl;ax左移8位
mov dx,0x1f4
out dx,al
;LBA的16~23位写入端口0x1f5
shr eax,cl
mov dx,0x1f5
out dx,al
;LBA的24~27位写入端口0x1f6
shr eax,cl;左移8位
and al,0x0f;与0000 1111相与,取后四位
or al,0xe0;设置7~4位为1110,表示lba模式
mov dx,0x1f6
out dx,al
;第三步,向0x1f7端口写入读命令,0x20
mov dx,0x1f7
mov al,0x20
out dx,al
;第四步,检测硬盘状态
.not_ready:
nop;空操作,什么也不做,相当于sleep,只是为了增加延迟
in al,dx;由于读取硬盘状态信息的端口仍旧是0x1f7,因此不需要再为dx指定端口号
;取出状态位的第3位和第7位
;其中第3位若为1表示硬盘控制器已经准备好数据传输,第7位为1表示硬盘忙
and al,0x88
;cmp为减法指令,此处是为了判断第7位是否为1,如果为1,则减法的结果应该为0
cmp al,0x08
;JNZ是条件跳转指令,它表示"Jump if Not Zero",
;也就是如果零标志位(ZF)不为0,则进行跳转。否则不进行跳转
jnz .not_ready;若未准备好,则继续等
;第五步,从0x1f0端口读取数据
mov ax,di;di是之前备份的要读取的扇区数
mov dx,256
;mul指令表示乘法操作,当只有一个操作数时,被乘数隐含在al或者ax中
mul dx;一个扇区512字节,每次读取两个字节,需要读取di*256次
mov cx,ax;cx在此时表示要循环读取的次数
mov dx,0x1f0
.go_on_read:
in ax,dx
mov [bx],ax;通过循环将输入写入bx寄存器所指向的内存,每次读入2个字节的数据
add bx,2
loop .go_on_read
ret
times 510-($-$$) db 0
db 0x55,0xaa
nasm -o ./build/mbr.bin -I ./include/ mbr.S
dd if=./osCode/build/mbr.bin of=./bochs/hd60M.img bs=512 count=1 conv=notrunc
实验结果
查看物理机上装载的内存容量
cat -n boot.disk | grep meg
如上所示,机器上装了32MB内存。megs指定的内存以MB为单位
接下来我们验证我们的代码的正确性,是否可以检测出来这段32MB的内存容量
./bin/bochs -f boot.disk
运行上述命令
- 出现[6]直接按下回车
- 继续按下c,出现如下界面
这是我们之前的实验结果
接下来按下ctrl+c,在终端输入
xp 0xb00
其中,xp命令表示查看内存内容,0xb00是我们在代码中设计的变量total_mem_bytes的地址,total_mem_bytes保存了代码最终计算的结果,如下所示
注,0x02000000换算为十进制就是32