第08章下 内存管理系统
Posted perfy576
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第08章下 内存管理系统相关的知识,希望对你有一定的参考价值。
程序是指令的集合,程序要运行,必须将其加载早内存中。这就是cpu的cs:ip寄存器是在内存中取指令的原因。
1 内存管理的思路
内核和用户程序分别在自己的地址空间中运行,在实模式下,程序中的地址就等于物理地址,在保护模式下,程序中的地址编程了虚拟地址,虚拟地址对应的物理地址需要经过分页机制的映射。
内存池是将所有可用的内存集中放在一个池子中管理的方式。在分页机制下,因为有虚拟地址和物理地址,所以需要创建管理虚拟地址和物理地址的内存池。内存池中的内存按照页来管理。
为了内核的正常运行(因为我们内核运行在物理内存的低1M中,已经可以正常运行,而这里的运行值得是内核去管理或者位进程提供其他功能,提供这些功能,就需要额外的内存),因此,物理内存中固定一部分分配给内核,该部分的内存不能被用户程序使用,另一部分给用户程序。因此将物理内存分成两部分,一部分内核使用,一部分用户态使用。
首先,要确认的是我们内存管理是要管理还没被使用的内存,意思就是管理那些,到进入内核运行之前还没有被使用内存.而在进入内核之前已经被使用的内存有:
- 物理内存的低1MB,是被用来作为内核运行的内存了
- 1M向上,紧接着,分别是内核的页表目录,和内核虚拟地址高1G的页表.其中页表目录最后一项指向了自己,所以一共255个页表 ,加上一个页表,所以一共256个页用来构建内核的页表目录和页表了.
所以总共者两部分,一共1M+2K*256的物理内存使用.
然后剩下的物理内存一分为二:
- 低位的物理内存给内核使用,因为我们在创建进程和线程的时候要分配结构去管理进程和线程,这也是需要内存的,还有其他的情况,内核在申请内存的会后就从该部分去申请.
- 高位的物理内存给用户态使用,当用户态需要申请内存的时候,内核就从该部分去申请物理内存.
然后,物理内存的管理我们使用位图,但是物理内存不同与虚拟内存,物理内存有大小,还有其实分配的物理地址.因此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空间里的,且由我们指定位置.
管理用户程序的位图结构,是指向由内核经过内存分配获取的内存.
然后,考虑一下,分配内存的过程(不考虑内存交换的问题,因为到目前我们还能有效的操纵磁盘):
- 首先程序申请内存,
- 然后,内核去管理虚拟内存的结构中,找一块虚拟内存,取得要返回的该虚拟内存的地址,因为当程序用光了他所有可用的虚拟内存以后,就没有办法再使用内存的.
- 接着,对应的内核内存池或是用户态内存池中寻找一块物理内存,获得物理内存的物理地址.那第2步取的虚拟地址最后经过页部件的转换就会对应到该块物理内存上.
- 最终的需要,去修改该程序的页表目录,因为程序在使用新的虚拟地址的时候,需要经过页部件,转换为物理地址访问.因此需要在程序的页表目录中建立虚拟地址和物理地址的映射.
- 最后,返回虚拟地址.
过程还是很复杂的.其中去修改页表可能是最麻烦的.
下面一步步来.
2 实现:内存管理
2.1 结构体定义
Bitmap
用来管理每一个4K的内存(虚拟内存和物理内存).其中bits
的指向为:
- 内核管理虚拟内存和内核物理内存池,用户空间物理内存池时,指向的是在内核运行的低1M空间中指定的3块区域.
- 管理用户虚拟内存时,是内核从内核物理内存池申请的一页,指向的是这一页中的某个部分
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; // 本内存池字节容量
};
然后,内核当前需要管理的是:
- 用户空间物理内存池
- 内核空间物理内存池
- 内和空间虚拟地址空间(或是内存池)
因此,有如下三个变量:
// 内核用于管理所有物理内存的两个内存池
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()
函数
其过程就是:
- 首先计算进入内核前已经使用了多少物理内存,这部分物理内存是从物理内存
0x0
开始使用的.是认为安排的 - 然后计算还未使用的物理内存,能够分成多少页.内核态和用户态各一半
- 然后开始填充
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);
}
然后考虑,这个函数的参数,这个函数的传参比较特殊.首先参数是计算机的物理内存,而这个物理内存需要用汇编获取,然后之前我么你在实模式下已经用中断获取过一次了.进入实模式下,这些中断就不能用了,因为他们是实模式下的中断,所以只能在实模式下获取.然后在打开分页机制下使用.因此,我们需要在实模式获取到物理内存大小的时候,记录的地址,需要我们知道,有这么几种方式
- 不改变原先的代码,直接计算出先前计算物理内存容量的.然后在调用的时候,先去构造指针,然后解引用.
- 改变原有的代码,直接安排记录物理内存容量的那个内存空间在指定的位置
总之,就是需要通过直接知道内存空间的形式传参.
这里选择直接改变代码:
先看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章下 内存管理系统的主要内容,如果未能解决你的问题,请参考以下文章
如何使用模块化代码片段中的LeakCanary检测内存泄漏?
14.VisualVM使用详解15.VisualVM堆查看器使用的内存不足19.class文件--文件结构--魔数20.文件结构--常量池21.文件结构访问标志(2个字节)22.类加载机制概(代码片段