第08章下 内存管理系统

Posted perfy576

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第08章下 内存管理系统相关的知识,希望对你有一定的参考价值。

程序是指令的集合,程序要运行,必须将其加载早内存中。这就是cpu的cs:ip寄存器是在内存中取指令的原因。

1 内存管理的思路

内核和用户程序分别在自己的地址空间中运行,在实模式下,程序中的地址就等于物理地址,在保护模式下,程序中的地址编程了虚拟地址,虚拟地址对应的物理地址需要经过分页机制的映射。

内存池是将所有可用的内存集中放在一个池子中管理的方式。在分页机制下,因为有虚拟地址和物理地址,所以需要创建管理虚拟地址和物理地址的内存池。内存池中的内存按照页来管理。

为了内核的正常运行(因为我们内核运行在物理内存的低1M中,已经可以正常运行,而这里的运行值得是内核去管理或者位进程提供其他功能,提供这些功能,就需要额外的内存),因此,物理内存中固定一部分分配给内核,该部分的内存不能被用户程序使用,另一部分给用户程序。因此将物理内存分成两部分,一部分内核使用,一部分用户态使用。

首先,要确认的是我们内存管理是要管理还没被使用的内存,意思就是管理那些,到进入内核运行之前还没有被使用内存.而在进入内核之前已经被使用的内存有:

  1. 物理内存的低1MB,是被用来作为内核运行的内存了
  2. 1M向上,紧接着,分别是内核的页表目录,和内核虚拟地址高1G的页表.其中页表目录最后一项指向了自己,所以一共255个页表 ,加上一个页表,所以一共256个页用来构建内核的页表目录和页表了.

所以总共者两部分,一共1M+2K*256的物理内存使用.

然后剩下的物理内存一分为二:

  1. 低位的物理内存给内核使用,因为我们在创建进程和线程的时候要分配结构去管理进程和线程,这也是需要内存的,还有其他的情况,内核在申请内存的会后就从该部分去申请.
  2. 高位的物理内存给用户态使用,当用户态需要申请内存的时候,内核就从该部分去申请物理内存.

然后,物理内存的管理我们使用位图,但是物理内存不同与虚拟内存,物理内存有大小,还有其实分配的物理地址.因此Pool结构去管理物理内存:

struct Pool {
   struct Bitmap btmp;   // 本内存池用到的位图结构,用于管理物理内存
   uint32_t phy_addr_start;  // 本内存池所管理物理内存的起始地址
   uint32_t size;        // 本内存池字节容量
};

Bitmap结构用于管理该区域内存的每一页,该区域内存的起始地址是phy_addr_start.因为管理的物理内存不是从0x0开始的,所以有一个起始地址,该起始地址,配合位图中的位就可以定位一页物理内存.同样应为物理内存是有限的,所以需要size来记录该块物理内存的大小,单位是字节.

同样,虚拟内存也是需要管理的,他管理的主要是那一块虚拟内存空间已经使用了,当用户申请内存的时候,不能将同一物理内存多次分配出去(不考虑收回的情况).因此也需要一个结构去管理虚拟内存.

/* 用于虚拟地址管理 */
struct VirtualAddr {
/* 虚拟地址用到的位图结构,用于记录哪些虚拟地址被占用了。以页为单位。*/
   struct Bitmap vaddr_bitmap;
/* 管理的虚拟地址 */
   uint32_t vaddr_start;
};

虚拟内存的管理,不需要size字段,因为,虚拟内存可以使用全部的32位空间.但是,虚拟内存也需要起始地址,因为内核处在高1G空间,而用户处于低3G空间,所以需要虚拟地址的起始地址.

紧接着,考虑位图结构:

struct Bitmap {
   uint32_t btmp_bytes_len;
/* 在遍历位图时,整体上以字节为单位,细节上是以位为单位,所以此处位图的指针必须是单字节 */
   uint8_t* bits;
};

因为在编译期不能获得要分配位图的大小,所以不能使用数组,而是使用了指针.按常规来说,该指针以后要指向一块内存的.该内存又需要分配.内核的内管管理没搭建好之前又不能进行内存管理.陷入循环.

因此,管理内核内存的位图bits指针指向的内存 ,是在物理内存的1M里面选取一块固定的内存去管理,因为内核的1MB空间不会被分配出去,而且其实这1M空间是由我们分配使用的.所以,和内核相关的三个位图:管理内核物理内存和用户物理内存的内存池,和管理内核内存的虚拟地址的位图结构指针指向的是内核所在的低1M空间里的,且由我们指定位置.

管理用户程序的位图结构,是指向由内核经过内存分配获取的内存.

然后,考虑一下,分配内存的过程(不考虑内存交换的问题,因为到目前我们还能有效的操纵磁盘):

  1. 首先程序申请内存,
  2. 然后,内核去管理虚拟内存的结构中,找一块虚拟内存,取得要返回的该虚拟内存的地址,因为当程序用光了他所有可用的虚拟内存以后,就没有办法再使用内存的.
  3. 接着,对应的内核内存池或是用户态内存池中寻找一块物理内存,获得物理内存的物理地址.那第2步取的虚拟地址最后经过页部件的转换就会对应到该块物理内存上.
  4. 最终的需要,去修改该程序的页表目录,因为程序在使用新的虚拟地址的时候,需要经过页部件,转换为物理地址访问.因此需要在程序的页表目录中建立虚拟地址和物理地址的映射.
  5. 最后,返回虚拟地址.

过程还是很复杂的.其中去修改页表可能是最麻烦的.

下面一步步来.

2 实现:内存管理

2.1 结构体定义

Bitmap用来管理每一个4K的内存(虚拟内存和物理内存).其中bits的指向为:

  1. 内核管理虚拟内存和内核物理内存池,用户空间物理内存池时,指向的是在内核运行的低1M空间中指定的3块区域.
  2. 管理用户虚拟内存时,是内核从内核物理内存池申请的一页,指向的是这一页中的某个部分
struct Bitmap {
   uint32_t btmp_bytes_len;
/* 在遍历位图时,整体上以字节为单位,细节上是以位为单位,所以此处位图的指针必须是单字节 */
   uint8_t* bits;
};

管理虚拟空间的内存池.

/* 用于虚拟地址管理 */
struct VirtualAddr {
/* 虚拟地址用到的位图结构,用于记录哪些虚拟地址被占用了。以页为单位。*/
   struct Bitmap vaddr_bitmap;
/* 管理的虚拟地址 */
   uint32_t vaddr_start;
};

管理物理空间的内存池:

struct Pool {
   struct Bitmap btmp;   // 本内存池用到的位图结构,用于管理物理内存
   uint32_t phy_addr_start;  // 本内存池所管理物理内存的起始地址
   uint32_t size;        // 本内存池字节容量
};

然后,内核当前需要管理的是:

  1. 用户空间物理内存池
  2. 内核空间物理内存池
  3. 内和空间虚拟地址空间(或是内存池)

因此,有如下三个变量:

// 内核用于管理所有物理内存的两个内存池
struct Pool kernel_pool, user_pool;
// 内核用于管理内核虚拟地址空间的内存池
struct VirtualAddr kernel_vaddr;

2.2 物理内存池的初始化

首先需要几个宏,用于定义一些常数:

// 一页由4096字节
#define PG_SIZE 4096
// 三个位图bits中第一个位图指向的内存区域
#define MEM_BMT_START 0xc009a000
// 内核的虚拟地址应设在 0xc0000000
// 其中低1M已经被内核占用了,所以其虚拟地址可以分配的是从
// 0xc0100000 开始的
#define KERNEL_HEAD_START 0xc0100000

然后正式的pool_init()函数

其过程就是:

  1. 首先计算进入内核前已经使用了多少物理内存,这部分物理内存是从物理内存0x0开始使用的.是认为安排的
  2. 然后计算还未使用的物理内存,能够分成多少页.内核态和用户态各一半
  3. 然后开始填充kernel_pool, user_pool,kernel_vaddr这三个结构

这里直接贴全部的代码.

目录结构:

