今回はいよいよOSでメモリ管理を行っていく。
※過去回の記事とソースコードは下記から入手できる
https://github.com/Shadow5523/osdev/blob/master/README.md
また、ここで使用するコードはGitHubからダウンロードできる。
https://github.com/Shadow5523/osdev/releases/tag/version0.10.0
目次
概要
いよいよメモリ管理をしなくちゃいけないところまで来てしまったので実装を行う。今回は物理メモリを4KB単位で割当、開放する方法と仮想メモリ管理の準備としてcr3レジスタにページディレクトリのベースアドレスを渡し、cr0レジスタのページングビットを1にして、ページングを有効化してみる。
物理アドレス管理では4KBを一つのメモリブロックとし、その管理状況をビットマップにして、メモリブロックに対応するビットを1にしたり0にしたりすることで割当済メモリ/空きメモリの使用状況を保持する。このビットマップはカーネルイメージのすぐ後ろに置かれることが多く、今回もそれにならってビットマップをカーネルイメージの後ろへ配置する。
物理メモリ管理が4KB単位での管理なのは、次回実装する仮想メモリ管理において詳しく解説しようと思うが、ページング方式の実装において、ページのサイズ、ページを表すPTEをまとめたページテーブルのサイズ、ページテーブルをまとめたPDEのサイズが4KB(4MBであることも)であるためで、そのほうが管理しやすいからである。
今回行うことの流れとしては。。。
の流れで実装していこうと思う。
カーネルイメージのサイズを算出
1. 現在のカーネルイメージのサイズを算出するため、linker.ldを以下のように編集する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
ENTRY(_start) SECTIONS{ . = 1M; .text BLOCK(4K) : ALIGN(4K){ __kernel_start = .; //追加(カーネルイメージの始まり位置) *(.multiboot) *(.text) } .rodata BLOCK(4K) : ALIGN(4K){ *(.rodata) } .data BLOCK(4K) : ALIGN(4K){ *(.data) } .bss BLOCK(4K) : ALIGN(4K){ *(COMMON) *(.bss) __kernel_end = .; //追加(カーネルイメージの終わり位置) } } |
「__kernel_start」と「__kernel_end」に代入している”.”は現在のアドレスの位置を表す特殊な表記で、これでカーネルイメージが始めるアドレスと終わるアドレスを保持することができる。
2.「getmmap.h」「getmmap.c」を改造し、返り値に全メモリサイズを返すようにする。まずはヘッダファイルから変更していく。
1 2 3 4 5 6 7 8 |
#ifndef _GETMMAP_H_ #define _GETMMAP_H_ #include "multiboot.h" uint32_t getmmap(multiboot_info_t*); //変更 #endif _GETMMAP_H_ |
3.「getmmap.c」を以下のように変更し、メモリ全体のサイズを取得し、その値を返すように設定する。
1 2 3 4 5 6 7 8 |
・・・・ total_mem_size = (mbt -> mem_lower + mbt -> mem_upper + 1024) / 1024; sh_printf("Total memory %dMB\n", total_mem_size); sh_printf("===================================================\n\n"); return total_mem_size; } |
4.カーネルイメージの大きさを取得するプログラムを書く。まずヘッダファイル「get_ksize.h」を以下のように作成する。
1 2 3 4 5 6 7 8 9 10 11 |
#ifndef _GET_KSIZE_H_ #define _GET_KSIZE_H_ #include <stdint.h> extern uint32_t __kernel_start; extern uint32_t __kernel_end; uint32_t get_ksize(void); #endif _GET_KSIZE_H_ |
「linker.ld」で定義したシンボルをCのソースファイルで使用する場合でも、extern宣言を行えば問題なく使用することができる。「uint32_t get_ksize(void)」は実際にサイズを算出する関数のプロトタイプで、サイズをuint32_tで返す。
5.次にカーネルイメージのサイズを算出するプログラム本体を書いていく。「get_ksize.c」を以下のように作成する。
1 2 3 4 5 |
#include "../include/get_ksize.h" uint32_t get_ksize(void){ return &__kernel_end - &__kernel_start; } |
単純に、カーネルイメージ終わりアドレスから、始まりのアドレスを引いて、その差を返すようになっている。
物理メモリを初期化
1.物理メモリの初期化を行う。ヘッダーファイル「init_pmemory.h」を以下のように作成する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#ifndef _INIT_PMEMORY_H_ #define _INIT_PMEMORY_H_ #include <stdint.h> #include <stddef.h> #include "multiboot.h" #include "get_ksize.h" #include "../sh_libc/include/stdio.h" #include "../sh_libc/include/string.h" typedef struct{ uint32_t system_msize; //システムメモリサイズ uint32_t system_mbloks; //システムメモリブロック数 uint32_t allocated_blocks; //割り当て済のブロック数 uint32_t free_blocks; //フリーブロック数 uint32_t* mmap; //実際にビットマップが配置されてる場所へのポインタ uint32_t mmap_size; //ビットマップのサイズ }p_memory_info; p_memory_info pm_info; void get_system_mblocks(uint32_t); void setmemory(int); void clearmemory(int); void pbitmap_free(uint32_t, uint32_t); void pbitmap_alloc(uint32_t, uint32_t); #endif _INIT_PMEMORY_H_ |
p_memory_infoは実際に物理メモリを管理するビットマップのステータスを保持する構造体となっており、メモリの空きブロック数や割り当て済ブロック数、ビットマップへのポインタが格納されている。
また、この構造体の構造は下記サイトを参考にして作成した。
http://softwaretechnique.jp/OS_Development/kernel_development06.html
2.「memory/init_pmemory.c」を作成し、関数「get_system_mblocks()」を作る。
1 2 3 4 5 6 7 8 9 10 11 |
#include "../include/init_pmemory.h" void get_system_mblocks(uint32_t msize){ pm_info.system_msize = msize; pm_info.system_mbloks = msize / 4096; pm_info.allocated_blocks = pm_info.system_mbloks; pm_info.free_blocks = 0; pm_info.mmap = &__kernel_end; pm_info.mmap_size = pm_info.system_mbloks / sizeof(uint32_t) * 8; sh_memset((void *)pm_info.mmap, 0xff, pm_info.mmap_size); } |
ここではメモリ管理を行うビットマップの初期化を行う。この関数で受け取る「msize」は「getmmap()」で取得したメモリサイズを格納する。「allocated_blocks」メンバは割り当て済のメモリブロック数を、「free_blocks」は空きメモリブロック数を格納しており、この初期化時はすべてのメモリを割り当て済にし、空きメモリブロック数は0に設定している。「mmap」メンバはビットマップへのポインタであり、カーネルイメージのすぐ後ろを指すようにし、「mmap_size」メンバにはそのビットマップのサイズを格納している。
ビットマップでは4kb毎のメモリブロックの使用状況が格納されており、0であれば空き、1であれば割り当て済となる。
そして最後にビットマップのすべてのフィールドに1をセットし初期化は完了。
3.次にビットマップに割り当てられているメモリブロックに対応するビットを0にしたり1にしたりする関数を作成する。
1 2 3 4 5 6 7 8 |
void setmemory(int bnum){ pm_info.mmap[bnum / 32] |= (1 << (bnum % 32)); } void clearmemory(int bnum){ pm_info.mmap[bnum / 32] &= ~(1 << (bnum % 32)); } |
「setmemory()」では割り当てたいメモリブロックのビットを特定し、or演算で元のビット構成を崩さないように該当ビットに1をセットする。同様に「clearmemory()」では開放したいメモリブロックに対応するビットに0をセットする。
メモリブロックに対応するビットの算出の仕方は以下の通り
1 << (ビット番号 % 32)
32で剰余算してるのは、ビットマップの範囲外に書き込まないようにするため。
4.次に、メモリ初期化時にGRUBで取得したメモリマップをもとにして使用していいブロックとそうでないブロックを初期化する為に以下の関数を作成する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
void pbitmap_free(uint32_t address, uint32_t size){ address /= 4096; size /= 4096; for(size_t i = address; i <= size; i++){ clearmemory(address); address++; pm_info.allocated_blocks--; pm_info.free_blocks++; } } void pbitmap_alloc(uint32_t address, uint32_t size){ address /= 4096; size = (size / 4096) == 0 ? 1 : size / 4096; for(size_t i = address; i <= size; i++){ setmemory(address); address++; pm_info.allocated_blocks++; pm_info.free_blocks--; } } |
「pbitmap_free()」ではメモリブロックの割り当てを、「pbitmap_alloc()」では開放を行っている。引数で受け取るアドレスは取得したいメモリ領域の開始アドレスであり、もうひとつの引数のsizeは取得したいメモリのサイズが入っている。メモリブロックが4kb単位となっているので、アドレス/サイズともに4096で割って対応するメモリブロックへと変換する。その後、「setmemory()」「clearmemory()」を呼び出し、pm_infoのメンバの割り当て済ブロック数/開放済ブロック数を増減させ、割り当て/開放をforループで割り当てたいブロックぶんだけ繰り返す。
5.物理メモリを初期化させるパーツが揃ったので、それらを呼び出し初期化を行う関数を作成する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
void init_pmemory(multiboot_info_t *mbt, uint32_t total_msize){ uint32_t send_addr; uint32_t send_length; multiboot_memory_t* mmap = mbt -> mmap_addr; get_system_mblocks(total_msize * 1024 * 1024); for (mmap; mmap < (mbt -> mmap_addr + mbt -> mmap_length); mmap++) { send_addr = (mmap -> base_addr_high << 8) | mmap -> base_addr_low; send_length = (mmap -> length_high << 8 ) | mmap -> length_low; if(mmap -> type == 0x1 || mmap -> type == 0x3) { if (send_addr == &__kernel_start){ pbitmap_alloc(send_addr, get_ksize() + pm_info.mmap_size); pbitmap_free(&__kernel_end, send_length - (get_ksize() + pm_info.mmap_size)); } else { pbitmap_free(send_addr, send_length); } } else { pbitmap_alloc(send_addr, send_length); } } } |
この関数では、GRUBで取得したmultiboot_info_t構造体とメモリの総量のサイズを引数に取る。
multiboot_info_t構造体のポインタは現在のメモリマップの状態を指しており、これをもとにアドレス領域毎にメモリの割り当て/開放を行っていく。メモリタイプが「0x1」か「0x3」は使用してもOKなメモリ領域なので「pbitmap_free()」を呼び出して開放し、それ以外は「pbitmap_alloc()」を呼び出して割り当て済とする。
またここでは、カーネルイメージが配置されているメモリ領域も使用可能メモリ領域として認識してしまうため、メモリの開始領域(send_addr)がカーネルイメージの開始位置と同じ(&__kernel_start)なら、カーネルイメージのサイズ分 + メモリマップのサイズ(メモリマップはカーネルの直後に配置されるように設定したため)だけメモリを割り当て済とし、それ以外を開放するようにした。
4kb単位でメモリの確保/開放する関数を作成
1.新規にヘッダファイル「pmalloc.h」を以下のように作成する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#ifndef _PMALLOC_H_ #define _PMALLOC_H_ #include <stdint.h> #include <stddef.h> #include "init_pmemory.h" #include "../sh_libc/include/stdio.h" extern p_memory_info pm_info; int findfreememory(unsigned int*); void* malloc4kb(void); void free4kb(void*); #endif _PMALLOC_H_ |
物理メモリの情報を格納する構造体「pm_info」は外部で宣言、初期化されてるのでextern宣言する。
2.次に「memory/pmalloc.c」を作成する。まずは割り当てたいメモリブロックがすでに割当られてないか確認する関数「findfreememory()」を作成する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include "../include/pmalloc.h" int findfreememory(unsigned int *bnum){ size_t b_count; for (size_t b_index = 0; b_index < pm_info.mmap_size; b_index++) { if (pm_info.mmap[b_index] != 0xFFFFFFFF) { for (b_count = 0; b_count < 32; b_count++) { if (!(pm_info.mmap[b_index] & (1 << (b_count % 32)))) { *bnum = b_index * sizeof(unsigned int) * 8 + b_count; return 0; } } } } return -1; } |
最初は、for文で32ビットずつ大雑把に検索をかけ、一つでもビットマップにゼロがあったら、対象の32ビットを1ビットずつ検索し、空いているメモリブロックを検索する。無事空いていたら引数で受け取ったポインタに対象のメモリアドレスを格納し1を返す。割当に失敗したら、-1を返すようにする。
3.4kbのメモリを確保する「malloc4kb()」を作成する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
void* malloc4kb(void){ unsigned int b_number; void* p_address; int status; if (pm_info.free_blocks <= 0) { return NULL; } if (findfreememory(&b_number)) { return NULL; } setmemory(b_number); p_address = (void *)(b_number * 4096); pm_info.allocated_blocks++; pm_info.free_blocks--; return p_address; } |
この関数は呼び出されると、まず空きブロックがすでに無いか確認し、なければNULLを返す。もし、空きブロックがあれば、先程の「findfreememory()」で空きメモリブロックの位置を探し出し、そのメモリブロックが割り当てられているbitにフラグを立てて、割当済とする。最後にpm_info構想体の割当済メモリブロック数、空きブロック数を変更し、割り当てた物理メモリを返す。
4.次にメモリを4kb単位で開放する関数「free4kb()」を作成する。
1 2 3 4 5 6 7 8 9 |
void free4kb(void* p_address){ unsigned int b_number; b_number = (unsigned int)p_address / 4096; clearmemory(b_number); pm_info.allocated_blocks--; pm_info.free_blocks++; } |
この関数は、受け取った物理メモリを4096で割ってメモリブロックを特定し、「clearmemory()」で割り当てたフラグを取り除きメモリを開放する。最後に「malloc4kb()」と同様にpm_info構造体の割当済/空きメモリブロック数を更新し、処理を終了する。
ここまでで、物理メモリの管理が完了した。次にページングの有効化を行う。
ページング有効化
1.まずはヘッダーファイル「include/init_vmemory.h」を以下のように作成する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#ifndef _INIT_VMEMORY_H_ #define _INIT_VMEMORY_H_ #include <stdint.h> #include <stddef.h> #include "init_pmemory.h" #include "pmalloc.h" #define PE_PFLAG 0x00000001 #define PE_RWFLAG 0x00000002 //pte & pdeの形を定義 typedef uint32_t page_table_entry; typedef uint32_t page_dir_entry; extern p_memory_info pm_info; extern void enable_paging(page_dir_entry *); int init_paging(void); int init_vmemory(void); #endif _INIT_PMEMORY_H_ |
定数で宣言している「PE_PFLAG」と「PE_RWFLAG」はPTE/PDEで使用するフラグ。
下位1ビット目がPフラグと呼ばれており、ページ、またはページディレクトリが物理メモリにロードされているかを設定する。1であれば物理メモリ上に存在しているが、0であると物理メモリ上にないとCPUは判断してしまい、ページフォールト例外が発生する。
下位2ビット名がR/Wフラグを呼ばれており、ページやページディレクトリに読み取り/書き込み属性を設定する。1がセットされている場合は読み書きができ、0であれば読み込み専用となる。
2.ページングを有効にする関数をGASで記述する。「paging.s 」を作成する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
.text .global enable_paging enable_paging: push %ebp mov %esp, %ebp //writing cr3 mov 8(%esp), %eax mov %eax, %cr3 //writing cr0 mov %cr0, %eax or $0x80000000, %eax mov %eax, %cr0 mov %ebp, %esp pop %ebp ret |
ここでは、CR3レジスタにページディレクトリのベースアドレスを書き込み、その後、cr0レジスタをeaxレジスタと0x80000000とを、OR演算した値をcr0に書き込むことにおよってページングが有効になる。
3.ページディレクトリ, ページテーブルを初期化する関数「init_paging(void)」を作成する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include "../include/init_vmemory.h" int init_paging(void){ page_dir_entry* page_directory = (page_dir_entry*)malloc4kb(); page_table_entry* page_table = (page_table_entry*)malloc4kb(); for(size_t i = 0, address = 0x0; i < 1024; i++, address += 0x1000){ page_directory[i] = PE_RWFLAG; page_table[i] = address | (PE_RWFLAG | PE_PFLAG); } page_directory[0] = (page_dir_entry)page_table | (PE_RWFLAG | PE_PFLAG); enable_paging(page_directory); return 0; } |
先程作成した「malloc4kb()」でページディレクトリ、ページテーブル用のメモリを確保し、初期化を行う。
その後、ページディレクトリの要素には読み書き可能にするためにフラグをセットし、ページテーブルには仮想アドレスにPフラグ、RWフラグを加えたものをセットする。もし空きメモリブロックがゼロであればRWフラグは読み取りでフラグを立てるようにした。
次にページディレクトリの先頭要素に、ページテーブルの先頭アドレスと、Pフラグ、RWフラグをセットし「enable_paging」にページディレクトリの先頭アドレスを送りページングを有効化している。
4.「init_paging()」を呼び出し、正常に仮想メモリが初期化できたか調べる。
1 2 3 4 5 |
int init_vmemory(void){ if(!init_paging()){ sh_printf("Initialize paging init... OK!\n"); } } |
5.そして「kernel.c」「kernel.h」を以下のように修正し、起動時に物理メモリが初期化されるようにする。また、今回追加したファイルを「Makefile」へ記述しコンパイル/アセンブルされるようにする。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
.... #include "terminal.h" #include "keyboard.h" #include "inb_outb.h" #include "gdt.h" #include "pic.h" #include "idt.h" #include "multiboot.h" #include "getmmap.h" #include "get_ksize.h" //追加 #include "init_pmemory.h" //追加 #include "init_vmemory.h" //追加 #include "pmalloc.h" //追加 .... |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
void kernel_main(multiboot_info_t* mbt, uint32_t magic){ terminal_initialize(); //memory init_pmemory(mbt, getmmap(mbt)); //追加 sh_printf("physical memory init... OK!\n"); //追加 init_vmemory(); //追加 sh_printf("virtual memory init... OK!\n"); //追加 gdt_init(); pic_init(); idt_init(); key_init(); sh_printf("Hello, kernel World! \n\n"); //cr0レジスタのフラグを確認するために以下を追加する uint32_t cr0_data; asm volatile( "pop %%eax \n" "mov %%cr0, %%eax \n" : "=a"(cr0_data) : : ); sh_printf("cr0 registar=0x%x\n", cr0_data); prompt(); } ・・・・ |
動作確認
VMWareなどで実行してみて、CR0レジスタの値が0x8000〜になってたらページングは有効になっている。
長くはなったけど、今日はここまで。。。
次回は仮想メモリと物理メモリのマッピングを行う。