secondboot.asm

このコードは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
戻る

サブルーチン

read_number

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

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

try_vesa_mode

まず、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
inserted by FC2 system
戻る