《操作系统真象还原》第五章(1)

《操作系统真象还原》第五章(1)

本篇对应书籍第五章5.1–5.2的内容
本篇介绍并实践获取物理内存容量的方法,以及开启分页机制

获取物理内存容量的方法

上一篇章中,我们从CPU的实模式进入到了保护模式,首先要处理的第一个问题就是内存地址的访问,保护模式下,内存地址使用虚拟地址到物理地址映射访问,在那之前,要先获取物理内存的大小

可以通过调用 BIOS中断 0x15 实现,BIOS中断 0x15 有3个子功能:

  • EAX = 0xE820:遍历主机上全部内存
  • AX = 0xE801:分别检测低 15MB 和 16MB ~ 4GB 的内存,最大支持 4GB
  • AH = 0x88:最多检测出 64MB 内存,实际内存容量超过此容量也按照 64MB 返回

利用 BIOS 中断 0x15 子功能 0xe820 获取内存

0xe820能够获取系统的内存布局,每次BIOS只返回一种类型的内存信息,直到全部返回完成;

内存信息被存在一个结构中–地址范围描述符 ARDS:

在这里插入图片描述

一共5个字段,每个字段4个字节

Type字段含义:

在这里插入图片描述

为什么 BIOS 要按类型来返回呢?

因为内存可能处于多种状态,按类型好区分

BIOS 中断 0x15 子功能 0xe820 说明:

在这里插入图片描述

在这里插入图片描述

此中断的调用步骤:

  • 填写好“调用前输入”中列出的寄存器
  • 执行中断调用 int 0x15
  • 在 CF 位为 0 的情况下,“返回后输出”中对应的寄存器便会有对应的结果

利用 BIOS 中断 0x15 子功能 0xe801 获取内存

这种方法最大识别内存为 4GB

BIOS 中断 0x15 子功能 0xe801 说明:

在这里插入图片描述

此中断的调用步骤:

  • 将 AX 寄存器写入 0xE801
  • 执行中断调用 int 0x15
  • 在 CF 位为 0 的情况下,“返回后输出”中对应的寄存器便会有对应的结果

利用 BIOS 中断 0x15 子功能 0x0e88 获取内存

只能识别到 64MB内存

在这里插入图片描述

此中断的调用步骤:

  • 将 AX 寄存器写入 0X88
  • 执行中断调用 int 0x15
  • 在 CF 位为 0 的情况下,“返回后输出”中对应的寄存器便会有对应的结果

实现获取物理内存容量

本节主要用于演示 BIOS 0x15 中断的用法,可跳过

MBR.asm

将:

jmp LOADER_BASE_ADDR 

修改成:

jmp LOADER_BASE_ADDR + 0x300	; 跳过数据段, 跳跃到代码区 

loader.asm

%include "boot.inc"

