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まで判定できるようにして、測定すると
f:id:No000:20191208155308p:plain
出てます!でも、著者はツールセットを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コンパイラちゃんの最適化の目をあざむけたので、実行すると
f:id:No000:20191208173719p:plain ちゃんと認識してる
やっと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 解放するメモリに近いアドレスを持つフィールドを探します。その後、前に詰めます。その時後ろも詰めれそうであれば詰めちゃいます。 また、後ろにしか詰めれない場合も詰めます。 もし、表を埋め尽くして詰めれなかった場合は、表のところどころの穴(何も入ってないけど管理されてるやつ)を埋めることで枠を作ります。それもできなかったとき、ロストを増やす処理を行う感じです。

実行すると
f:id:No000:20191213200959p:plain できた!成功!

最後に

メモリ管理の内容が濃かったですが、ノートとかに図としてまとめるとわかりやすかったですね。やっと2桁の日にちに行けます