└── bochs
├── 02.tar.gz
├── 03.tar.gz
├── 04.tar.gz
├── 05a.tar.gz
├── 05b.tar.gz
├── 05c.tar.gz
├── 06a.tar.gz
├── 07a.tar.gz
├── 07b.tar.gz
├── 07c.tar.gz
├── 08
│?? ├── boot
│?? │?? ├── include
│?? │?? │?? └── boot.inc
│?? │?? ├── loader.asm
│?? │?? └── mbr.asm
│?? ├── build
│?? │?? ├── bitmap.o
│?? │?? ├── debug.o
│?? │?? ├── idt.o
│?? │?? ├── init.o
│?? │?? ├── interrupt.o
│?? │?? ├── kernel.bin
│?? │?? ├── kernel.o
│?? │?? ├── loader.bin
│?? │?? ├── mbr.bin
│?? │?? ├── memory.o
│?? │?? ├── print.o
│?? │?? ├── string.o
│?? │?? └── timer.o
│?? ├── device
│?? │?? ├── timer.c
│?? │?? └── timer.h
│?? ├── kernel
│?? │?? ├── debug.c
│?? │?? ├── debug.h
│?? │?? ├── global.h
│?? │?? ├── idt.asm
│?? │?? ├── init.c
│?? │?? ├── init.h
│?? │?? ├── interrupt.c
│?? │?? ├── interrupt.h
│?? │?? ├── main.c
│?? │?? ├── memory.c
│?? │?? └── memory.h
│?? ├── lib
│?? │?? ├── kernel
│?? │?? │?? ├── bitmap.c
│?? │?? │?? ├── bitmap.h
│?? │?? │?? ├── io.h
│?? │?? │?? ├── print.asm
│?? │?? │?? ├── print.h
│?? │?? │?? ├── string.c
│?? │?? │?? └── string.h
│?? │?? └── libint.h
│?? ├── makefile
│?? └── start.sh

memory.h`

#ifndef __KERNEL_MEMORY_H
#define __KERNEL_MEMORY_H
#include "libint.h"
#include "bitmap.h"

/* 用于虚拟地址管理 */
struct VirtualAddr {
/* 虚拟地址用到的位图结构,用于记录哪些虚拟地址被占用了。以页为单位。*/
   struct Bitmap btmp;
/* 管理的虚拟地址 */
   uint32_t vaddr_start;
};

extern struct Pool kernel_pool, user_pool;
void mem_init(void);
#endif

因为struct Poll只会用在内核管理物理内存,所以直接定义在.c文件中.而VirtualAddr以后内核还有其他用,所以暴露出去.

#include "libint.h"
#include "memory.h"
#include "bitmap.h"
#include "global.h"
#include "debug.h"
#include "string.h"
#include "print.h"

// 一页由4096字节
#define PG_SIZE 4096
// 三个位图bits中第一个位图指向的内存区域
#define MEM_BMT_START 0xc009a000
// 内核的虚拟地址应设在 0xc0000000
// 其中低1M已经被内核占用了,所以其虚拟地址可以分配的是从
// 0xc0100000 开始的
#define KERNEL_HEAD_START 0xc0100000

struct Pool {
   struct Bitmap btmp;   // 本内存池用到的位图结构,用于管理物理内存
   uint32_t phy_addr_start;  // 本内存池所管理物理内存的起始地址
   uint32_t size;        // 本内存池字节容量
};

// 内核用于管理所有物理内存的两个内存池
struct Pool kernel_pool, user_pool;
// 内核用于管理内核虚拟地址空间的内存池
struct VirtualAddr kernel_vaddr;

// 参数是,在还没打开分页机制之前,使用中断获取的可用物理内存大小.
// 单位是字节
static void pool_init(uint32_t all_mem)
{
      put_str("poll_init start \n");

      // 构建内核的页表目录以及页表使用的内存.(紧挨着内核的1M空间)
      // 页表目录占一页,然后从高1G开始一共占255个页,
      // 最后一个表项指向的是页表目录自身,所以一共 256 页
      uint32_t used_page_table_size = (1 + 255) * PG_SIZE;

      // 在进入内核之前已近使用的内存大小
      // 因为内核运行的内存位于物理内存的低1M,紧接这后面就是内核的页表目录和255个页表
      // 因此已用内存其实就是从1开始,多少字节已经使用.也对应的是物理内存被使用
      uint32_t used_mem = 0x100000 + used_page_table_size;

      uint32_t free_mem = all_mem - used_mem;
      // 因为用户物理内存和内核物理内存池是根据页来分配的,所以具体的去分配页.`
      uint32_t free_mem_page = free_mem / PG_SIZE;

      // 分配内核和用户能够使用的物理内存空间的大小,单位是ye
      uint32_t kernel_free_mem_page = free_mem_page / 2;
      uint32_t user_free_mem_page = (free_mem_page - kernel_free_mem_page);

      // 计算两个物理内存池位图结构的长度,每8个页使用一个字节记录,所以/8
      uint32_t kernel_btmp_len = kernel_free_mem_page / 8;
      uint32_t user_btmp_len = user_free_mem_page / 8;

      // 内存池的的size 记录可用的字节数,而对应位图结构len记录的是位图的长度
      kernel_pool.size = kernel_free_mem_page * PG_SIZE;
      user_pool.size = user_free_mem_page * PG_SIZE;

      kernel_pool.btmp.len = kernel_btmp_len;
      user_pool.btmp.len = user_btmp_len;

      // 设置三个位图的基地址,其都在内存的1M中,地址上三者是紧接的.
      kernel_pool.btmp.bits = (void *)MEM_BMT_START;
      user_pool.btmp.bits = (void *)MEM_BMT_START + kernel_btmp_len;
      kernel_vaddr.btmp.bits = (void *)MEM_BMT_START + kernel_btmp_len + user_btmp_len;

      // used_mem 记录的是已使用的物理内存,是从0x0开始计算的,所以
      // used_mem 就是可以使用的物理内存的开始位置
      kernel_pool.phy_addr_start = used_mem;
      user_pool.phy_addr_start = used_mem + kernel_free_mem_page * PG_SIZE;

      // 内核的虚拟内存地址是从 高1G开始的
      kernel_vaddr.vaddr_start = KERNEL_HEAD_START;

      // 初始化位图结构
      bitmap_init(&kernel_pool.btmp);
      bitmap_init(&user_pool.btmp);
      bitmap_init(&kernel_vaddr.btmp);

      put_str("kernel_pool_bitmap_start:");put_int((int)kernel_pool.btmp.bits);put_str("\n");
      put_str("kernel_pool_phy_addr_start:");put_int(kernel_pool.phy_addr_start);put_str("\n");
      put_str("user_pool_bitmap_start:");put_int((int)user_pool.btmp.bits);put_str("\n");
      put_str("user_pool_phy_addr_start:");put_int(user_pool.phy_addr_start);
      put_str("\n");

      put_str("mem_pool_init done\n");
}

void mem_init()
{
      // 用一个数值,构造一个指针,然后取该指针出的地址.
      uint32_t mem_bytes_total = (*(uint32_t*)(0xc0000b00));
      // mem_bytes_total=32*1024*1024;
      pool_init(mem_bytes_total);
}

然后考虑,这个函数的参数,这个函数的传参比较特殊.首先参数是计算机的物理内存,而这个物理内存需要用汇编获取,然后之前我么你在实模式下已经用中断获取过一次了.进入实模式下,这些中断就不能用了,因为他们是实模式下的中断,所以只能在实模式下获取.然后在打开分页机制下使用.因此,我们需要在实模式获取到物理内存大小的时候,记录的地址,需要我们知道,有这么几种方式

  1. 不改变原先的代码,直接计算出先前计算物理内存容量的.然后在调用的时候,先去构造指针,然后解引用.
  2. 改变原有的代码,直接安排记录物理内存容量的那个内存空间在指定的位置

总之,就是需要通过直接知道内存空间的形式传参.

这里选择直接改变代码:

先看loader.asm

SECTION loader vstart=LOADER_IN_MEM
; 上来就跳转
    jmp 0:loader_start
    
    gdt_base: dq GDT_BASE 
    gdt_code: dq GDT_CODE 
    gdt_data: dq GDT_DATA 
    gdt_vga:  dq GDT_VGA

    gdt_size equ $-gdt_base

; 这里预留出 60 个段描述符的位置
    times 60 dq 0

; 界限,也就是全局段描述符表在内存中的地址位:gdt_base + 界限
; 因此界限=长度-1
    gdt_ptr dw (gdt_size-1)
            dd gdt_base 