section loader vstart=LOADER_BASE_ADDR

    LOADER_STACK_TOP equ LOADER_BASE_ADDR           ; loader在保护模式下的栈指针地址,esp

    ; 构建GDT及其内部描述符, 每个描述符8个字节, 拆分为高低各4字节(32位)
    GDT_BASE:   dd 0x00000000                       ; 第0个描述符,不可用
                dd 0x00000000

    CODE_DESC:  dd 0x0000ffff                       ; 低32位31~16位为段基址15~0位, 15~0位为段界限15~0位
                dd DESC_CODE_HIGH4

    DATA_STACK_DESC:    dd 0x0000ffff               ; 数据段(栈段)描述符
                        dd DESC_DATA_HIGH4

    VIDEO_DESC: dd 0x80000007                       ; 0xB8000 到0xBFFFF为文字模式显示内存 段界限:limit=(0xbffff-0xb8000) / 4k=0x7
                dd DESC_VIDEO_HIGH4                 ; 0xB

    GDT_SIZE:   equ $ - GDT_BASE                    ; 当前位置减去GDT_BASE的地址 等于GDT的大小
    GDT_LIMIT:  equ GDT_SIZE - 1                    ; SIZE - 1即为最大偏移量

    times 60 dq 0                                   ; 预留60个 四字型 描述符空位, 用于后续扩展
                                                    
    SELECTOR_CODE  equ (0x0001<<3) + TI_GDT + RPL0  ; 段选择子: 低3位为TI RPL状态, 其余为描述符索引值
    SELECTOR_DATA  equ (0x0002<<3) + TI_GDT + RPL0  
    SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0

    ; total_mem_bytes用于保存内存容量,以字节为单位,此位置比较好记。
    ; 当前偏移loader.bin文件头0x200字节,loader.bin的加载地址是0x900,
    ; 故total_mem_bytes内存中的地址是0xb00.将来在内核中咱们会引用此地址
    total_mem_bytes dd 0

    ; gdt指针, 前2字节为gdt界限, 后4字节为gdt起始地址(共48位)
    gdt_ptr dw GDT_LIMIT
            dd GDT_BASE

    ; 人工对齐:total_mem_bytes 4字节 + gdt_ptr 6字节 + ards_buf 244字节 + ards_nr 2字节 , 共256字节
    ards_buf times 244 db 0             ; 记录内存大小的缓冲区
    ards_nr dw 0                        ; 记录 ARDS 结构体数量


    loader_start:

        ; -------  int 15h eax = 0000E820h ,edx = 534D4150h ('SMAP') 获取内存布局  -------
        xor ebx, ebx                    ; 第一次调用将eb置0
        mov edx, 0x534d4150             ; edx只赋值一次,循环体中不会改变
        mov di,  ards_buf               ; di指向ARDS缓冲区

        .e820_mem_get_loop:             ; 循环获取每个ARDS内存范围描述结构
            mov eax, 0x0000e820         ; 执行int 0x15后,eax值变为0x534d4150, 所以每次执行int前都要更新为子功能号
            mov ecx, 20                 ; ARDS地址范围描述符结构大小是20字节
            int 0x15
            jc  .e820_failed_so_try_e801    ; 若cf位为1则有错误发生,尝试0xe801子功能
            add di, cx                  ; 使di增加20字节指向缓冲区中新的ARDS结构位置
            inc word [ards_nr]          ; ARDS数量加1
            cmp ebx, 0                  ; 若 ebx 为 0 且 cf 不为 1, 这说明 ards 全部返回,当前已是最后一个
            jnz .e820_mem_get_loop      ; 不为0则循环获取

            ; 在所有ards结构中,找出(base_add_low + length_low)的最大值,即内存的容量
            mov cx, [ards_nr]           ; 遍历每一个ARDS结构体,循环次数是ARDS的数量
            mov ebx, ards_buf
            xor edx, edx                ; 用edx记录最大值, 在这里先清零

        .find_max_mem_area:             ; 无须判断type是否为1,最大的内存块一定是可被使用
            mov eax, [ebx]              ; base_add_low
            add eax, [ebx+8]            ; base_add_low + length_low = 这块ADRS容量
            add ebx, 20                 ; 指向下一块ARDS
            cmp edx, eax                ; 找出最大,edx寄存器始终是最大的内存容量
            jge .next_ards              ; 如果edx>=eax, 继续遍历下一块
            mov edx, eax                ; 如果edx<=eax, 更新edx
        .next_ards:
            loop .find_max_mem_area
            jmp .mem_get_ok             ; 获取内存容量结束


        ; ------  int 15h ax = E801h 获取内存大小,最大支持4G  ------
        ; 返回后, ax cx 值一样,以KB为单位,bx dx值一样,以64KB为单位
        ; 在ax和cx寄存器中为低16M,在bx和dx寄存器中为16MB到4G
        .e820_failed_so_try_e801:
            mov ax, 0xe801
            int 15
            jc .e801_failed_so_try88    ; 若当前e801方法失败,就尝试0x88方法

            ; 1 先算出低15M的内存, ax和cx中是以KB为单位的内存数量,将其转换为以byte为单位
            ; 乘数在eax, 即内存数量, 积高16位在edx, 低16位在eax
            mov cx, 0x400               ; 0x400 = 1024
            mul cx
            shl edx, 16                 ; 左移16位, 将低16位放到edx高16位
            and eax, 0x0000FFFF         ; 0x0000FFFF = 1111 1111 1111 1111, 高16位置0
            or  edx, eax                ; 获得完整的32位积
            add edx, 0x100000           ; edx比实际少1M, 故要加1MB
            mov esi, edx                ; 先把低15MB的内存容量存入esi寄存器备份

            ; 2 再将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 15h ah = 0x88 获取内存大小,只能获取64M之内  ----------
        .e801_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
            or  edx, eax                ; 把积的低16位组合到edx,为32位的积
            add edx, 0x100000           ; 0x88子功能只会返回1MB以上的内存, 故实际内存大小要加上1MB


        .mem_get_ok:
            mov [total_mem_bytes], edx  ; 将内存换为byte单位后存入total_mem_bytes处


        ; --------------------------------- 设置进入保护模式 -----------------------------
        ; 1 打开A20 gate
        ; 2 加载gdt
        ; 3 将cr0 的 pe位(第0位)置1

        ; -----------------  打开A20  ----------------
        in al, 0x92                     ; 端口号0x92 中的第1位变成 1 即可
        or al, 0000_0010B
        out 0x92, al

        ; -----------------  加载GDT  ----------------
        lgdt [gdt_ptr]

        ; -----------------  cr0第0位置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
            mov ax, SELECTOR_VIDEO
            mov gs, ax

            mov byte [gs:160], 'P'

            jmp $

