起動するまでの長い道のり メモリ管理編(2) メモリ・マップ取得の巻

 ページングのことを書こうかと思ったが、まずマシンに何MBのメモリが搭載されているのか分からないと始まらない。それに、実はメモリ領域にも使えるところと使えないところがある。そういった情報も必要だ。
 今回は、ACPIのファンクションを使ってメモリの領域情報を得る方法について解説する。

今回のソース

メモリ量を得るには

 メモリがいくつ搭載されているか調べるには、またまたBIOSファンクションを使わなければならない。BIOSファンクションを使わないで調べる方法もあるらしいけど私は良く知らない。
 メモリ量を調べるBIOSファンクションにも何種類かある。まずint 12hというファンクションがある。これを使うとKB単位でメモリ量が分かる。ただ、16ビットの数値で返ってくるので最大で64K*1K=64Mバイトまでしか分からない。いまどき64MBなんてOSの最低動作メモリだ。他にもint 15h 88hとかint 15h e801hとか幾つか方法があるようだ。これらも同じようにメモリ容量を返す。
 実は、メモリは搭載されている分端から端まで利用可能というわけではなくて、予約されている領域やROMになっている使えない部分がある。メモリ容量だけではその範囲が分からない。そこで今回は、ACPI(Advanced Configuration and Power Interface)のメモリ・マップ取得用のファンクションを使う。
 ACPIとは本体や周辺機器の電源管理を行ったり、マルチプロセッサの管理を行うためのインターフェイスのことらしい。これの中に含まれているBIOSファンクションe820hを呼ぶと、メモリ・マップというデータ構造が取得できる。メモリ・マップにはメモリのどの部分が使えるか、どの部分が予約されているかが記されている。これを見ればメモリ量は一目瞭然だ。(だがACPI未対応の古いマシンではダメだったりする。まあ1997年以降のPC/ATマシンなら大丈夫だろう。1997年ってもう10年前か……遠い目)

参照:ACPI

ACPIのファンクション

 e820hの仕様は以下の通り。

呼び出し時
レジスタ 内容
EAX e820hを設定。
EBX 最初は0。次回以降はファンクションから返された値。
ES:DI メモリ・マップ格納先のバッファ。
ECX バッファ・サイズ。
EDX 'SMAP'という4バイトの値を設定する決まりになっている。
呼び出し後
レジスタ 内容
CF エラーが起きたら設定される。
EAX ちゃんと実行できた場合'SMAP'になっている。
EBX 次回呼び出しでEBXに指定するべきアドレス(?)値。次がないときは0。
ES:DI 変わらない。
ECX コピーされたバイト数。

 1回の呼び出しでメモリ範囲1個分が取得できる。ループで繰り返し呼び出し、EBXが0になったら終了すれば良い。バッファ・アドレスを進めたりEAXやECXを再設定するのを忘れないように気をつけよう。
 取得できるメモリ・マップの構造については後述。

メモリ・マップの格納先

 さて、このメモリ・マップは呼び出してみるまでエントリがいくつあるか分からない。つまり可変長だ。可変長のデータを取り扱うために可変長のデータが必要だなんて……。実は後に続くページ・テーブルとかでも同様の問題があったりする(汗)。
 今回はとりあえずカーネルの末尾に追記してしまうことにする。まあ結構空いてるから大丈夫だろう。メモリ・マップのエントリが一つ24バイトで、大体5〜6個くらいが相場だと思う。

構造体定義

 メモリ・マップのエントリをD言語の構造体として定義しておくことにする。つまり、メモリ・マップはこういう構造をしているのでよろしく。

/// メモリ・マップ・エントリ。
struct MemoryMapEntry {
    
    /// メモリ範囲の型。
    enum Type : uint32_t {
        MEMORY = 1,     /// 通常のメモリ。
        RESERVED = 2,   /// 予約領域。
        ACPI = 3,       /// ACPI領域。
        NVS = 4,        /// ACPI NVS領域。
        UNUSUABLE = 5,  /// 使えない。
    }
    