; --------------------------------获取物理内存大小--------------------------------
    mem_total dd 0          ; 存放最终结果的内存区域
    ards_buf times 240 db 0 ; ARDS结构,这里定义了240/20个
    ards_counts dw 0        ; 最后需要找到所有的ards结构中长度最大的那个内存最为物理内存,因此需要一个遍历
loader_start:

; ...省略

首先loader.bin加载在:KERNEL_BIN_IN_MEM,然后文件中,64个段描述符,占用64*8字节.加上48/8=6字节的全局描述符寄存器的值,然后加上240字节,的ARDS缓存,然后加上4字节记录ARDS已使用的个数的内存,再加上2字节,用于记录物理内存大小内存空间.这是764字节,也就是说,开头用于定义数据使用了764字节,然后,在ards_buf中再增加4字节.这样就是一共使用了768字节(0x300).

然后去掉开头的jmp,在mbr.asm中,直接jmp LOADER_IN_MEM +0x300,这样就可以直接调到,内存中loader_start的位置去执行.

顺表调整一下mem_total的位置.

%include "boot.inc"

SECTION loader vstart=LOADER_IN_MEM

    gdt_base: dq GDT_BASE 
    gdt_code: dq GDT_CODE 
    gdt_data: dq GDT_DATA 
    gdt_vga:  dq GDT_VGA

    gdt_size equ $-gdt_base

; 这里预留出 60 个段描述符的位置
    times 60 dq 0

    mem_total dd 0          ; 存放最终结果的内存区域


; 界限,也就是全局段描述符表在内存中的地址位:gdt_base + 界限
; 因此界限=长度-1
    gdt_ptr dw (gdt_size-1)
            dd gdt_base 
    ; --------------------------------获取物理内存大小--------------------------------
    ards_buf times 244 db 0 ; ARDS结构,这里定义了240/20个
    ards_counts dw 0        ; 最后需要找到所有的ards结构中长度最大的那个内存最为物理内存,因此需要一个遍历 

loader_start:
; ...省略

这样,mem_total就在物理内存的LOADER_IN_MEM+64*8=0x00000900+0x200=0x00000b00,因此对应的虚拟内存就是0xc0000b00(当然直接使用0x00000b00也可以的).

2.3 运行

mbr.asm只更改了跳转的代码:jmp LOADER_IN_MEM+0x300

loader.asm更改了头部代码

技术分享图片

3 实现内存获取

3.1 内存页获取

首先,实现两个函数,分别是从虚拟内存和物理内存中获取一个地址的函数.

都是先获取,获取了就是set位,如果没获取到,就返回null.

// 从内核的虚拟内存空间获取一页的虚拟地址
static void *kernel_vaddr_get(uint32_t pg_cnt)
{
      // 从内核的虚拟地址池中获取一页
      uint32_t bit_idx_start = bitmap_get(&kernel_vaddr.btmp, pg_cnt);
      uint32_t cnt = 0;

      // 当没有获取到的时候没返回-1,外层函数也能知道
      if (bit_idx_start == -1)
      {
            return NULL;
      }
      while (cnt < pg_cnt)
      {
            // 当获取到了以后,那么设置对应的位已使用
            bitmap_set(&kernel_vaddr.btmp, bit_idx_start + cnt++, 1);
      }
      // 然后转换为实际的虚拟地址
      uint32_t vaddr_start = kernel_vaddr.vaddr_start + bit_idx_start * PG_SIZE;
      return (void *)vaddr_start;
}

// 从给定的内存池中获取一页物理地址
static void *palloc(struct Pool *m_pool)
{
      // 从给定的物理内存池中获取一页
      int bit_idx = bitmap_get(&m_pool->btmp, 1);
      if (bit_idx == -1)
      {
            return NULL;
      }
      // 标记为已使用
      bitmap_set(&m_pool->btmp, bit_idx, 1);
      // 转换为实际的物理地址
      uint32_t page_phyaddr = ((bit_idx * PG_SIZE) + m_pool->phy_addr_start);

      return (void *)page_phyaddr;
}

然后最难的的是去构造虚拟地址到物理地址的映射,也就是填充页表.

由于有从内核内存池和用户内存池两处获取物理内存,因此使用一个函数封装他们:

/* 内存池标记,用于判断用哪个内存池 */
enum PoolFlag {
   PF_KERNEL = 1,    // 内核内存池
   PF_USER = 2       // 用户内存池
};

static void *vaddr_get(enum PoolFlag pf, uint32_t pg_cnt)
{
      void *vaddr_start = 0;
      int bit_idx_start = -1;
      uint32_t cnt = 0;
      if (pf = PF_KERNEL)
      {
            vaddr_start = kernel_vaddr_get(pg_cnt);
      }
      else
      {
            // 用户态的,和面补
      }
      return vaddr_start;
}

3.2 内存页映射

首先回顾一下虚拟地址到物理地址的映射过程:

在构造页表目录的时候,将页表目录的最后一个表项,指向了自己.那么就可以通过这个小技巧达到修改页表目录,建立虚拟地址和物理地址的映射.

假设,我们得到了一个虚拟地址vaddr,32位的:

如果想要获得该虚拟地址,在页表目录中的表项:那么直接构造,高10位是1,中间10位是1,最低12位是该虚拟地址的高12位的新虚拟地址new_vaddr这样转换过程就是:首先新地址的高10位作为下标在页表目录中索引,得到的是页表目录的最后一项,指向的是页表目录自己,然后用new_vaddr的中间10位再次在页表目录中索引(因为高10位最终得到的"页表"还是页表目录),因为中间10位还是全1,所以最后又得到了页表目录本身.最后一次用new_vaddr的低12位再次在页表目录中索引(因为第二次索引到的"页"还是页表目录),因此得到了该虚拟地址所在的页表,他的到的是一个数值,转换为地址,指向的就是虚拟地址在页表目录中的那个表项.

如果想要获得该虚拟地址,在转换过程中,所在的页表中的表项:那么构造,高10位为1,剩下的22位是虚拟地址的高22位的新虚拟地址new_vaddr,这样转换过程就是:首先新地址的高10位作为下表在页表目录的索引,得到的是页表目录的最后一项,指向的是页表目录本身,然后new_vaddr的中间10位再次在页表目录中索引(因为高10位最终得到的"页表"还是页表目录),这此中间10位确实是虚拟地址的高10位,因此这次得到的是虚拟地址在页表目录中的表项,指向的是该虚拟地址所在的页表.最后,用new_vaddr低12位加上页表基址.得到的地址转换为指针,指向的内存空间,就是虚拟地址应该转换为的那物理页的基址.

因此这两个函数为:

uint32_t *pte_ptr(uint32_t vaddr)
{
      uint32_t ret = (uint32_t)(0xffc00000 +                      
                     ((0xffc00000 & vaddr) >> 10) +
                     ((0x003ff000 & vaddr) >> 12) * 4);
      return (uint32_t *)ret;
}

uint32_t *pde_ptr(uint32_t vaddr)
{
      uint32_t ret = (uint32_t)(0xfffff000 +
                     ((vaddr & 0xffc00000) >> 22) * 4);
      return (uint32_t *)ret;
}

3.3 修改页表目录

页表目录的表项,有一个P位,表示该表项代表的内存数据是否在内存中。同时这个表示也可以代表,该表项所能代表的所代表的虚拟内存其中有部分已经分配出去了(是页表目录中的表项)。