在这里插入图片描述

编译&写入硬盘

nasm -I include/  -o mbr.bin mbr.asm
nasm -I include/  -o loader.bin loader.asm

sudo dd if=/home/steven/source/os/bochs/code/mbr.bin of=/home/steven/source/os/bochs/hd60M.img bs=512 count=1 conv=notrunc


sudo dd if=/home/steven/source/os/bochs/code/loader.bin of=/home/steven/source/os/bochs/hd60M.img bs=512 count=4 seek=2 conv=notrunc

运行Bochs

Bochsrc.disk 配置文件的内存设置:

megs: 512

Bochs运行起来后,使用xp 0xb00查看获取结果:

在这里插入图片描述

0x20000000 换成十进制正是 512 MB

0x20000000 = 536,870,912
536,870,912 / 1024 / 1024 = 512

分页机制

段基址 + 段内偏移地址是线性地址,是连续的,是唯一的,只能属于某一个进程

有时候内存中剩余的连续的地址空间无法装下一个新的进程,内存空间利用率低下,为了能让可用地址空间不是连续的也能用,于是有了分页机制

一级页表

分页机制建立在分段机制之上:

  • 没打开分页机制,就按分段机制直接访问物理内存地址
  • 打开分页机制后,线性地址变成虚拟地址,需要经过地址转换才能进行访问

在这里插入图片描述

分页机制的思想是:通过映射,可以使连续的线性地址与任意物理内存地址相关联,逻辑上连续的线性地址其对应的物理地址可以不连续。

分页机制的作用:

  • 将线性地址转换成物理地址
  • 将大小相等的页代替大小不等的段

在这里插入图片描述

经过段部件输出的线性地址也叫虚拟地址,地址先进行逻辑上的分段,然后将线性地址的段拆分成以页为单位的大小相同的小内存块,也就是图片中间那部分

CPU 采用的页大小是 4KB ,4GB 地址空间被划分出 1M 个页,只要是 4KB 的地址空间都可以称为一页,线性地址的一页都对应物理地址的一页,这就是一级页表


页表是一个线性表,就像数组一样,可以用下标进行索引,页表的表项只有一个字段,那就是这个页所对应的真实的物理地址

分页机制打开前要将页表地址加载到控制寄存器CR3中,这个地址是实际的物理地址,不会被分页进行转换

一级页表转换原理

知道页表的物理地址后,用虚拟地址的高20位用来索引页表的表项,因为页表表项大小为4字节,所以将高20位乘4即是虚拟地址所对应的表项的地址,内容就是对应的物理地址

然后再加上低12位的偏移,即是真正的物理地址,如下图所示:

在这里插入图片描述

二级页表

二级页表是另一种页表形式,为了减少页表的空间占用,所以需要二级页表,二级页表占用内存示意图如下:

在这里插入图片描述

每个页表的物理地址在页目录表中都以页目录项(Page Directory Entry,PDE)的形式存储。

原来一级页表是1M个表项,二级页表则是将1M个表项分成1K个页表,每个页表有1K个表项的形式,页表的地址装载页目录表中页目录表也有1024个项

二级页表转换原理

  • 高10位作为页目录表的索引,获取页表的物理地址
  • 中间10位作为物理页表项的索引,从页表中获取物理页地址
  • 将获取到的物理页地址加上后12位页内偏移,获取真实的物理地址

10位二进制位刚好能表示0~1023这1024个表项或者页表

把4G内存分成了1024份,每份的大小是4M,每份又分成了1024小份,每小份大小是4K

地址转换则是先按4M来分,找到属于哪一个4M,然后再看属于哪一个4K,然后再以此为基址加上偏移地址找到真实地址

示意图如下图:

在这里插入图片描述

每个进程都有自己的页表

页目录项和页表项