    /// メモリ範囲の属性。
    enum Attribute : uint32_t {
        ENABLED = 0b001,        /// 有効。
        NON_VOLATILE = 0b010,   /// 不揮発メモリ。
    }
    
    /// メモリ範囲のベース・アドレス。
    uint64_t base;
    
    /// メモリ範囲の長さ。
    uint64_t length;
    
    /// メモリ範囲の型。
    Type type;
    
    /// メモリ範囲の属性。
    Attribute attribute;
}

/// メモリ・マップ。
extern(C) const MemoryMapEntry[] MEMORY_MAP;

 メモリの型とか属性とか、正直よく分からない。属性とか全部0だったりするし。まあ普通っぽいところだけ利用すればいいだろう。
 最後のメモリ・マップ配列は、アセンブラ命令で初期化する。D言語の関数からこの配列を参照すれば、メモリの情報が得られることになる。

問い合わせの実行

 というわけで、ファンクションを呼び出して実際にメモリ・マップを取得してみよう。setup.s参照。

    #メモリ・マップの取得。
    
    # カーネル末尾以降にメモリ・マップを格納する。
    movl    $_kernel_end, %edi
    
    # 最初は0。
    xorl    %ebx, %ebx
    
    # メモリ・マップ・エントリの取得。
query_memory_map:
    # 引数の設定。
    movl    $query_memory_map_fn, %eax
    movl    $memory_map_size, %ecx
    movl    $query_memory_sign, %edx
    
    # 問い合わせ。
    int     $0x15
    
    # エラーが起きたら終了。
    jc      start_protect_mode
    
    # メモリ・マップ格納先のアドレスを進める。
    addw    $memory_map_size, %di
    
    # 次のエントリがあれば繰り返し。
    orl     %ebx, %ebx
    jnz     query_memory_map
    
    # 終了。
    jmp     start_protect_mode

start_protect_mode:

 因みにこれはリアル・モードの部分に書いてある。なんかちょっと気にならないか。そう、32ビットレジスタを使っているのだ! 実は、リアル・モードでも32ビットのレジスタは使えたりするのだった。今だからこそ明かせる真実。(でも命令にプリフィックスが付いたりしてマシン語的に汚くなるらしい)
 そしてこのメモリ・マップを配列に格納する。というか、配列にアドレスと長さを設定する。

    # メモリ・マップのエントリ数と開始アドレスをD言語の配列に設定。
    
    # エントリ数の計算。
    movl    %edi, %eax
    subl    $_kernel_end, %eax
    xorl    %edx, %edx
    movl    $memory_map_size, %ecx
    divl    %ecx
    
    # 構造体に格納。
    movl    $_MEMORY_MAP, %edi
    movl    %eax, 0(%edi)
    movl    $_kernel_end, 4(%edi)

 これはプロテクト・モードに入ってから行っている。
 D言語の配列は「ポインタ+長さ」という構造体になっているので、その通り設定してやる。

内容の出力

 で、startup.dのstartupでメモリ・マップを出力してやる。

    foreach(i, e; MEMORY_MAP) {
        print("memory ");
        printDec(i);
        print("\r\n");
        
        print("base ");
        printHex(cast(uint) e.base);        
        print(" length ");
        printHex(cast(uint) e.length);
        print("\r\n");
        
        print("type ");
        printDec(e.type);
        print(" attribute ");
        printDec(e.attribute);
        print("\r\n\r\n");
    }

 printとかprintDecとかprintHexとか色々勝手に作ってしまった。実装はコードを見て欲しい。それぞれ文字列・10進整数・16進整数を出力する関数だ。

次回予告

 上記までがうまく動けば、メモリがどのくらい搭載されていてどこまで利用できるのかはっきり分かる。次回は、その搭載メモリ分のページ・テーブルを作ってページングをOnにするところまでやりたい。