static void page_table_add(void *p_vaddr, void *p_phyaddr)
{

      uint32_t vaddr = (uint32_t)p_vaddr;
      uint32_t phyaddr = (uint32_t)p_phyaddr;
      put_str("\n vaddr->phyaddr: ");put_int(vaddr);put_str(" -> ");put_int(phyaddr);
      put_str("\n");

      uint32_t *pde = pde_ptr(vaddr);
      uint32_t *pte = pte_ptr(vaddr);

      // 当页表目录中已经映射过该虚拟地址的表项,
      // 那么直接在他对应的页表中添加表项即可
      if ((*pde & 0x00000001))
      {
            ASSERT(!(*pte & 0x00000001));
            *pte = (phyaddr | PG_US_U | PG_RW_W | PG_P_1);
      }
      else
      {
            // 如果在页表目录中表项不存在,也就是说页表不存在,
            // 那么需要想分配一页物理内存,作为页表,添加在页表目录中
            // 然后再在页表中添加表项
            // 注意:页表目录中的地址都是物理地址
            uint32_t pde_phyaddr = (uint32_t)palloc(&kernel_pool);
            *pde = (pde_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
            memset((void *)((int)pte & 0xfffff000), 0, PG_SIZE);
            *pte = (phyaddr | PG_US_U | PG_RW_W | PG_P_1);
      }
}

因为在loader.asm中已经做好了内核页表目录的高 1G的表项的映射(当然,只是设置了指向,但是具体的页表没有分配),因此对于做内核虚拟地址的映射,因此if的判断条件总是1.

3.4 内核获取页

该部分封装上面所有的操作:

void *malloc_page(enum PoolFlag pf, uint32_t pg_cnt)
{
      ASSERT(pg_cnt > 0 && pg_cnt < 3840);

      // 先去获取一个连续的虚拟地址
      void *vaddr_start = vaddr_get(pf, pg_cnt);
      if (vaddr_start == NULL)
      {
            return NULL;
      }

      uint32_t vaddr = (uint32_t)vaddr_start;
      uint32_t cnt = pg_cnt;

      struct Pool *mem_pool = pf == PF_KERNEL ? &kernel_pool : &user_pool;

      // 一页一页的去获取物理地址,因为物理地址不一定连续
      // 所以一页一页的获取,然后去做映射
      while (cnt-- > 0)
      {
            void *page_phyaddr = palloc(mem_pool);
            if (page_phyaddr == NULL)
            { 
                  // 失败时要将曾经已申请的虚拟地址和物理页全部回滚,在将来完成内存回收时再补充
                  return NULL;
            }
            page_table_add((void *)vaddr, page_phyaddr); 
            // 继续做下一个虚拟地址的映射
            vaddr += PG_SIZE;                            
      }

      return vaddr_start;
}

void *get_kernel_pages(uint32_t pg_cnt)
{
      void *vaddr = malloc_page(PF_KERNEL, pg_cnt);
      if (vaddr != NULL)
      { // 若分配的地址不为空,将页框清0后返回
            memset(vaddr, 0, pg_cnt * PG_SIZE);
      }
      return vaddr;
}

4 代码

目录结构:

└── bochs
├── 02.tar.gz
├── 03.tar.gz
├── 04.tar.gz
├── 05a.tar.gz
├── 05b.tar.gz
├── 06a.tar.gz
├── 07a.tar.gz
├── 07b.tar.gz
├── 07c.tar.gz
├── 08
│?? ├── boot
│?? │?? ├── include
│?? │?? │?? └── boot.inc
│?? │?? ├── loader.asm
│?? │?? └── mbr.asm
│?? ├── build
│?? │?? ├── bitmap.o
│?? │?? ├── debug.o
│?? │?? ├── idt.o
│?? │?? ├── init.o
│?? │?? ├── interrupt.o
│?? │?? ├── kernel.bin
│?? │?? ├── kernel.o
│?? │?? ├── loader.bin
│?? │?? ├── mbr.bin
│?? │?? ├── memory.o
│?? │?? ├── print.o
│?? │?? ├── string.o
│?? │?? └── timer.o
│?? ├── device
│?? │?? ├── timer.c
│?? │?? └── timer.h
│?? ├── kernel
│?? │?? ├── debug.c
│?? │?? ├── debug.h
│?? │?? ├── global.h
│?? │?? ├── idt.asm
│?? │?? ├── init.c
│?? │?? ├── init.h
│?? │?? ├── interrupt.c
│?? │?? ├── interrupt.h
│?? │?? ├── main.c
│?? │?? ├── memory.c
│?? │?? └── memory.h
│?? ├── lib
│?? │?? ├── kernel
│?? │?? │?? ├── bitmap.c
│?? │?? │?? ├── bitmap.h
│?? │?? │?? ├── io.h
│?? │?? │?? ├── print.asm
│?? │?? │?? ├── print.h
│?? │?? │?? ├── string.c
│?? │?? │?? └── string.h
│?? │?? └── libint.h
│?? ├── makefile
│?? └── start.sh
└── hd60m.img

4.1 mbr.asm

修改很小,就是,修改了跳转的地址,加上了0x300,这是人为的安排loader.asm中数据的位置和大小得出来的:

%include "boot.inc"

; mbr.asm 主引导程序 
SECTION MBR vstart=0x7c00

; ---------- 初始化各个寄存器 ----------
    ; 使用cs寄存器的值去初始化其他的段寄存器.
    mov ax,cs
    mov ds,ax
    mov ss,ax
    mov fs,ax
    
    ; 设置栈指针
    mov sp,0x7c00
    
; ---------- 使用中断清除屏幕----------
; 清除屏幕
; INT 0x10 功能号0x06,AH中存功能号,AL中存上卷行数,BH中存上卷行属性
; ah:子功能号,al:0表示0x06子功能号,向上卷屏幕的行数,0表示全部
; bh:空白区域缺省属性
; cx,dx:上卷时候认为屏幕的大小,也就是只上卷cx,dx标识的矩形区域内的字符.
    mov ax,0x600
    mov bx,0x0700
    mov cx,0x0
    mov dx,0x184f

    int 0x10

; 设置光标位置
; INT 0x10 子功能号0x02,AH中存子功能号
; bh:显示页码,dh:y,dl:x
    
    mov ax,0x02
    mov dx,0x0 

    int 0x10

; ---------- 使用中断在屏幕上打印字符串 ----------
; 显示字符串
; int 0x10 子功能号0x13 
; es:bp:字符串起始地址,bh:页码,cx:字符串长度,al输出模式

    mov ax,msg_start
    mov bp,ax
    
    mov cx,msg_end-msg_start
    mov bx,0x0002
    mov ax,0x1301
    int 0x10

; ---------- 直接操作显卡内存,打印字符 ----------
; 直接读写显卡映射内存
    mov ax,0xb800
    mov gs,ax

    mov byte [gs:320],' '
    mov byte [gs:321],00001111b
    mov byte [gs:322],'d'
    mov byte [gs:323],00001111b
    mov byte [gs:324],'i'
    mov byte [gs:325],00001111b
    mov byte [gs:326],'r'
    mov byte [gs:327],00001111b
    mov byte [gs:328],'e'
    mov byte [gs:329],00001111b
    mov byte [gs:330],'c'
    mov byte [gs:331],00001111b
    mov byte [gs:332],'t'
    mov byte [gs:333],00001111b

; ---------- 加载 loader ----------
; 设置调用rd_disk_m_16时候的3个参数.
; eax 是要读取磁盘的LBA地址
; bx 是读取出来的数据,加载到内存的位置
; cx 是要读取的扇区数.
    mov eax,LOADER_IN_DISK
    mov bx,LOADER_IN_MEM
    mov cx,4

    mov byte [gs:640],'r'
    mov byte [gs:641],00001111b
    mov byte [gs:642],'e'
    mov byte [gs:643],00001111b
    mov byte [gs:644],'a'
    mov byte [gs:645],00001111b
    mov byte [gs:646],'d'
    mov byte [gs:647],00001111b
    mov byte [gs:648],' '
    mov byte [gs:649],00001111b
    mov byte [gs:650],'d'
    mov byte [gs:651],00001111b
    mov byte [gs:652],'i'
    mov byte [gs:653],00001111b
    mov byte [gs:654],'s'
    mov byte [gs:655],00001111b
    mov byte [gs:656],'k'
    mov byte [gs:657],00001111b

; 开始读取
    call rd_disk_m_16

    mov byte [gs:800],'j'
    mov byte [gs:801],00001111b
    mov byte [gs:802],'m'
    mov byte [gs:803],00001111b
    mov byte [gs:804],'p'
    mov byte [gs:805],00001111b

; 因为读取的是 内核加载器loader ,因此读取完成以后,直接跳转过去执行
    jmp LOADER_IN_MEM + 0x300

; ---------- 从磁盘中能够读取数据的函数 ----------
; 功能:读取硬盘n个扇区
; 参数:
; eax:开始读取的磁盘扇区
; cx:读取的扇区个数
; bx:数据送到内存中的起始位置

rd_disk_m_16:
; 这里要保存eax 的原因在与,下面section count 寄存器需要一个8位的寄存器
; 只有acbd这四个寄存器能够拆分为高低8位来使用,而dx作为寄存器号,被占用了
; 因此需要个abc三个寄存器中一个来用,这里选择了 ax
    mov esi,eax
    mov di,cx

; 0x1f2 寄存器:sector count ,读写的时候都表示要读写的扇区数目
; 该寄存器是8位的,因此送入的数据位 cl
    mov dx,0x1f2
    mov al,cl 
    out dx,al 

; 恢复eax
    mov eax,esi 

; eax中存放的是要读取的扇区开始标号,是一个32位的值,因此 al 是低8位
; 0x1f3 存放0~7位的LBA地址,该寄存器是一个8位的
    mov dx,0x1f3
    out dx,al 

; 下面的 0x1f4 和5 分别是8~15,16~23位LBA地址,这俩寄存器都是8位的
; 因此是用shr,将eax右移8位,然后每次都用al取eax中的低8位
    mov cl,8 
    shr eax,cl 
    mov dx,0x1f4 
    out dx,al 

    shr eax,cl
    mov dx,0x1f5 
    out dx,al 

; 0x1f6 寄存器低4位存放 24~27位LBA地址,
; 0x1f6 寄存器是一个杂项,其第六位,1标识LBA地址模式,0标识CHS模式
; 上面使用的是LBA地址,因此第六位位1
    shr eax,cl 
    and al,0x0f 
    or al,0xe0    
    mov dx,0x1f6 
    out dx,al 
    
; 0x1f7 寄存器,读取该寄存器的时候,其中的数据是磁盘的状态
; 写到该寄存器的时候,写入的僵尸要执行的命令,写入以后,直接开始执行命令
; 因此需要在写该寄存器的时候,将所有参数设置号
; 0x20 表示读扇区,0x30写扇区
    mov dx,0x1f7 
    mov al,0x20
    out dx,al 

    .not_ready:
; 读 0x1f7 判断数据是否就绪,没就绪就循环等待.
        nop
        in al,dx
        and al,0x88 
        cmp al,0x08 
        jnz .not_ready

; 到这一步表示数据就绪,设置各项数据,开始读取
; 一个扇区512字节,每次读2字节,因此读一个扇区需要256次从寄存器中读取数据
; di 中是最开始的cx也就是要读取的扇区数
; mul dx 是ax=ax * dx ,因此最终ax 中是要读取的次数
    mov ax,di
    mov dx,256 
    mul dx  
    mov cx,ax 

; 0x1f0 寄存器是一个16位寄存器,读写的时候,都是数据.
    mov dx,0x1f0

    .go_on_read:
        in ax,dx 
        mov [bx],ax 
        add bx,2  ;ax 是 16位寄存器,读出的也是2字节,因此读一次 dx+2
        loop .go_on_read
        ret 

    msg_start db "2 mbr start!!!!!"
    msg_end db 0

    times 510-($-$$) db 0
    db 0x55,0xaa

4.2 loader.asm

调整了,开头数据部分的顺序和大小。让mem_total的位置可控,便于在mem_init()中获取到物理内存的大小。

%include "boot.inc"

SECTION loader vstart=LOADER_IN_MEM

    gdt_base: dq GDT_BASE 
    gdt_code: dq GDT_CODE 
    gdt_data: dq GDT_DATA 
    gdt_vga:  dq GDT_VGA

    gdt_size equ $-gdt_base

; 这里预留出 60 个段描述符的位置
    times 60 dq 0

    mem_total dd 0          ; 存放最终结果的内存区域


; 界限,也就是全局段描述符表在内存中的地址位:gdt_base + 界限
; 因此界限=长度-1
    gdt_ptr dw (gdt_size-1)
            dd gdt_base 
    ; --------------------------------获取物理内存大小--------------------------------
    ards_buf times 244 db 0 ; ARDS结构,这里定义了240/20个
    ards_counts dw 0        ; 最后需要找到所有的ards结构中长度最大的那个内存最为物理内存,因此需要一个遍历 

loader_start:
    xor ebx,ebx             ;初始ebx为0,告诉bios从头开始遍历每种类型的内存,后续该寄存器的值由bios更新,由bios使用
    mov edx,0x534d4150
    mov di,ards_buf

    .get_mem_size_e820:
        mov eax,0x0000e820 ; 子功能号
        mov ecx,20         ; 每次填充的大小,类似于指示可用buf区大小的意思

        int 0x15
        add di,cx          ; di中保存的是buf
        inc word [ards_counts] ;计数

        cmp ebx,0
        jnz .get_mem_size_e820

    ; 在所有的ards结构中找内存最大值
    mov ebx,ards_buf        ;ebx 中存放的是地址
    mov ecx,[ards_counts]    ;ecx 中存放的是次数,需要取地址

    xor edx,edx ;保存当前最大值

    .find_max_mem:
        mov eax,[ebx]
        add eax,[ebx+8]
        add ebx,20
        cmp edx,eax
        jge .next_ards
        mov edx,eax 
    .next_ards:
        loop .find_max_mem
        jmp .set_max_mem
        

    .set_max_mem:
        mov [mem_total],edx ;最终结果存放
; --------------------------------获取物理内存大小--------------------------------


    mov ax,0xb800
    mov gs,ax

    mov byte [gs:1120],'l'
    mov byte [gs:1121],00000111b
    mov byte [gs:1122],'o'
    mov byte [gs:1123],00000111b
    mov byte [gs:1124],'a'
    mov byte [gs:1125],00000111b
    mov byte [gs:1126],'d'
    mov byte [gs:1127],00000111b
    mov byte [gs:1128],'e'
    mov byte [gs:1129],00000111b
    mov byte [gs:1130],'r'
    mov byte [gs:1131],00000111b
    mov byte [gs:1132],'!'
    mov byte [gs:1133],00000111b

; 开启A20
    in al,0x92
    or al,000000010b 
    out 0x92,al
; 开启保护模式
    mov eax,cr0
    or eax,0x00000001
    mov cr0,eax
; 加载GDT
    lgdt [gdt_ptr]

; 流水线的原因,要强制刷新一次流水线
; 这里要用远跳转,因为进入了保护模式了,cs寄存器中存的不在是段基址,而是选择子
    jmp select_code:p_mode

; 主动告诉编译器下面的代码按照32位机器码编译
[bits 32]
p_mode:
    ; 注意这里ss段寄存器,也被赋值为 select_data
    mov ax,select_data
    mov ds,ax
    mov es,ax
    mov ss,ax
    mov esp,LOADER_STACK_TOP

    mov ax,select_vag
    mov gs,ax
    mov byte [gs:1440],'p'

; ------------------------------  新加代码 1 start   ------------------------------
    ; 加载内核的ELF文件
    mov eax,KERNEL_BIN_IN_DISK
    mov ebx,KERNEL_BIN_IN_MEM
    mov ecx,200 
    call rd_disk_m_32
; ------------------------------  新加代码 1 end     ------------------------------

    mov byte [gs:1442],'r'
    
    ; 构建页表目录
    call setup_page

    ; 修改全局描述符表
    sgdt [gdt_ptr]
    mov ebx,[gdt_ptr+2]
    or dword [ebx+0x18+4],0xc0000000
    add dword [gdt_ptr+2],0xc0000000

    add esp,0xc0000000

    ; 将页表目录放入cr3
    mov eax,PAGE_DIR_TABLE_POS
    mov cr3,eax

    ; 打开cr0的pg位
    mov eax,cr0
    or eax,0x80000000
    mov cr0,eax 

    ; 重新加载 全局描述符表
    lgdt [gdt_ptr]
    
    mov byte [gs:1600],'V'

; ------------------------------  新加代码 2 start   ------------------------------
    ; 解析内核文件,病跳转执行
    call kernel_init
    ; 需要设置栈指针
    mov esp,0xc009f000
    jmp KERNEL_ENTRY
; ------------------------------  新加代码 2 end     ------------------------------


;  ----------------------------------- 构建内核页表目录 -----------------------------------
setup_page:
    ; 首先使用 clear_page 将页表目录所在的那一页 PAGE_DIR_TABLE_POS 开始
    ; 的4K空间清零
    mov ecx,4096
    mov esi,0
    .clear_page:
        mov byte [PAGE_DIR_TABLE_POS+esi],0
        inc esi
        loop .clear_page
    
    ; 将内核页表目录的第一个页表和内核在3G处的页表设为同一个,为了是在一进入分页机制后,虚拟地址映射正常
    .create_pde:
        mov eax,PAGE_DIR_TABLE_POS
        add eax,0x1000                        ; 页表目录后面紧跟着就是第一个页表 eax+4K,
        mov ebx,eax                         ; 因此eax中存的是第一个页表的地址
        or eax,PAGE_US_U|PAGE_RW_W|PAGE_P   ; eax中是第一个页表的地址,然后设置属性,让他成为第一个页表项
        
        mov [PAGE_DIR_TABLE_POS],eax        ; 将第一个页表放在页表目录的第0个表项和高1G内存开始的地方.

        mov [PAGE_DIR_TABLE_POS+768*4],eax    ;768=3*1024*1024 /(4*1024),所以高1G是在第768个页表,*4是因为,每个表项4字节

        sub eax,4096                        ; eax是第一个页表的地址,减去4KB,就是 PAGE_DIR_TABLE_POS
        mov [PAGE_DIR_TABLE_POS+4092],eax   ; 将页表目录最后一项设为自己的地址

    ; 填充第一个页表
        mov ecx,256             
        mov esi,0
        mov edx,PAGE_US_U|PAGE_RW_W|PAGE_P  ; edx现在是第一个页表中的第一个表项,他代表的是物理内存的第一页.

    .create_pte:
        mov [ebx+esi*4],edx                 ; 将物理内存从0开始依次映射到第一个页表中
        add edx,4096
        inc esi 
        loop .create_pte

    ; 填充高1G的其他页表目录项,页表目录所在内存页的后面页就放着页表,也就是说这些页表,在物理内存上是连续的.
    mov eax,PAGE_DIR_TABLE_POS
    add eax,4096*2                      ; eax 中现在是第一个页表的地址
    or eax,PAGE_US_U|PAGE_RW_W|PAGE_P 
    mov ebx,PAGE_DIR_TABLE_POS
    mov ecx,254                         ;第一个和最后一个已经创建完了,所以是256-2个
    mov esi,769                         ;是高1G的第2个页表

    .create_kernel_pde:
        mov [ebx,esi*4],eax
        inc esi 
        add eax,4096 
        loop .create_kernel_pde
        ret 

; ------------------------------  新加代码 1 start  ------------------------------

; 功能:保护模式下读取硬盘n个扇区
; 参数:
; eax:开始读取的磁盘扇区
; cx:读取的扇区个数
; bx:数据送到内存中的起始位置
rd_disk_m_32:
; 这里要保存eax 的原因在与,下面section count 寄存器需要一个8位的寄存器
; 只有acbd这四个寄存器能够拆分为高低8位来使用,而dx作为寄存器号,被占用了
; 因此需要个abc三个寄存器中一个来用,这里选择了 ax
    mov esi,eax
    mov di,cx

; 0x1f2 寄存器:sector count ,读写的时候都表示要读写的扇区数目
; 该寄存器是8位的,因此送入的数据位 cl
    mov dx,0x1f2
    mov al,cl 
    out dx,al 

; 恢复eax
    mov eax,esi 

; eax中存放的是要读取的扇区开始标号,是一个32位的值,因此 al 是低8位
; 0x1f3 存放0~7位的LBA地址,该寄存器是一个8位的
    mov dx,0x1f3
    out dx,al 

; 下面的 0x1f4 和5 分别是8~15,16~23位LBA地址,这俩寄存器都是8位的
; 因此是用shr,将eax右移8位,然后每次都用al取eax中的低8位
    mov cl,8 
    shr eax,cl 
    mov dx,0x1f4 
    out dx,al 

    shr eax,cl
    mov dx,0x1f5 
    out dx,al 

; 0x1f6 寄存器低4位存放 24~27位LBA地址,
; 0x1f6 寄存器是一个杂项,其第六位,1标识LBA地址模式,0标识CHS模式
; 上面使用的是LBA地址,因此第六位位1
    shr eax,cl 
    and al,0x0f 
    or al,0xe0    
    mov dx,0x1f6 
    out dx,al 
    
; 0x1f7 寄存器,读取该寄存器的时候,其中的数据是磁盘的状态
; 写到该寄存器的时候,写入的僵尸要执行的命令,写入以后,直接开始执行命令
; 因此需要在写该寄存器的时候,将所有参数设置号
; 0x20 表示读扇区,0x30写扇区
    mov dx,0x1f7 
    mov al,0x20
    out dx,al 

    .not_ready:
; 读 0x1f7 判断数据是否就绪,没就绪就循环等待.
        nop
        in al,dx
        and al,0x88 
        cmp al,0x08 
        jnz .not_ready

; 到这一步表示数据就绪,设置各项数据,开始读取
; 一个扇区512字节,每次读2字节,因此读一个扇区需要256次从寄存器中读取数据
; di 中是最开始的cx也就是要读取的扇区数
; mul dx 是ax=ax * dx ,因此最终ax 中是要读取的次数
    mov ax,di
    mov dx,256 
    mul dx  
    mov cx,ax 

; 0x1f0 寄存器是一个16位寄存器,读写的时候,都是数据.
    mov dx,0x1f0

    .go_on_read:
        in ax,dx 
        mov [ebx],ax ;区别在这里,使用的是ebx
        add ebx,2  ;ax 是 16位寄存器,读出的也是2字节,因此读一次 dx+2
        loop .go_on_read
    ret 

; 功能:解析内核ELF文件
; 不需要参数,因为参数是 KERNEL_BIN_IN_MEM ,而且代码只是用一次
; 因此参数直接写死,不更改
kernel_init:
    xor eax,eax
    xor ebx,ebx
    xor ecx,ecx
    xor edx,edx

    ; 偏移42字节处是 e_phentsize是每个程序头结构的大小
    mov dx,[KERNEL_BIN_IN_MEM+42]
    ; 偏移28字节处是 e_phoff,是第一个 程序头结构在文件中的偏移
    mov ebx,[KERNEL_BIN_IN_MEM+28]

    ; 加上 KERNEL_BIN_IN_MEM ,就是第一个程序头在文件中的偏移地址
    add ebx,KERNEL_BIN_IN_MEM
    ; 偏移44字节处是 e_phnum ,是程序头结构的个数
    mov cx,[KERNEL_BIN_IN_MEM+44]

    ; 遍历每个程序头结构,按照其 p_offset,加载到最内存的指定位置
    .each_segment:
        ; p_type =0 则该程序头未使用,跳过
        cmp byte [ebx+0],ELF_PT_NULL
        je .pt_null 

        ; 程序头结构的偏移16 字节是 p_filesz 是结构的大小 
        push dword [ebx+16]
        ; 程序头结构的偏移16 字节是 p_offset 是该结构在ELF文件中的偏移地址
        mov eax,[ebx+4]
        ; 加上 KERNEL_BIN_IN_MEM 
        add eax,KERNEL_BIN_IN_MEM
        push eax 
        ; 程序头结构的偏移4 字节是 p_vaddr 是该结构最终 加载在内存中地址
        push dword [ebx+8]
        call memcpy

        add esp,12 
    .pt_null:
        add ebx,edx 
        loop .each_segment 
    ret

memcpy:
    cld 
    push ebp 
    mov ebp,esp 

    push ecx 
    mov edi,[ebp+8]
    mov esi,[ebp+12]
    mov ecx,[ebp+16]
    rep movsb 
    pop ecx 
    pop ebp 
    ret 
; ------------------------------  新加代码 2    end  ------------------------------

4.3 memory.h、memory.c

memory.h定义暴露给外部的接口

#ifndef __KERNEL_MEMORY_H
#define __KERNEL_MEMORY_H
#include "libint.h"
#include "bitmap.h"

/* 内存池标记,用于判断用哪个内存池 */
enum PoolFlag {
   PF_KERNEL = 1,    // 内核内存池
   PF_USER = 2       // 用户内存池
};

#define  PG_P_1   1 // 页表项或页目录项存在属性位
#define  PG_P_0   0 // 页表项或页目录项存在属性位
#define  PG_RW_R  0 // R/W 属性位值, 读/执行
#define  PG_RW_W  2 // R/W 属性位值, 读/写/执行
#define  PG_US_S  0 // U/S 属性位值, 系统级
#define  PG_US_U  4 // U/S 属性位值, 用户级

/* 用于虚拟地址管理 */
struct VirtualAddr {
/* 虚拟地址用到的位图结构,用于记录哪些虚拟地址被占用了。以页为单位。*/
   struct Bitmap btmp;
/* 管理的虚拟地址 */
   uint32_t vaddr_start;
};

extern struct Pool kernel_pool, user_pool;
void mem_init(void);
void* get_kernel_pages(uint32_t pg_cnt);
void* malloc_page(enum PoolFlag pf, uint32_t pg_cnt);
void malloc_init(void);
uint32_t* pte_ptr(uint32_t vaddr);
uint32_t* pde_ptr(uint32_t vaddr);
#endif

具体实现:

#include "libint.h"
#include "memory.h"
#include "bitmap.h"
#include "global.h"
#include "debug.h"
#include "string.h"
#include "print.h"

// 一页由4096字节
#define PG_SIZE 4096
// 三个位图bits中第一个位图指向的内存区域
#define MEM_BMT_START 0xc009a000
// 内核的虚拟地址应设在 0xc0000000
// 其中低1M已经被内核占用了,所以其虚拟地址可以分配的是从
// 0xc0100000 开始的
#define KERNEL_HEAD_START 0xc0100000

struct Pool
{
      struct Bitmap btmp;      // 本内存池用到的位图结构,用于管理物理内存
      uint32_t phy_addr_start; // 本内存池所管理物理内存的起始地址
      uint32_t size;           // 本内存池字节容量
};

// 内核用于管理所有物理内存的两个内存池
struct Pool kernel_pool, user_pool;
// 内核用于管理内核虚拟地址空间的内存池
struct VirtualAddr kernel_vaddr;

// 参数是,在还没打开分页机制之前,使用中断获取的可用物理内存大小.
// 单位是字节
static void pool_init(uint32_t all_mem)
{
      put_str("poll_init start \n");

      // 构建内核的页表目录以及页表使用的内存.(紧挨着内核的1M空间)
      // 页表目录占一页,然后从高1G开始一共占255个页,
      // 最后一个表项指向的是页表目录自身,所以一共 256 页
      uint32_t used_page_table_size = (1 + 255) * PG_SIZE;

      // 在进入内核之前已近使用的内存大小
      // 因为内核运行的内存位于物理内存的低1M,紧接这后面就是内核的页表目录和255个页表
      // 因此已用内存其实就是从1开始,多少字节已经使用.也对应的是物理内存被使用
      uint32_t used_mem = 0x100000 + used_page_table_size;

      uint32_t free_mem = all_mem - used_mem;
      // 因为用户物理内存和内核物理内存池是根据页来分配的,所以具体的去分配页.`
      uint32_t free_mem_page = free_mem / PG_SIZE;

      // 分配内核和用户能够使用的物理内存空间的大小,单位是ye
      uint32_t kernel_free_mem_page = free_mem_page / 2;
      uint32_t user_free_mem_page = (free_mem_page - kernel_free_mem_page);

      // 计算两个物理内存池位图结构的长度,每8个页使用一个字节记录,所以/8
      uint32_t kernel_btmp_len = kernel_free_mem_page / 8;
      uint32_t user_btmp_len = user_free_mem_page / 8;

      // 内存池的的size 记录可用的字节数,而对应位图结构len记录的是位图的长度
      kernel_pool.size = kernel_free_mem_page * PG_SIZE;
      user_pool.size = user_free_mem_page * PG_SIZE;

      kernel_pool.btmp.len = kernel_btmp_len;
      user_pool.btmp.len = user_btmp_len;
      kernel_vaddr.btmp.len = kernel_btmp_len;

      // 设置三个位图的基地址,其都在内存的1M中,地址上三者是紧接的.
      kernel_pool.btmp.bits = (void *)MEM_BMT_START;
      user_pool.btmp.bits = (void *)MEM_BMT_START + kernel_btmp_len;
      kernel_vaddr.btmp.bits = (void *)MEM_BMT_START + kernel_btmp_len + user_btmp_len;

      // used_mem 记录的是已使用的物理内存,是从0x0开始计算的,所以
      // used_mem 就是可以使用的物理内存的开始位置
      kernel_pool.phy_addr_start = used_mem;
      user_pool.phy_addr_start = used_mem + kernel_free_mem_page * PG_SIZE;

      // 内核的虚拟内存地址是从 高1G开始的
      kernel_vaddr.vaddr_start = KERNEL_HEAD_START;

      // 初始化位图结构
      bitmap_init(&kernel_pool.btmp);
      bitmap_init(&user_pool.btmp);
      bitmap_init(&kernel_vaddr.btmp);

      put_str("   kernel_pool_bitmap_start:");
      put_int((int)kernel_pool.btmp.bits);
      put_str("\n");
      put_str("   kernel_pool_phy_addr_start:");
      put_int(kernel_pool.phy_addr_start);
      put_str("\n");
      put_str("   kernel_pool_phy_len:");
      put_int(kernel_pool.btmp.len);
      put_str("\n");

      put_str("   user_pool_bitmap_start:");
      put_int((int)user_pool.btmp.bits);
      put_str("\n");
      put_str("   user_pool_phy_addr_start:");
      put_int(user_pool.phy_addr_start);
      put_str("\n");
      put_str("   user_pool_phy_len:");
      put_int(user_pool.btmp.len);
      put_str("\n");
      put_str("   user_pool_phy_len:");
      put_int(user_pool.btmp.len);
      put_str("\n");

      put_str("poll_init done\n");
}

// 从内核的虚拟内存空间获取一页的虚拟地址
static void *kernel_vaddr_get(uint32_t pg_cnt)
{
      // 从内核的虚拟地址池中获取一页
      uint32_t bit_idx_start = bitmap_get(&kernel_vaddr.btmp, pg_cnt);
      uint32_t cnt = 0;

      // 当没有获取到的时候没返回-1,外层函数也能知道
      if (bit_idx_start == -1)
      {
            return NULL;
      }
      while (cnt < pg_cnt)
      {
            // 当获取到了以后,那么设置对应的位已使用
            bitmap_set(&kernel_vaddr.btmp, bit_idx_start + cnt++, 1);
      }
      // 然后转换为实际的虚拟地址
      uint32_t vaddr_start = kernel_vaddr.vaddr_start + bit_idx_start * PG_SIZE;
      return (void *)vaddr_start;
}

// 从给定的内存池中获取一页物理地址
static void *palloc(struct Pool *m_pool)
{
      // 从给定的物理内存池中获取一页
      int bit_idx = bitmap_get(&m_pool->btmp, 1);
      if (bit_idx == -1)
      {
            return NULL;
      }
      // 标记为已使用
      bitmap_set(&m_pool->btmp, bit_idx, 1);
      // 转换为实际的物理地址
      uint32_t page_phyaddr = ((bit_idx * PG_SIZE) + m_pool->phy_addr_start);

      return (void *)page_phyaddr;
}


static void *vaddr_get(enum PoolFlag pf, uint32_t pg_cnt)
{
      void *vaddr_start = 0;
      int bit_idx_start = -1;
      uint32_t cnt = 0;
      if (pf = PF_KERNEL)
      {
            vaddr_start = kernel_vaddr_get(pg_cnt);
      }
      else
      {
            // 用户态的,和面补
      }
      return vaddr_start;
}



uint32_t *pte_ptr(uint32_t vaddr)
{
      uint32_t ret = (uint32_t)(0xffc00000 +                      
                                ((0xffc00000 & vaddr) >> 10) +
                                ((0x003ff000 & vaddr) >> 12) * 4);
      return (uint32_t *)ret;
}

uint32_t *pde_ptr(uint32_t vaddr)
{
      uint32_t ret = (uint32_t)(0xfffff000 +
                                ((vaddr & 0xffc00000) >> 22) * 4);
      return (uint32_t *)ret;
}

static void page_table_add(void *p_vaddr, void *p_phyaddr)
{

      uint32_t vaddr = (uint32_t)p_vaddr;
      uint32_t phyaddr = (uint32_t)p_phyaddr;
      put_str("\n vaddr->phyaddr: ");put_int(vaddr);put_str(" -> ");put_int(phyaddr);
      put_str("\n");

      uint32_t *pde = pde_ptr(vaddr);
      uint32_t *pte = pte_ptr(vaddr);

      // 当页表目录中已经映射过该虚拟地址的表项,
      // 那么直接在他对应的页表中添加表项即可
      if ((*pde & 0x00000001))
      {
            ASSERT(!(*pte & 0x00000001));
            *pte = (phyaddr | PG_US_U | PG_RW_W | PG_P_1);
      }
      else
      {
            // 如果在页表目录中表项不存在,也就是说页表不存在,
            // 那么需要想分配一页物理内存,作为页表,添加在页表目录中
            // 然后再在页表中添加表项
            // 注意:页表目录中的地址都是物理地址
            uint32_t pde_phyaddr = (uint32_t)palloc(&kernel_pool);
            *pde = (pde_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
            memset((void *)((int)pte & 0xfffff000), 0, PG_SIZE);
            *pte = (phyaddr | PG_US_U | PG_RW_W | PG_P_1);
      }
}

void *malloc_page(enum PoolFlag pf, uint32_t pg_cnt)
{
      ASSERT(pg_cnt > 0 && pg_cnt < 3840);

      // 先去获取一个连续的虚拟地址
      void *vaddr_start = vaddr_get(pf, pg_cnt);
      if (vaddr_start == NULL)
      {
            return NULL;
      }

      uint32_t vaddr = (uint32_t)vaddr_start;
      uint32_t cnt = pg_cnt;

      struct Pool *mem_pool = pf == PF_KERNEL ? &kernel_pool : &user_pool;

      // 一页一页的去获取物理地址,因为物理地址不一定连续
      // 所以一页一页的获取,然后去做映射
      while (cnt-- > 0)
      {
            void *page_phyaddr = palloc(mem_pool);
            if (page_phyaddr == NULL)
            { 
                  // 失败时要将曾经已申请的虚拟地址和物理页全部回滚,在将来完成内存回收时再补充
                  return NULL;
            }
            page_table_add((void *)vaddr, page_phyaddr); 
            // 继续做下一个虚拟地址的映射
            vaddr += PG_SIZE;                            
      }

      return vaddr_start;
}

void *get_kernel_pages(uint32_t pg_cnt)
{
      void *vaddr = malloc_page(PF_KERNEL, pg_cnt);
      if (vaddr != NULL)
      { // 若分配的地址不为空,将页框清0后返回
            memset(vaddr, 0, pg_cnt * PG_SIZE);
      }
      return vaddr;
}

void mem_init()
{
      put_str("mem_init start\n");

      // 用一个数值,构造一个指针,然后取该指针出的地址.
      uint32_t mem_bytes_total = (*(uint32_t *)(0xc0000b00));
      // mem_bytes_total=32*1024*1024;
      pool_init(mem_bytes_total);
      put_str("mem_init done\n");
}

4.4 init.c

加上了内存初始化的代码

#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "memory.h"

/*负责初始化所有模块 */
void init_all() {
   put_str("init_all\n");
   idt_init();   //初始化中断
   mem_init();
}

4.5 main.c

添加了获取一页虚拟地址的测试代码

#include "print.h"
#include "init.h"
#include "debug.h"
#include "interrupt.h"
#include "memory.h"

int main(int argc, char const *argv[])
{
 
    set_cursor(880);    
    put_char('k');
    put_char('e');
    put_char('r');
    put_char('n');
    put_char('e');
    put_char('l');
    put_char('\n');
    put_char('\r');
    put_char('1');
    put_char('2');
    put_char('\b');
    put_char('3');
    put_str("\n put_char\n");

    init_all();
 
    put_str("interrupt on\n");

    void* addr = get_kernel_pages(3);
    put_str("\n get_kernel_page start vaddr is ");
    put_int((uint32_t)addr);
    // intr_enable();
    ASSERT(1==2);
    while (1)
    {
    }

    return 0;
}

4.6 makefile

最后makefile中,添加memroy.c的编译,同时在OBJ中间,加入memory.o让链接器链接


BUILD_DIR=./build
AS=nasm
NASM_ELF=-f elf
INCLUDE=-I./lib -I ./lib/kernel -I./kernel
ASINCLUDE=-I./boot/include/
CFLAGS=-m32 -fno-builtin
LDFLAGS=-Ttext 0xc0001500 -m elf_i386 -e main
CC=gcc

# 注意这里: $(BUILD_DIR)/kernel.o 一定要放在第一个上,因此,这个变量是为连接器准备的.如果不放在第一项,保证错
OBJ=$(BUILD_DIR)/kernel.o $(BUILD_DIR)/print.o $(BUILD_DIR)/idt.o $(BUILD_DIR)/debug.o \
    $(BUILD_DIR)/interrupt.o  $(BUILD_DIR)/init.o $(BUILD_DIR)/memory.o $(BUILD_DIR)/string.o  \
    $(BUILD_DIR)/bitmap.o
    
# 最终要生成的文件
all: $(BUILD_DIR)/kernel.bin $(BUILD_DIR)/loader.bin $(BUILD_DIR)/mbr.bin

# 编译 并刻录 loader.bin
$(BUILD_DIR)/loader.bin: ./boot/loader.asm
    $(AS) -o [email protected] $^ $(ASINCLUDE)
    dd if=$(BUILD_DIR)/loader.bin of=./hd60m.img bs=512 count=4 seek=2 conv=notrunc


# 编译 并刻录 mbr.bin 
$(BUILD_DIR)/mbr.bin: ./boot/mbr.asm
    $(AS) -o [email protected] $^ $(ASINCLUDE)
    dd if=$(BUILD_DIR)/mbr.bin of=./hd60m.img bs=512 count=1 conv=notrunc

# 编译 print.asm
$(BUILD_DIR)/print.o:./lib/kernel/print.asm
    $(AS) $(NASM_ELF) -o [email protected] $^ $(ASINCLUDE) 

# 编译 idt.asm
$(BUILD_DIR)/idt.o:./kernel/idt.asm
    $(AS) $(NASM_ELF) -o [email protected] $^ $(ASINCLUDE) 

# 编译 interrupt.c
$(BUILD_DIR)/interrupt.o:./kernel/interrupt.c 
    $(CC) -o [email protected]  $(CFLAGS) -fno-stack-protector  -c $^ $(INCLUDE) 

# 编译 debug.c
$(BUILD_DIR)/debug.o:./kernel/debug.c
    $(CC) -o [email protected] $(CFLAGS) -c $^ $(INCLUDE)

# 编译 init.c
$(BUILD_DIR)/init.o:./kernel/init.c
    $(CC)  -o [email protected] $(CFLAGS) -c $^ $(INCLUDE) 

# 编译 kernel/memory.c
$(BUILD_DIR)/memory.o:./kernel/memory.c
    $(CC)  -o [email protected] $(CFLAGS) -c $^ $(INCLUDE) 

# 编译 lib/kernel/string.c
$(BUILD_DIR)/string.o:./lib/kernel/string.c
    $(CC)  -o [email protected] $(CFLAGS) -c $^ $(INCLUDE) 

# 编译 lib/kernel/bitmap.c
$(BUILD_DIR)/bitmap.o:./lib/kernel/bitmap.c
    $(CC)  -o [email protected] $(CFLAGS) -c $^ $(INCLUDE) 

# 编译 main.c
$(BUILD_DIR)/kernel.o:./kernel/main.c
    $(CC)  -o [email protected] $(CFLAGS) -c $^ $(INCLUDE) 

# 最终链接
$(BUILD_DIR)/kernel.bin:$(OBJ)
    $(LD) -Ttext 0xc0001500 -m elf_i386 -e main -o ./build/kernel.bin $(OBJ)
    dd if=./build/kernel.bin of=./hd60m.img bs=512 count=40 seek=9 conv=notrunc

clean:
    rm -rf ./build/*
    

4.7运行

timer.c的打印字符串,注释起来

首先make,然后bochs(已经完全不用start.sh了)

技术分享图片

以上是关于第08章下 内存管理系统的主要内容,如果未能解决你的问题,请参考以下文章

第07章下 中断和8253

java基础 第一章下(java格式,基本类型,运算符)

C 中的共享内存代码片段

如何使用模块化代码片段中的LeakCanary检测内存泄漏?

14.VisualVM使用详解15.VisualVM堆查看器使用的内存不足19.class文件--文件结构--魔数20.文件结构--常量池21.文件结构访问标志(2个字节)22.类加载机制概(代码片段

《30天自制操作系统》第9天