在这里插入图片描述

  • P, Present,存在位。若为 1 表示该页存在于物理内存中,若为 0 表示不在物理内存中。
  • RW, Read/Write,读写位。若为 1 表示可读可写,若为 0 表示可读不可写。
  • US,User/Supervisor,意为普通用户/超级用户位。若为 1,表示处于 User 级,任意级别(0、1、2、3)特权的程序都可以访问该页。若为 0,表示处于 Supervisor 级,特权级别为 3 的程序不允许访问该页,只允许特权级别为 0、1、2 的程序访问。
  • PWT,Page-level Write-Through,页级通写位。若为 1 表示此项采用通写方式,表示该页不仅是普通内存,还是高速缓存。
  • PCD,Page-level Cache Disable,页级高速缓存禁止位。若为 1 表示该页启用高速缓存,为 0 表示禁止将该页缓存。
  • A,Accessed,访问位。若为 1 表示该页被 CPU 访问过了。操作系统定期将该位清 0,统计一段时间内变成 1 的次数可计算内存页的使用频率。
  • D,Dirty,脏页位。当 CPU 对一个页面执行写操作时,就会设置对应页表项的 D 位为 1。此项仅针对页表项有效,并不会修改页目录项中的 D 位。
  • PAT,Page Attribute Table,页属性表位。
  • G,Global,全局位。1 表示是全局页,0 表示不是全局页。若为全局页,该页将在高速缓存 TLB 中一直保存。
  • AVL,Available 位。

启用分页机制的准备

启用分页机制要做三件事:

  1. 准备好页目录表及页表
  2. 将页表地址写入控制寄存器 cr3
  3. 寄存器 cr0 的 PG 位置 1

存储段地址有专门的寄存器–段描述符缓冲寄存器,存储页表地址也有专门的寄存器:cr3,也叫页目录基址寄存器:

在这里插入图片描述

只需要把页目录无敌离职的高20位写入CR3寄存器即可

设计内存布局

设计页表也就是设计内存布局

用户代码加上需要用的操作系统中的部分代码才是完整的程序,用户进程需要与操作系统共同配合

所以用户进程都需要共享操作系统,只要让虚拟内存空间的0~3GB分给用户进程3~4GB分给操作系统即可实现共享,让所有进程的虚拟空间的3~4GB部分都指向同一个物理地址

启用分页机制

boot.inc

新增如下内容:

PAGE_DIR_TABLE_POS      equ 0x100000

;--------------页表 属性---------------------
PG_P    equ     1b
PG_RW_R equ     00b
PG_RW_W equ     10b
PG_US_S	 equ  000b 
PG_US_U	 equ  100b 

loader.asm:p_mode_start

这里首先将gdt里面的显存 0xb8000 修改到了内核内存部分中 0xc00b8000

然后将 gdt 的地址修改到了 内核部分中,这里主要是为了以后不重复加载

由于修改了页表,现在访问虚拟内存0xc0000000开始1M的地址,都会map到0-1M内存的位置

最后通过操作显存,

p_mode_start内容有变动:

其中setup_page函数在后文单独列出

[bits 32]
p_mode_start: 

	; 初始化32位的段寄存器
	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

    ; 创建页目录及页表并初始化页内存位图
    call setup_page

    ; gdt需要放在内核里
    ; 将描述符表地址&偏移量写入内存gdt_ptr,一会用新的地址加载
    sgdt [gdt_ptr]                          ; 取出GDT地址和偏移信息,存放在gdt_ptr这个内存位置上

    ; 视频段需要放在内核里与用户进程进行共享
    ; 将gdt描述符中视频段的段基址 + 0xc0000000
    mov ebx, [gdt_ptr + 2]                  ; 这里gdt_ptr前2字节是偏移量,后4字节是GDT基址,先选中GDT
    or dword [ebx + 0x18 + 4], 0xc0000000   ; 一个描述符8字节,0x18处是第3个段描述符也就是视频段, 修改段基址最高位为C, +4进入高4字节, 用or修改即可

    ; 将gdt的基址加上 0xc0000000 成为内核所在的地址
    add dword [gdt_ptr + 2], 0xc0000000
    add esp, 0xc0000000                     ; 将栈指针同样map到内核地址

    ; 页目录地址赋值给cr3
    mov eax, PAGE_DIR_TABLE_POS
    mov cr3, eax

    ; 打开 cr0 的 pg位 (第31位)
    mov eax, cr0
    or  eax, 0x80000000
    mov cr0, eax

    ; 开启分页后, 用 gdt 新的地址重新加载
    lgdt [gdt_ptr]

    ; 重新初始化gs寄存器
    mov ax, SELECTOR_VIDEO
    mov gs,  ax
    mov byte [gs:160], 'V'


    jmp $

