30日OS自作入門9日目(Win10)
はじめに
メモリの容量チェックとメモリ管理のはじめをやるようです。
ちょうどOSの講義でメモリを習っているので、楽しみです
C言語のコメントが読みにくかったので改行しました。
もくじ
本文
ソースの整理(harib06a)
キーボードとマウスの処理をHarimainから別のファイルにそれぞれ整理します。
マウス関連の処理はとキーボード関連の処理をそれぞれファイルとして分け、Makefileとbootpack.hを修正します。
メモリ容量チェック(harib06b)
メモリ管理の最初の一歩として、メモリの量を把握しないと管理ができないので把握できるようにします。(BIOSに聞くこともできるが、バージョンの違い・アセンブラを使用することが必要となる)
その際に、メモリの容量を書き込んでから読み込み、同じ値かで判定するプログラムを利用してチェックするのですが、キャッシュがONだとその処理をキャッシュメモリの方で覚えてしまいチェックができなくなります。なので、キャッシュメモリをOFFにするようです。
そのためには、CR0のビットパターンをいじる必要があるので、アセンブリで
_load_cr0: ; void load_cr0(void); MOV EAX,CR0 ; CR0をEAXに格納(読み出し) RET _store_cr0: ; void store_cr0(int cr0); MOV EAX,[ESP+4] ; スタックポインタのデータをEAXに読み出し MOV CR0,EAX ; CR0に戻す RET
これで、CR0にアクセスできるので
#define EFLAGS_AC_BIT 0x00040000 #define CR0_CACHE_DISABLE 0x60000000 unsigned int memtest(unsigned int start, unsigned int end) { char flg486 = 0; /* 486かのフラグをオフとして定義 */ unsigned int eflg, cr0, i; /* eflg:EFLAGのビットパターン格納用, cr0:cr0のビットパターン格納用, i:メモリの容量用 */ /* 386か、486以降なのかの確認(EFLAGの18ビットの判定) */ eflg = io_load_eflags(); /* EFLAGの内容を変数eflgに格納 */ eflg |= EFLAGS_AC_BIT; /* AC-bit = 1(ACフラグをON) */ io_store_eflags(eflg); /* EFLAGに読み込み */ eflg = io_load_eflags(); /* 再度読み出し */ if ((eflg & EFLAGS_AC_BIT) != 0) { /* 386ではAC=1にしても自動で0に戻ってしまう(386か486の判定) */ flg486 = 1; /* 486であると判定しフラグをON */ } eflg &= ~EFLAGS_AC_BIT; /* AC-bit = 0(AND演算子とビット反転を使って全ビットを0にリセット) */ io_store_eflags(eflg); /* EFLAGに読み込み */ if (flg486 != 0) { /* もし486なら */ cr0 = load_cr0(); /* cr0の現在のビットパターンをロードして */ cr0 |= CR0_CACHE_DISABLE; /* キャッシュの許可をセット */ store_cr0(cr0); /* cr0にストア(というより戻す?)する */ } i = memtest_sub(start, end); /* メモリ容量の測定 */ if (flg486 != 0) { /* もし486なら */ cr0 = load_cr0(); /* cr0のビットパターンを読み出し */ cr0 &= ~CR0_CACHE_DISABLE; /* キャッシュの許可をセット */ store_cr0(cr0); /* CR0に戻す */ } return i; /* メモリの容量を返す */ }
要点は
・486以降にキャッシュメモリは搭載されている
・EFLAGレジスタのACフラグで判別可能
・キャッシュメモリの停止にはCR0レジスタの29ビット目と30ビット目を立てる
次は、メモリの容量を数えるプログラムみたいで
/* アドレスstartからendまででのメモリ容量の判定 */ unsigned int memtest_sub(unsigned int start, unsigned int end) { unsigned int i, *p, old, pat0 = 0xaa55aa55, pat1 = 0x55aa55aa; for (i = start; i <= end; i += 0x1000) { p = (unsigned int *) (i + 0xffc); old = *p; /* いじる前の値を覚えておく */ *p = pat0; /* 試しに書いてみる */ *p ^= 0xffffffff; /* そしてそれをメモリ上で反転してみる */ if (*p != pat1) { /* 反転結果になってなければ */ not_memory: *p = old; /* データを元に戻す */ break; /* ブレイク */ } *p ^= 0xffffffff; /* もう一度反転してみる */ if (*p != pat0) { /* 元に戻ったか? */ goto not_memory; /* 指定ラベルに飛ぶ */ } *p = old; /* いじった値を元に戻す */ } return i; /* 測定した値を返す */ }
これはメモリにデータをロードして、メモリ上と一致しているかで判定しているみたいです。
・反転する理由は、一部のアーキテクチャだと読めていしまうことがあるかららしいです。
そして最大3GBまで判定できるようにして、測定すると
出てます!でも、著者はツールセットを32MBにセットしているので多すぎる…?
メモリチェック(2)(harib06c)
なんで測定値が違うのだろう…といった章になるみたいです。書籍を読んだら細かく書いてありますが、コンパイラの最適化が悪さをしていたみたですね…
bootpack.nasを生成してみてみると
_memtest_sub: PUSH EBP ; Cコンパイラでの決まり文句 MOV EBP,ESP ; これも決まり文句 MOV EDX,DWORD [12+EBP] ; EAX = end;(EDXに変数endを格納) MOV EAX,DWORD [8+EBP] ; EAX = start; /* EAXはi */(EAXに変数) CMP EAX,EDX ; if (EAX > EDX) { ( 比較して ) JA L30 ; goto L30; ( ジャンプ ) L36: L34: ADD EAX,4096 ; EAX += 0x1000; CMP EAX,EDX ; JBE L36 ; L30: POP EBP ; PUSHしておいたEBPの受け取る RET ; return;
XORがないですね…
これは、コンパイラちゃんが、それぞれの変数の計算結果ってどうせ同じだし、早くするために消しちゃお!って感じで消しているみたいですね。
コンパイラちゃん天才すぎ!
ということでコンパイラちゃんの目を欺くために、アセンブリでmemtest_subの処理を書くみたいです。
_memtest_sub: ; unsigned int memtest_sub(unsigned int start, unsigned int end) PUSH EDI ; (EBXm, ESI, EDIも使いたいので) PUSH ESI PUSH EBX MOV ESI,0xaa55aa55 ; pat0 = 0xaa55aa55; MOV EDI,0x55aa55aa ; pat1 = 0x55aa55aa; MOV EAX,[ESP+12+4] ; i = start; mts_loop: MOV EBX,EAX ADD EBX,0xffc ; p = i + 0xffc; MOV EDX,[EBX] ; old = *p; /* いじる前の値を覚えておく */ MOV [EBX],ESI ; *p = pat0; /* 試しに書いてみる */ XOR DWORD [EBX],0xffffffff ; *p ^= 0xffffffff /* そしてそれをメモリ上で反転してみる */ CMP EDI,[EBX] ; if (*p != pat1) goto fin; /* 反転結果になってなければ */ JNE mts_fin ; break XOR DWORD [EBX],0xffffffff ; *p ^= 0xffffffff /* もう一度反転してみる */ CMP ESI,[EBX] ; if (*p != pat0) goto fin; /* 元に戻ったか? */ JNE mts_fin MOV [EBX],EDX ; *p = old /* いじった値を元に戻す */ ADD EAX,0x1000 ; i += 0x1000; /* forの開始カウンタ */ CMP EAX,[ESP+12+8] ; if (i <= end) goto mts_loop; /* カウンタの上限定義 */ JBE mts_loop POP EBX POP ESI POP EDI RET mts_fin: MOV [EBX],EDX ; *p = old; /* いじった値を元に戻す */ POP EBX POP ESI POP EDI RET
このコードでは、for文の初期位置と最後の判定の仕方が、とても面白かったです。
これでCコンパイラちゃんの最適化の目をあざむけたので、実行すると
ちゃんと認識してる
やっと32MBとでました…
メモリ管理に挑戦(harib06d)
ここは5日目並みに難関でした。理解するならソースコードを読むしかないと思います。
大事なのは、物理メモリ上を区画に分けて管理するイメージをするのではなくて、各種情報のエクセルのような表を作っているイメージで行くことが大事だと思います。(自分は頭が固いせいか気づくのに時間がかかってしまった。)
やることは、メモリの管理(どこが使用できて、どこが使用できないか)を構造体を駆使して、管理することです。
まず、構造体の構成が以下のようになっています。
/* Freeのアドレスとサイズ管理 */ struct FREEINFO { /* あき情報 */ unsigned int addr, size; /* 各種、addr:アドレス, size:大きさ */ }; /* 失ったサイズやどれだけFreeかの管理 */ struct MEMMAN { /* メモリ管理 */ int frees, maxfrees, lostsize, losts; /* 関数memman_initを参照 */ struct FREEINFO free[MEMMAN_FREES]; /* 配列で表にする */ };
構造体MEMANは、メモリ全体を管理する情報を扱います。
メンバ | 内容 |
---|---|
frees | 重要なメンバで、空き要素の個数を管理 |
maxfrees | 状況観察用、現在どの程度メモリを使えるか |
lostsize | 解放に失敗したメモリの合計サイズ |
losts | 解放に失敗したメモリが何個あるかを管理 |
FREEINFO free[] | それぞれの空き情報(配列で管理) |
構造体FREEINFOで、区切ったそれぞれの空き情報の管理を行います。
メンバ | 内容 |
---|---|
addr | どこのアドレスから空きがあるか |
size | そのアドレスからどれだけ空いているか(バイト) |
といった感じに管理がされています。
各種関数は以下の感じ
関数名 | 処理内容 |
---|---|
memman_init | 全体としての管理する情報を初期化(管理する配列は最初は0なので) |
memman_total | 登録された構造体FREEINFOの空き情報sizeをすべて繰り返し処理で加算 |
memman_alloc | 空き情報を上から探索していって、引数として渡した大きさとマッチすれば確保(各種データの調整しアドレスを返す) |
memfree | 解放するメモリに近いアドレスを持つフィールドを探します。その後、前に詰めます。その時後ろも詰めれそうであれば詰めちゃいます。 また、後ろにしか詰めれない場合も詰めます。 もし、表を埋め尽くして詰めれなかった場合は、表のところどころの穴(何も入ってないけど管理されてるやつ)を埋めることで枠を作ります。それもできなかったとき、ロストを増やす処理を行う感じです。 |
実行すると
できた!成功!
最後に
メモリ管理の内容が濃かったですが、ノートとかに図としてまとめるとわかりやすかったですね。やっと2桁の日にちに行けます