<%@ Register tagprefix="pt" tagname="header" src="../header.ascx" %> <%@ Register tagprefix="pt" tagname="footer" src="../footer.ascx" %> <%@ Register tagprefix="pt" tagname="wiki" src="../wiki.ascx" %>

開発環境の構築メモ

ここではVC++を用いてカーネルイメージを得る方法について述べます。必要なものは、

  • Microsoft Visual Studio.NET 2003(のVisual C++)、またはVisual C++ Toolkit 2003
  • Cygwinと、それに含まれるbinutils
  • です。

    ビルド

    コードの準備

    次のコードを kernel.cpp として保存します。このコードは画面上に文字を書き込みます。

    extern void kernel() {
    	char* vram = (char*)0xb8000;
    	vram[0] = 'O';
    	vram[1] = 12;
    	vram[2] = 'K';
    	vram[3] = 12;
    	for(;;) __asm {hlt};
    }

    次のコードを _entrypoint.cpp として保存します。ファイル名の先頭のアンダースコアは、ほかのソースコードファイルを昇順で整列したときに先頭に来るようにするためのもので、もし一つのアンダースコアで足りないときは必要に応じて増やす必要があります。(関数名のアンダースコアは適当でかまいません。) このコードはカーネルのエントリポイントとして機能します。

    extern void kernel();
    
    extern void __declspec(naked) _entrypoint() {
    	kernel();
    	for(;;) __asm {hlt};
    }

    コンパイル

    次のコマンドを実行します。(見栄えの都合により改行しているが、一行として入力してください。) なお、実行にはMSVC++がインストールされている環境で、適切にパスが通っていなければなりません。簡単には、スタートメニューのVC++のところの中に環境変数が自動的に設定されるコマンドプロンプトのショートカットがあると思うので、それを使うと良いでしょう。

    > cl.exe _entrypoint.cpp kernel.cpp /link /OUT:"kernel.dll" /DLL /NODEFAULTLIB /MAP /MAPINFO:EXPORTS
    /OPT:REF /OPT:ICF /NOENTRY /RELEASE /BASE:"0x100000" /NOASSEMBLY /MERGE:".text=.entry" /MACHINE:X86 /FIXED

    オプションについて説明します。

    _entrypoint.cpp kernel.cpp

    ソースファイルのリストには、先頭に _entrypoint.cpp を記述しなければなりません。それ以降は任意の順序でかまいません。VC++のリンカはファイルリストの順に関数を配置するので、先頭のファイルの内容はDLLの先頭に配置されることになります。VS.NETの統合環境はファイル名順にこのリストを渡すようなので、若いファイル名をつけておくことによって位置を制御できます。

    /link

    続くオプションがリンカへのパラメタであることを示します。

    /OUT:"kernel.dll" /DLL /MAP /MAPINFO:EXPORTS /OPT:REF /OPT:ICF /NOENTRY /RELEASE /MACHINE:X86 /NOASSEMBLY

    ここらへんはVC++のヘルプを調べてください。

    /NODEFAULTLIB

    これはVC++の標準ライブラリを無視するために必要な設定です。これを指定しておけば、意図せずライブラリ関数とリンクされてしまったときにリンカエラーとして検出できるようになります。

    /MAP /MAPINFO:EXPORTS /BASE:"0x100000" /MERGE:".text=.entry" /FIXED

    この指定が最も重要です。

    マップ関係の指定は、リンクの結果を確認して、DLLの配置アドレスを調べるために使います。

    /BASEの指定は、DLLの標準配置アドレスを指定します。DLLのなかの機械語は、この位置に展開されたときに動作するように生成されます。ディフォルトでは0x4000000などになっているので、明示的に指定する意味も込めて、0x100000にしておきます。ブートローダはイメージのこのアドレス(付近)にロードすることになります。

    /MARGEによって余分なセクションが結合されます。

    /FIXEDによって再配置情報が破棄されます。

    コンパイル結果の確認

    コンパイルがうまくいくと、kernel.map というファイルが生成されています。その中身をみてみましょう。

     kernel
    
     Timestamp is 435e69ab (Wed Oct 26 02:21:47 2005)
    
     Preferred load address is 00100000
    
     Start         Length     Name                   Class
     0001:00000000 00000005H .entry                  CODE
     0001:00000010 00023a07H .text                   CODE
     0002:00000000 00008861H .rdata                  DATA
     0002:00008861 00000000H .edata                  DATA
     0003:00000000 00000240H .data                   DATA
     0003:00000240 0000141cH .bss                    DATA
    
      Address         Publics by Value              Rva+Base     Lib:Object
    
     0000:00000000       ___safe_se_handler_count   00000000     <absolute>
     0000:00000000       ___safe_se_handler_table   00000000     <absolute>
     0001:00000000       _entrypoint@12             00101000 f   _entrypoint.obj
    

    注目するべきなのは、__safe_se_handler_* というエントリのすぐ次に _entrypoint 関数が配置されていることと、そのRVA (Relative Virtual Address)が0x00101000であることです。

    この後の作業でDLLをstripコマンドで処理しますが、これによってできるイメージファイルは、ファイルの一バイト目が_entrypoint関数の本体になります。ここで、_entrypoint関数は0x00101000番地に配置されたときに動作するようにリンクされたことが上記のマップファイルから読み取れますので、結果的に、stripで処理したファイルを0x00101000に配置すれば動作しそうだと分かるのです。

    イメージの作成

    kernel.dll を Cygwin の strip で処理します。

    $ strip -o kernel.img -O binary kernel.dll

    これによって、PE形式から、DLLがメモリ上のロードされたときの生イメージを得ることができます。この得られたファイルはPE形式などという構造化はされていませんので、使うときには単純に読み込んでメモリにコピーするだけで利用できます。

    イメージの利用

    あとは、上記 kernel.img をブートローダで読み込み、ファイルの先頭が0x00101000になるように配置し、0x00101000番地に制御を移してください。このとき、各種セグメントとスタックポインタなどは予め設定されている必要があります。