このコードはKERNEL.IMGというカーネルファイルの先頭の部分にあります。 ブートローダーはファイルシステムからこのファイルをメモリの0x0100:0x0000、つまり 0x01000番地に展開して実行します。そして、A20ラインを有効にし、ルートディレクトリから MONA.CFGを読み取ってディスプレイの設定を行い、プロテクトモードに移行してcstart.cppの cstart()を呼び出して、C++で書かれたコードに移行します。
ax | 0x0000 |
ds | 0x0000 |
es | 0x0000 |
ss | 0x0000 |
sp | 0x1000 |
0x00000 0x01000 ------------------------------------------------------------------------- stack domain ← | kernel code domain | | | -------------------------------------------------------------------------
ここではA20ラインを有効にしています。A20ラインというのはアドレスの第20番ラインの ことで、デフォルトでは常に0を出力するようになっています。これをCPUの出力どおりに 出力するようにするための設定が次のコードです。このことをA20ラインを有効にする といいます。これをしないと32ビットアドレス空間をうまく使うことができません。
A20ラインの設定はキーボードコントローラーを介して行います。具体的にはコントローラーに 0xDFというコマンドを送るのですが、そのための手続きが煩雑になっています。
まず、目的の0xDFという値を送るために0x64ポートに0xD1という値を書き込みます。 この値を書き込む前に0x64ポートを読み込んでビット1のビットが1になっていることを 確認します。もし、ビットが0だったら1になるまで待たなければなりません。このように、 同じポートアドレスでも読み込みと書き込みの意味が違っていたり、あるビットがオンに なるまで待たなければならない、ということはデバイスを扱うプログラムでは よくあることなので慣れてください。
[bits 16] cli a20enable: in al, 0x64 test al, 0x02 jnz a20enable mov al, 0xD1 out 0x64, al
0xD1というコードを書き込んだら次に0x60ポートに0xDFというコードを書き込みます。 これでA20ラインが有効になります。書き込む前に0x64のビット1のビットが立つまで 待たなければならないことは先ほどと同じです。
a20enable_1: in al, 0x64 test al, 0x02 jnz a20enable_1 ;wait every KBC cmd. mov al, 0xDF out 0x60, al
A20ラインを有効にすれば最後にキーボードコントローラーに対して出力を 6マイクロ秒Lowにします。なぜこのようなことをするのかはわかりませんが おそらく必要な処理なのでしょう。
a20enable_2: in al, 0x64 test al, 0x02 jnz a20enable_2 mov al, 0xFF out 0x64, al a20enable_3: in al, 0x64 test al, 0x02 jnz a20enable_3
A20ラインを設定し終えたら次はディスプレイを設定します。この設定は ルートディレクトリのMONA.CFGというファイルを読み込んで行います。
思い出してください。firstboot.asmでルートディレクトリエントリを0x90000に 読み込みました。ここではそこに読み込んだ情報をもう一度利用しています。 この情報を利用するためにESにfile_buf_seg=0x9000を代入してアクセスできるように しています。
;------------------------------------------------------------------------------- ; Read MONA.CFG ;------------------------------------------------------------------------------- push ds mov ax, cs mov ds, ax mov ax, file_buf_seg mov es, ax xor di, di mov bx, word[rde]
ルートディレクトリエントリにアクセスできるようになったらMONA.CFGという ファイルを探します。このファイル名はディレクトリエントリでは"MONA CFG"という 文字列になっています。この文字列がアドレスfnameに保存されており、それと比較する ことでMONA.CFGのディレクトリエントリを探すことができます。MONA.CFGが見つかれば file_foundに飛びますが、見つからなければchange_vesa_modeに飛んでディスプレイの 設定を行います。
file_next: mov si, fname mov cx, 0x000b push di rep cmpsb pop di je file_found add di, 0x20 dec bx jnz file_next jmp change_vesa_mode
ファイルが見つかったのでそのファイルを読み出す準備をしています。まず、 ファイルの先頭クラスタをCXに格納してex:fat(=9000:6000)にFAT情報を読み込みます。 この情報自体はfirstboot.asmで読み込んでいたのですが、ここでもう一度読み込みます。 そして最後にDSの値を変更しています。get_fatサブルーチンを呼び出すためにDSは FATを読み込んだセグメントと同じセグメントを指すようにしなければならないためです。
file_found: add di, 0x1a ; start sector mov cx, [es:di] mov bx, fat mov ax, 0x0001 mov di, [spf] call readsector mov ax, es mov ds, ax ; -- 何気にDSを変更している。by TAKA xor bx, bx
get_fatサブルーチンが使えるようになったのでファイルの読み込みに入ります。読み込み 先はES:BXでここでは9000:0000になっています。つまり、ルートディレクトリエントリの 情報があったところに上書きするわけです。 読み込みはreadsectorサブルーチンを用いて1クラスタ(セクタ)ずつ読み込んでいきます。 すべて読み切るか0x1000バイト読み込んだ時点で読み込み終了とします。そして、 DSの値を変更してこのファイルで定義した変数(アドレス)にアクセスできるようにします。
file_load: mov ax, cx add ax, 31 mov di, 1 call readsector push bx mov bx, cx call get_fat pop bx cmp ax, 0x0fff je end_of_file mov cx, ax add bx, 0x0200 ; jnc file_load cmp bx, 0x1000 ; limit file size jc file_load ; mov bx, es ; add bh, 0x10 ; mov es, bx ; xor bx, bx ; jmp file_load end_of_file: add bx, 0x0200 mov ax, cs mov ds, ax xor di, di
ファイルを読み込んだので次にその内容を解析します。まず、先頭がVESA_RESOLUTION=か VESA_BPP=で始まっているかどうかをチェックします。そして、VESA_BPP=で始まっていれば read_vesa_bppに飛び、VESA_RESOLUTION=で始まっていればread_vesa_resに飛びます。 どちらでもなければsearch_next_lineに飛んで次の行を読み込みます。
line_loop: mov si, strvbpp mov cx, [lenvbpp] push di rep cmpsb pop di je read_vesa_bpp mov si, strvres mov cx, [lenvres] push di rep cmpsb pop di jne search_next_line
VESA_RESOLUTION=あるいはVESA_BPPで始まる行があればここに飛びます。ここでは イコール記号(=)の後にある10進数標記の数字を読み取って、その内容を[vesares]または [vesabpp]に保存しています。[vesares]は画面解像度を表しており、[vesabpp]は色の数を 表しています。それぞれ次のようになっています。
[vesares]の値 | 画面の大きさ |
320 | 320x200 |
640 | 640x480 |
800 | 800x600 |
1024 | 1024x768 |
1280 | 1280x1024 |
[vesabpp]の値 | 色の数 |
16 | 16ビットダイレクトカラー。65536色(5:6:5) |
32 | 32ビットダイレクトカラー。約1680万色 |
10進数表記の数字の読み取りはread_numberサブルーチンで 行っています。
read_vesa_res: add di, word[lenvres] call read_number cmp ax, 0 je search_next_line mov [vesares], ax jmp search_next_line read_vesa_bpp: add di, word[lenvbpp] call read_number cmp ax, 0 je search_next_line mov [vesabpp], ax
最後に次の行を探します。これはLFかCRを読み取ったら次の行の開始とみなします。 これで、改行コードがMac、Win、Unixのどの形式であっても正しく処理できます。 なお、ファイルの最後まで読み進めたらchenge_vesa_modeに進んでvesa_modeサブルーチンを 呼び出してディスプレイの設定を行います。
search_next_line: mov al, [es:di] inc di cmp di, bx jnc change_vesa_mode cmp al, 13 je line_loop cmp al, 10 je line_loop jmp search_next_line change_vesa_mode: pop ds call vesa_mode
ディスプレイの設定が終わればいよいよプロテクトモードに移行します。プロテクトモードへは CR0レジスタの0番目のビットを立てるだけでいいのですが、その前に グローバルデスクリプタテーブル(GDT)の先頭アドレスを読み込ませる必要があります。 それがはじめの3行の処理です。gdtrに先頭アドレスとその大きさが格納されており、 その後にテーブルのデータが続いています。そして、割り込みを禁止してCR0の0番目のビットを 立ててflush_q1に飛びます。このジャンプ命令はまだ16ビットモードで記述されていて、 CSもリアルモードの値を引き継いでいます。
RealToProtect: ;;; Real to Protect mov ax, cs ; we jump from firstboot mov ds, ax ; so ds is changed lgdt [gdtr] ; load gdtr cli ; disable interrupt mov eax, cr0 ; real or eax, 1 ; to mov cr0, eax ; protect jmp flush_q1
プロテクトモードに移行した後実行の処理はここに飛んできます。まず、flush_q1で ジャンプ命令を生バイナリで書いています。意味はjmp 0x0008:0x00001000 + set_cs_desc1 で、セグメントセレクタの値を0x0008にして、EIPの値を0x00001000 + set_cs_desc1に せよ、ということです。要するにCSを変更して次にあるset_cs_desc1に飛べと いっているのです。NASMではこのジャンプ命令は書くことができないのでこのような 形式をとっています。
CSを設定したあと、DS、ES、SS、ESPを設定しKERNEL_ADDRに飛んでいます。 このKERNEL_ADDRはnasmを実行するときに外部から与えられるマクロで Mona0.2.0beta18では0x0400に設定されています。NASMでこのように書くと 自動的にファイルの先頭のコードのアドレスから加算してくれるので 結果としては0x1400に飛びます。C++のコードはリンカにより0x1400から 始まるように設定してあるのでこれでC++コードに飛ぶことができます。
[bits 32] flush_q1: db 0eah dw set_cs_desc1 + 0x100 * 16 dw 08h set_cs_desc1: mov ax, 0x10 ; ds & es mov ds, ax ; selector is mov es, ax ; 0x10 mov ax, 0x18 ; ss selector mov ss, ax ; is 0x18 mov esp, 0x80000 ; sp is 3MB push eax jmp KERNEL_ADDR
ES:DIから10進数表記の数字を読み取ってAXに保存します。読み取るのは '0'から'9'までの数字の連続でそれ以外の文字が出てきたら読み取りを 中止して、それまで読み込んだ数字の値を返します。ひとつも数字が読み込まれ なければ0を返します。また、安全のためファイルの境界(BXがファイルの大きさを あらわしている)を超えないようになっています。
read_number: xor ax, ax read_number_loop: xor cx, cx mov cl, [es:di] sub cl, '0' jc read_number_return cmp cl, 10 jnc read_number_return mov dx, 10 mul dx add ax, cx inc di cmp di, bx jc read_number_loop read_number_return: ret
vesa_modeサブルーチンは[cs:vesares]および[cs:vesabpp]の値を読み取ってVBEモード 番号を決定し、実際にそのモードに設定します。参考までに解像度と色数とモード番号の 表を掲載しておきます。この表によると解像度が320, 640, ..., と増えるにしたがって、 同じ色数ならモード番号が3ずつあがっていくことがわかります。また、同じ解像度でも 色の数が16ビットの場合のモード番号は32ビットの場合のモード番号-1になっています。 ここではとりあえず色数を32ビットしておいて画面解像度の解析を行ってモード番号を 決定しvesa_loopに入ります。
モード番号 | 解像度 | 色数 |
---|---|---|
0x010E | 320x200 | 64K (16) |
0x010F | 320x200 | 16.8M (32) |
0x0111 | 640x480 | 64K (16) |
0x0112 | 640x480 | 16.8M (32) |
0x0114 | 800x600 | 64K (16) |
0x0115 | 800x600 | 16.8M (32) |
0x0117 | 1024x768 | 64K (16) |
0x0118 | 1024x768 | 16.8M (32) |
0x011A | 1280x1024 | 64K (16) |
0x011B | 1280x1024 | 16.8M (32) |
;------------------------------------------------------------------------------- ; try VESA mode ;------------------------------------------------------------------------------ vesa_mode: mov ax, [cs:vesares] mov dx, 0x010f cmp ax, 320+1 jc vesa_loop add dx, 3 cmp ax, 640+1 jc vesa_loop add dx, 3 cmp ax, 800+1 jc vesa_loop add dx, 3 cmp ax, 1024+1 jc vesa_loop add dx, 3 cmp ax, 1280+1 jc vesa_loop add dx, 3
次に色数を判断して最終的なモード番号を決めてtry_vesa_modeサブルーチンを呼び出します。 これでモードが決まればいいのですが、決まらなかった場合はAXレジスタに0でない値が 入ります。この場合解像度、色数をともに下げていってモードが初期化されるまで続けられます。 320x200の16ビットモードまでグレードダウンしてもモードが初期化できなければ vesa_not_supportedに移ってVESAの使用をあきらめます。
vesa_loop: mov ax, [cs:vesabpp] cmp ax, 16+1 jc vesa_16bpp call try_vesa_mode cmp ax, 0 je vesa_mode_ret vesa_16bpp: dec dx call try_vesa_mode cmp ax, 0 je vesa_mode_ret cmp dx, 0x010e je vesa_not_supported sub dx, 2 jmp vesa_loop
VBEのどのモードも初期化できなければここに飛びます。ここではVESAのコントローラーの 情報を格納する領域に'N'と書いてVESAが使用できないことを通知します。そして、VGAによる 描画を試みます。
vesa_not_supported: mov di, vesa_info mov byte[di], 'N' jmp graphicalmode graphicalmode: mov ax, 0x0012 int 0x10 vesa_mode_ret: ret
まず、0x02ファンクションを実行して与えられたモードでの初期化を行います。このモードで 初期化できればAXレジスタに0x004Fという値が入るのでそれで初期化できたかどうかを 判定します。初期化できなければ、vesa_this_mode_ngに飛んで、エラーとしてAXレジスタに 0x0001を格納します。
;; try VESA dx = mode number try_vesa_mode: mov ax, 0x4F02 ; functon 02h mov bx, dx ; set video mode or bx, 0x4000 ; set linear mode int 0x10 cmp ax, 0x004F jne vesa_this_mode_ng
モードの初期化が終われば次にVBEコントローラーの情報を読み出します。これは 0x00ファンクションで行い、ES:DIで示されるアドレスにコントローラーの情報が 格納されます。この場合0000:vesa_info(=0000:0800=0x00800)に格納されます。 この情報はプロテクトモードにした後カーネルが使います。
get_vesa_info: xor bx, bx mov es, bx mov ax, 0x4F00 ; function 00h mov di, vesa_info ; 0x0000:vesa_info int 0x10 cmp ax, 0x004F jne vesa_this_mode_ng
コントローラーの情報を読み出したら次にモードの情報を読み出します。これも モードの情報を格納するアドレスをES:DIで指定します。この情報もプロテクトモードの 移行した後カーネルが使用します。
get_vesa_info_detail mov ax, 0x4F01 ; function 01h mov cx, dx ; set video mode mov di, vesa_info_detail ; 0x0000:vesa_info_detail int 0x10 cmp ax, 0x004F jne vesa_this_mode_ng
一連の動作が正常に終わればAXレジスタに0を格納し、どこかで失敗すれば1を格納して 処理が失敗したことを通知します。
vesa_this_mode_ok: xor ax, ax ret vesa_this_mode_ng: mov ax, 0x01 ret