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

本文介绍了如何在编写内核时,通过BIOS中断0x15的子功能0xE820获取物理内存信息,使用ARDS结构体来检测和计算最大内存容量,作为操作系统的基础配置。
摘要由CSDN通过智能技术生成

任务目标

由于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调用说明

代码

调用步骤

  1. 填写好“调用前输入”的寄存器
  2. 执行中断调用int 0x15
  3. 在CF位为0的情况下,获取对应的返回结果
  4. 遍历获取到的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

运行上述命令

  1. 出现[6]直接按下回车
  2. 继续按下c,出现如下界面

这是我们之前的实验结果

接下来按下ctrl+c,在终端输入

xp 0xb00

 其中,xp命令表示查看内存内容,0xb00是我们在代码中设计的变量total_mem_bytes的地址,total_mem_bytes保存了代码最终计算的结果,如下所示

注,0x02000000换算为十进制就是32 

  • 21
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值