坑点:最后打印V的时候,这里书上存在问题,寄存器没刷新直接写入,这样写入基址的还是0xb8000,这里需要重新初始化一下gs寄存器,才能向0xc00b8000基址写入内容

loader.asm:setup_page

因为系统需要被共享,让所有用户进程在自己的虚拟空间都能访问到系统,所以系统所在的虚拟地址对应的页目录,应该指向系统真实存在的页目录,指向同一个页表

页目录表0号项与768号项均指向第一个页表——0号项指向第一个页表(loader这个程序会运行在0-4M空间内,且跨越了段机制与页机制,顺序映射(第一个页表映射0开始4MB,第二个页表映射紧挨着下一个4MB空间)可以保证之前段机制下的线性地址和分页后的虚拟地址对应的物理地址一致,在这4M空间内,分段下的线性地址=分页后的虚拟地址=物理地址),768号项指向第一个页表是为了让分页机制下3GB开始的4MB虚拟地址空间(虚拟地址0-3G空间用户用,3-4G空间操作系统用)对应到了0-4M实际物理空间,因为这里放着我们的操作系统。这是为了所有进程共享操作系统做准备。页目录表最后一项1023号项指向自己——为的是将来动态操作页表做准备

初始化第一个页表(非页目录表)——因为这个页表管理着0—4M的物理地址空间,我们的操作系统就在这个空间内。

初始化页目录表769号-1022号项,769号项指向第二个页表(此页表紧挨着上面的第一个页表),770号指向第三个,以此类推,——为将来用户进程做准备,使所有用户进程共享内核空间(从768号项—1022号项的页目录表项会被拷贝到所有用户进程的页目录表项中)

; -------------   创建页目录及页表   ---------------
setup_page:

	; 先把页目录所占空间清 0
    mov ecx, 4096                               ; 1024 * 4 = 4096  
    mov esi, 0

    .clear_page_dir:
    	 mov byte [PAGE_DIR_TABLE_POS + esi], 0
         inc esi
         loop .clear_page_dir

    ; 开始创建页目录项(Page Directory Entry)
    .create_pde:
    	 mov eax, PAGE_DIR_TABLE_POS
         add eax, 0x1000                         ; 第一个页表的位置(仅次于页目录表,页目录表大小4KB)
         mov ebx, eax                            ; 0x00101 000, 储存到ebx为创建PTE时使用

         ; 下面将页目录项0和OxcOO都存为第一个页表的地址 ,每个页表表示4MB内存
         ; 这样Oxc03fffff(3G-3G04M)以下的地址和Ox003fffff(0-4M)以下的地址都 指向相同的页表
         ; 这是为将地址映射为内核地址做准备
         or eax,  PG_US_U | PG_RW_W | PG_P       ; 用户特权级,可读可写,存在内存
         mov [PAGE_DIR_TABLE_POS + 0x0], eax     ; 第一个目录项,0x00101 007
         mov [PAGE_DIR_TABLE_POS + 0xc00], eax   ; 第0xc00高10位0x300=768个页表占用的目录项,0xc00以上属于kernel空间
         ; 这里是把第768个目录页和第1个目录页指向同一个页表的物理地址:0x101000   
         ; 系统实际位于0~0x100000内存地址中,将系统虚拟地址0xc00000000映射到这低1M的空间内,只需要让0xc0000000的地址指向和低1M相同的页表即可

         sub eax, 0x1000
         mov [PAGE_DIR_TABLE_POS + 4092], eax    ; 使最后一个目录项指向页目录表自己的位置, 4092 = 1023 * 4

        
	; 创建页表项(Page Table Entry)
    mov ecx, 256                                ; 1M低端内存/每页大小4K = 256
    mov esi, 0
    mov edx, PG_US_U | PG_RW_W | PG_P           ; edx中地址为0x0,属性为7,即111b

    .create_pte:
         mov  [ebx + esi * 4], edx               ; ebx = 0x00101 000, 即第一个PTE起始地址, 每个PTE = 4 byte
         add  edx, 4096                          ; edx + 4KB
         inc  esi
         loop .create_pte                        ; 低端1M内存中,物理地址=虚拟地址,这里创建了1M空间的页表项


    ; 创建内核其他页表的PDE
    mov eax, PAGE_DIR_TABLE_POS
    add eax, 0x2000                             ; eax指向第二个页表(每个页表对应一个PDE, 含有1024个页表项)
    or  eax, PG_US_U | PG_RW_W | PG_P
    mov ebx, PAGE_DIR_TABLE_POS                 
    mov ecx, 254                                ; 769~1022的所有目录项数量, 1022 - 769 + 1 = 254
    mov esi, 769                                

    .create_kernel_pde:
         mov  [ebx + esi * 4], eax
         inc  esi
         add  eax, 0x1000                        ; eax指向下一个页表
         loop .create_kernel_pde

	ret

完整代码:

%include "boot.inc"

section loader vstart=LOADER_BASE_ADDR

    LOADER_STACK_TOP equ LOADER_BASE_ADDR           ; loader在保护模式下的栈指针地址,esp

    ; 构建GDT及其内部描述符, 每个描述符8个字节, 拆分为高低各4字节(32位)
    GDT_BASE:   dd 0x00000000                       ; 第0个描述符,不可用
                dd 0x00000000

    CODE_DESC:  dd 0x0000ffff                       ; 低32位31~16位为段基址15~0位, 15~0位为段界限15~0位
                dd DESC_CODE_HIGH4

    DATA_STACK_DESC:    dd 0x0000ffff               ; 数据段(栈段)描述符
                        dd DESC_DATA_HIGH4

    VIDEO_DESC: dd 0x80000007                       ; 0xB8000 到0xBFFFF为文字模式显示内存 段界限:limit=(0xbffff-0xb8000) / 4k=0x7
                dd DESC_VIDEO_HIGH4                 ; 0xB

    GDT_SIZE:   equ $ - GDT_BASE                    ; 当前位置减去GDT_BASE的地址 等于GDT的大小
    GDT_LIMIT:  equ GDT_SIZE - 1                    ; SIZE - 1即为最大偏移量

    times 60 dq 0                                   ; 预留60个 四字型 描述符空位, 用于后续扩展
                                                    
    SELECTOR_CODE  equ (0x0001<<3) + TI_GDT + RPL0  ; 段选择子: 低3位为TI RPL状态, 其余为描述符索引值
    SELECTOR_DATA  equ (0x0002<<3) + TI_GDT + RPL0  
    SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0

    ; total_mem_bytes用于保存内存容量,以字节为单位,此位置比较好记。
    ; 当前偏移loader.bin文件头0x200字节,loader.bin的加载地址是0x900,
    ; 故total_mem_bytes内存中的地址是0xb00.将来在内核中咱们会引用此地址
    total_mem_bytes dd 0

    ; gdt指针, 前2字节为gdt界限, 后4字节为gdt起始地址(共48位)
    gdt_ptr dw GDT_LIMIT
            dd GDT_BASE

    ; 人工对齐:total_mem_bytes 4字节 + gdt_ptr 6字节 + ards_buf 244字节 + ards_nr 2字节 , 共256字节
    ards_buf times 244 db 0                         ; 记录内存大小的缓冲区
    ards_nr dw 0                                    ; 记录 ARDS 结构体数量


    loader_start:

        ; -------  int 15h eax = 0000E820h ,edx = 534D4150h ('SMAP') 获取内存布局  -------
        xor ebx, ebx                                ; 第一次调用将eb置0
        mov edx, 0x534d4150                         ; edx只赋值一次,循环体中不会改变
        mov di,  ards_buf                           ; di指向ARDS缓冲区

        .e820_mem_get_loop:                         ; 循环获取每个ARDS内存范围描述结构
            mov eax, 0x0000e820                     ; 执行int 0x15后,eax值变为0x534d4150, 所以每次执行int前都要更新为子功能号
            mov ecx, 20                             ; ARDS地址范围描述符结构大小是20字节
            int 0x15
            jc  .e820_failed_so_try_e801            ; 若cf位为1则有错误发生,尝试0xe801子功能
            add di, cx                              ; 使di增加20字节指向缓冲区中新的ARDS结构位置
            inc word [ards_nr]                      ; ARDS数量加1
            cmp ebx, 0                              ; 若 ebx 为 0 且 cf 不为 1, 这说明 ards 全部返回,当前已是最后一个
            jnz .e820_mem_get_loop                  ; 不为0则循环获取

            ; 在所有ards结构中,找出(base_add_low + length_low)的最大值,即内存的容量
            mov cx, [ards_nr]                       ; 遍历每一个ARDS结构体,循环次数是ARDS的数量
            mov ebx, ards_buf
            xor edx, edx                            ; 用edx记录最大值, 在这里先清零

        .find_max_mem_area:                         ; 无须判断type是否为1,最大的内存块一定是可被使用
            mov eax, [ebx]                          ; base_add_low
            add eax, [ebx+8]                        ; base_add_low + length_low = 这块ADRS容量
            add ebx, 20                             ; 指向下一块ARDS
            cmp edx, eax                            ; 找出最大,edx寄存器始终是最大的内存容量
            jge .next_ards                          ; 如果edx>=eax, 继续遍历下一块
            mov edx, eax                            ; 如果edx<=eax, 更新edx
        .next_ards:
            loop .find_max_mem_area
            jmp .mem_get_ok                         ; 获取内存容量结束


        ; ------  int 15h ax = E801h 获取内存大小,最大支持4G  ------
        ; 返回后, ax cx 值一样,以KB为单位,bx dx值一样,以64KB为单位
        ; 在ax和cx寄存器中为低16M,在bx和dx寄存器中为16MB到4G
        .e820_failed_so_try_e801:
            mov ax, 0xe801
            int 15
            jc .e801_failed_so_try88                ; 若当前e801方法失败,就尝试0x88方法

            ; 1 先算出低15M的内存, ax和cx中是以KB为单位的内存数量,将其转换为以byte为单位
            ; 乘数在eax, 即内存数量, 积高16位在edx, 低16位在eax
            mov cx, 0x400                           ; 0x400 = 1024
            mul cx
            shl edx, 16                             ; 左移16位, 将低16位放到edx高16位
            and eax, 0x0000FFFF                     ; 0x0000FFFF = 1111 1111 1111 1111, 高16位置0
            or  edx, eax                            ; 获得完整的32位积
            add edx, 0x100000                       ; edx比实际少1M, 故要加1MB
            mov esi, edx                            ; 先把低15MB的内存容量存入esi寄存器备份

            ; 2 再将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 15h ah = 0x88 获取内存大小,只能获取64M之内  ----------
        .e801_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
            or  edx, eax                            ; 把积的低16位组合到edx,为32位的积
            add edx, 0x100000                       ; 0x88子功能只会返回1MB以上的内存, 故实际内存大小要加上1MB


        .mem_get_ok:
            mov [total_mem_bytes], edx              ; 将内存换为byte单位后存入total_mem_bytes处


        ; --------------------------------- 设置进入保护模式 -----------------------------
        ; 1 打开A20 gate
        ; 2 加载gdt
        ; 3 将cr0 的 pe位(第0位)置1

        ; -----------------  打开A20  ----------------
        in al, 0x92                                 ; 端口号0x92 中的第1位变成 1 即可
        or al, 0000_0010B
        out 0x92, al

        ; -----------------  加载GDT  ----------------
        lgdt [gdt_ptr]

        ; -----------------  cr0第0位置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: 

            ; 初始化32位的段寄存器
            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

            ; 创建页目录及页表并初始化页内存位图
            call setup_page

            ; gdt需要放在内核里
            ; 将描述符表地址&偏移量写入内存gdt_ptr,一会用新的地址加载
            sgdt [gdt_ptr]                          ; 取出GDT地址和偏移信息,存放在gdt_ptr这个内存位置上

            ; 视频段需要放在内核里与用户进程进行共享
            ; 将gdt描述符中视频段的段基址 + 0xc0000000
            mov ebx, [gdt_ptr + 2]                  ; 这里gdt_ptr前2字节是偏移量,后4字节是GDT基址,先选中GDT
            or dword [ebx + 0x18 + 4], 0xc0000000   ; 一个描述符8字节,0x18处是第3个段描述符也就是视频段, 修改段基址最高位为C, +4进入高4字节, 用or修改即可

            ; 将gdt的基址加上 0xc0000000 成为内核所在的地址
            add dword [gdt_ptr + 2], 0xc0000000
            add esp, 0xc0000000                     ; 将栈指针同样map到内核地址

            ; 页目录地址赋值给cr3
            mov eax, PAGE_DIR_TABLE_POS
            mov cr3, eax

            ; 打开 cr0 的 pg位 (第31位)
            mov eax, cr0
            or  eax, 0x80000000
            mov cr0, eax

            ; 开启分页后, 用 gdt 新的地址重新加载
            lgdt [gdt_ptr]

            ; 重新初始化gs寄存器
            mov ax, SELECTOR_VIDEO
            mov gs,  ax
            mov byte [gs:160], 'V'


            jmp $


    ; -------------   创建页目录及页表   ---------------
    setup_page:

        ; 先把页目录所占空间清 0
        mov ecx, 4096                               ; 1024 * 4 = 4096  
        mov esi, 0

        .clear_page_dir:
            mov byte [PAGE_DIR_TABLE_POS + esi], 0
            inc esi
            loop .clear_page_dir

        ; 开始创建页目录项(Page Directory Entry)
        .create_pde:
            mov eax, PAGE_DIR_TABLE_POS
            add eax, 0x1000                         ; 第一个页表的位置(仅次于页目录表,页目录表大小4KB)
            mov ebx, eax                            ; 0x00101 000, 储存到ebx为创建PTE时使用

            ; 下面将页目录项0和OxcOO都存为第一个页表的地址 ,每个页表表示4MB内存
            ; 这样Oxc03fffff(3G-3G04M)以下的地址和Ox003fffff(0-4M)以下的地址都 指向相同的页表
            ; 这是为将地址映射为内核地址做准备
            or eax,  PG_US_U | PG_RW_W | PG_P       ; 用户特权级,可读可写,存在内存
            mov [PAGE_DIR_TABLE_POS + 0x0], eax     ; 第一个目录项,0x00101 007
            mov [PAGE_DIR_TABLE_POS + 0xc00], eax   ; 第0xc00高10位0x300=768个页表占用的目录项,0xc00以上属于kernel空间
            ; 这里是把第768个目录页和第1个目录页指向同一个页表的物理地址:0x101000   
            ; 系统实际位于0~0x100000内存地址中,将系统虚拟地址0xc00000000映射到这低1M的空间内,只需要让0xc0000000的地址指向和低1M相同的页表即可

            sub eax, 0x1000
            mov [PAGE_DIR_TABLE_POS + 4092], eax    ; 使最后一个目录项指向页目录表自己的位置, 4092 = 1023 * 4

        
        ; 创建页表项(Page Table Entry)
        mov ecx, 256                                ; 1M低端内存/每页大小4K = 256
        mov esi, 0
        mov edx, PG_US_U | PG_RW_W | PG_P           ; edx中地址为0x0,属性为7,即111b

        .create_pte:
            mov  [ebx + esi * 4], edx               ; ebx = 0x00101 000, 即第一个PTE起始地址, 每个PTE = 4 byte
            add  edx, 4096                          ; edx + 4KB
            inc  esi
            loop .create_pte                        ; 低端1M内存中,物理地址=虚拟地址,这里创建了1M空间的页表项


        ; 创建内核其他页表的PDE
        mov eax, PAGE_DIR_TABLE_POS
        add eax, 0x2000                             ; eax指向第二个页表(每个页表对应一个PDE, 含有1024个页表项)
        or  eax, PG_US_U | PG_RW_W | PG_P
        mov ebx, PAGE_DIR_TABLE_POS                 
        mov ecx, 254                                ; 769~1022的所有目录项数量, 1022 - 769 + 1 = 254
        mov esi, 769                                

        .create_kernel_pde:
            mov  [ebx + esi * 4], eax
            inc  esi
            add  eax, 0x1000                        ; eax指向下一个页表
            loop .create_kernel_pde

        ret

在这里插入图片描述


内存布局:
在这里插入图片描述

运行Bochs

编译,写入硬盘。

运行,显示出了V:

在这里插入图片描述

gs寄存器的基地址,还有GDT的基地址,和视频段的基址都是虚拟地址:

在这里插入图片描述

用虚拟地址访问页表

  • 用虚拟地址获取页表中各数据类型的方法:
  • 获取页目录表物理地址:让虚拟地址的高 20 位为 0xfffff,低 12 位为 0x000,即 0xfffff000,这也是页目录表中第 0 个页目录项自身的物理地址
  • 访问页目录中的页目录项,即获取页表物理地址:要使虚拟地址为 0xfffffxxx,其中 xxx 是页目录项的索引乘以 4 的积访问页表中的页表项:要使虚拟地址高 10 位为 0x3ff,目的是获取页目录表物理地址。中间 10 位为页表的索引,因为是 10 位的索引值,所以这里不用乘以 4。低 12 位为页表内的偏移地址,用来定位页表项,它必须是已经乘以 4 后的值

快表简介

为了减少CPU频繁访问内存导致效率下降,于是发明了存储页信息的调整缓存,就是TLB,快表。

快表 TLB (Translation Lookaside Buffer) 中的条目是虚拟地址的高 20 位到物理地址高 20 位的映射结果。除此之外还有一些属性位,比如页表项的 RW 属性。

在这里插入图片描述

修改了页表之后,需要对快表进行更新

TLB对开发人员不可见,但又两种方法可以更新其中的条目:

  • 重新加载CR3
  • TLB 更新指令:invlpg m,其中 m 表示操作数为虚拟内存地址。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值