firstboot.asm

このコードはブートセクタにあるコードでマシンが起動するとまずこのコードが 読み出されて実行されます。また、このコードの初めはBPB(BIOS Parameter Block)に なっておりFATシステムのために必要な情報、例えば1セクタあたりのバイト数や1クラスタ あたりのセクタ数などの情報が格納されています。そういうわけでコードの実行は realstartから始まります。

メインルーチン

まず、ブートセクタはメモリアドレス0:7c00(0x07c00)に読み込まれています。 この位置はカーネルを読み込むにはちょっと中途半端な位置なのでtempseg:0000つまり 0x9f000番地に移動させて制御をそっちに移します。何故中途半端なのか、 何故この位置なのかは後のコードの動きをみていけば自然にわかると思います。

realstart:
        xor     si,si
        mov     di,si
        mov     ax,0x07c0
        mov     ds,ax
        mov     ax,tempseg
        mov     es,ax
        mov     cx,0x0100
        rep     movsw
        jmp     tempseg:realnext
ここまでのレジスタの値
ax tempseg = 0x9f00
bx ???????
cx 0x0100
dx ???????
di 0x0100
si 0x0100
cs 0
ds 0
es tempseg
ss ???????
sp ???????

次にdsの設定とスタックの設定を行います。スタック領域はプログラムが 実行される領域のずっと前にあり、後に読み込まれるルートディレクトリエントリの ために余裕を持たせています。スタックのセグメントは tempseg - 0x1000 = 8f00 で 0x8f000 より前の領域がスタック領域になります。この計算は直接AXレジスタを 操作するのではなくAHレジスタを操作して行っています。ちなみにスタックはアドレスの 上位から下位に向かって伸びていきます。

realnext:
        mov     ds,ax
        sub     ah,0x10
        mov     ss,ax
        xor     sp,sp
        ;
           0x8f000             0x9f000
-------------------------------------------------------------------------
 stack domain |                   | code domain     |
           ← |                   |                 |
-------------------------------------------------------------------------

次にint13-0の割り込みを掛けています。この割り込みはディスクドライバを 初期化します。また、putstringサブルーチンでMona loading...\n\r を表示します。 このサブルーチンはsiのアドレスに指定されている文字列を表示します。

        xor     ax,ax
        int     0x13
        jc      $
        ;
        mov     si,boot
        call    putstring
        ;

ここではルートディレクトリエントリをディスクから読み出しています。 ルートディレクトリエントリはその名のとおり、ルートディレクトリにある ファイルの名前とディスク上のファイルの位置を保存しています。この 領域はFAT領域の次にあり、ルートディレクトリの最大エントリ数 / 16 セクタ分 続いています。

コードによると、ルートディレクトリエントリは9000:0000、つまり、0x90000に 書き込まれます。FAT領域の占めるセクタ数は[spf]でアクセスでき、FAT領域の コピーの数は[nof]になっています。[spf] * [nof] に1足せば ルートディレクトリエントリの開始セクタ-1になります。このことは実際に紙にセクターの 配置を書いてみればわかります。セクタの開始番号が1であることに注意してください。 ちなみにルートディレクトリエントリが持つ最大エントリ数は512で 1エントリ当たり32バイトなので16384 = 0x4000 バイトになります。 この大きさだと0x90000から書き込んでもコード領域とぶつかることはありません。

セクタを読み出すサブルーチンはAXレジスタにセクタ番号、DIレジスタに 読み込みセクタ数、ES:BXに読込先のアドレスを指定することになっています。

        xor     dx,dx
        mov     cx,dx
        mov     bx,dx
        mov     ax,0x9000
        mov     es,ax
        mov     ax,word [spf]
        mov     cl,byte [nof]
        mul     cx
        inc     ax
        mov     di,word [rde]
        push    di
        shr     di,4            ; rde / 16 = required sectors
        inc     di
        call    readsector

        xor     di,di
        pop     bx
ここまでのレジスタの値
ax [spf] * [nof] + 1
bx ???????
cx 0x0100
dx 0x0000
di 0x0100
si boot
cs tempseg
ds tempseg
es 0x9000
ss 0x8f00
sp 0x8f00

前のコードでルートディレクトリエントリはES:DIに読み込まれました。 ここでは読み込まれたディレクトリエントリを1つずつみていきES:DIが "KERNEL IMG"となっているかどうかを調べます。もし文字列が一致すれば カーネルファイルが見つかったと言うことになるし、見つからなければ DIに32を足して次のエントリを探します。もし、全てのエントリを探しても 見つからなければkernel_not_foundに飛んでカーネルが見つからなかったと言うことを 通知します。ESの値を変更していないから実際は無限ループに陥るんじゃないかと思う。

kernel_next:
        mov     si,bname
        mov     cx,0x000b
        push    di
        rep     cmpsb
        pop     di
        je      kernel_found
        add     di,0x20
%ifdef DEBUG
       call    register_dump
%endif
        dec     bx
        jnz     kernel_next
        jmp     file_not_found

KERNEL.IMGを読み込むためにまず、FAT情報をfatで示しているアドレスに ロードします。fat = 0x6000、EX = 0x9000 だから0x96000 に読み込まれることになります。 FATの開始アドレスはブートセクタの次なので0x0001、読み込む大きさはds:spfで 示されるアドレスに格納されています。

kernel_found:
        add     di,0x1a                 ; start sector
        mov     cx,[es:di]
%ifdef DEBUG
       call    register_dump
%endif
        mov     bx,fat
        mov     ax,0x0001
        mov     di,[spf]
        call    readsector
        mov     ax,es
        mov     ds,ax
        xor     ax,ax
        mov     es,ax
        mov     bx,0x1000
ここまでのレジスタの値
ax 0x9000
bx 0x1000
cx KERNEL.IMGのはじめのクラスタ
dx 0x0000
di FAT情報が格納されているアドレス
si boot
cs tempseg
ds 0x9000
es 0x0000

KERNEL.IMGをES:BXに読み込みます。このアドレスは0x10000になっています。 読み込みはまず、1クラスタ(1セクタ)読み込んで、get_fatを呼び出して次の クラスタ番号を得てまた読み出して、と言うようになっています。 クラスタ番号からLBAへの変換はややこしいのですが、フロッピーディスクに 限って言えば「クラスタ番号+31」が対応するLBAになっています。 このようにして読み進め、最後まで読み終えたら、つまり次のクラスタ番号が 0x0FFFならば end_of_kernel に飛びます。まだ読み込む必要があるなら、 BX、必要ならESを更新して再び1クラスタ読み込みます。

kernel_load:
        mov     ax,cx
        add     ax,31
        mov     di,1
        call    readsector
        push    bx
        mov     bx,cx
        call    get_fat
        pop     bx
%ifdef DEBUG
       call    register_dump
%endif
        cmp     ax,0x0fff
        je      end_of_kernel
        mov     cx,ax
        add     bx,0x0200
        jnc     kernel_load
        mov     bx,es
        add     bh,0x10
        mov     es,bx
        xor     bx,bx
        jmp     kernel_load

カーネルを読み終えたのでそのカーネルに制御を移します。 カーネルは0x10000に読み込んでいるのでjmp 0x0100:0000で 飛びます。また、ここで新たにスタックの位置も設定しなおしています。。

0x00000          0x01000
-------------------------------------------------------------------------
 stack domain    ← | kernel code domain     |
                    |                        |
-------------------------------------------------------------------------
end_of_kernel:
        xor     ax,ax
        mov     ds,ax
        mov     es,ax
        mov     ss,ax
        mov     sp,kernel
        call    memory_dump

        jmp     0x0100:0x0000
ここまでのレジスタの値
ax 0x0000
ds 0x0000
es 0x0000
ss 0x0000
sp 0x1000
cs 0x0100
ip 0x0000

secondboot.asm に続きます。

戻る

サブルーチン

getfat

getfatサブルーチンはFAT情報を参照して、BXで与えられたクラスタ番号の 次のクラスタ番号をAXに返します。FAT12の場合次のクラスタ番号はまず、 FAT情報開始アドレスから BX + BX >> 1 バイト目のオフセット(16ビット) を得て、クラスタ番号が奇数なら読み込んだ値を4ビット右にシフトさせ、 偶数ならば上位4ビットをクリアします。このコードの場合読み込むオフセットは [SI + BX + fat]で求められるようになっています。

注意:コードには陽に現れていませんが、DSの値をFATを読み込んだセグメントを さすようにしなければなりません。

get_fat:
        mov     si,bx
        shr     si,1
        mov     ax,[si+bx+fat]
        jnc     _get_fat
        shr     ax,4
_get_fat:
        and     ah,0x0f
        ret

putstring

putstringサブルーチンはsiで指定されたアドレスにある文字列をスクリーンに表示します。 文字列の終端はNUL文字で示します。また、改行はLFCRの続きで指定します。 初めのcs lodsbがちょっと気になるところですが、これは通常DS:SIから読むところを CS:SIから読み込むように仕向けたもので、nasm独特のオプションだと思います。 int10-eで1文字ずつ表示させていきます。

putstring:
        pusha
.put:   cs lodsb
        or      al,al
        jz      .pute
        xor     bx,bx
        mov     ah,0x0e
        int     0x10
        jmp     .put
.pute:  popa
        ret

tohex

tohexサブルーチンはAXで指定された値を上位ビットからCLで指定された文字数分 出力します。つまり、AX=0x789AでCL=0x04なら789Aと出力されますし、CL=0x02なら 78と上位8ビットだけ出力されます。

16進数表記をするためには4ビットずつ値を取り出す必要があります。この処理を rol(左ローテーション)とand命令を使って実現しています。たとえば、0x789Aを 出力するとき、rol命令で0x89A7とした後and命令で0x0007になります。なお、途中の push命令は次の4ビット情報を取り出すために保存しておくためのものです。こうして 4ビットの値を取り出した後ASCIIコードに変換します。ASCIIコードは0から9までの 数字の場合は30を加えるだけですが、アルファベットの文字であれば37加える必要があります。 この辺の分類を.toあたりでやっています。

; tohex
;   ax = data to convert and display, cl = shift count

tohex:
        pusha
.tol:   rol     ax,4
        push    ax
        and     al,0x0f
        cmp     al,0x0a
        jb      .to
        add     al,0x07
.to:    add     al,0x30
        mov     ah,0x0e
        int     0x10
        pop     ax
        dec     cl
        jnz     .tol

必要な数字を出力し終えたら後の出力と区別するためにスペースを出力しています。

        mov     ax,0x0e20
        int     0x10
        popa
        ret

memory_dump

memory_dumpサブルーチンはES:DIで指定したアドレスから16バイト読み取って 画面に表示します。そして、最後に改行としてCRLFを出力して改行します。 画面への出力はtohexやputstringサブルーチンを使っています。

memory_dump:
        pusha
        mov     bl,0x10
.mem:   mov     ah,byte [es:di]
        inc     di
        mov     cl,0x02
        call    tohex
        dec     bl
        jnz     .mem
        mov     si,crlf
        call    putstring
        popa
        ret

register_dump

register_dumpサブルーチンはAX, BX, CX, DX, SI, DI, CS, DS, ES, SPの順番に 16進数表記で出力して最後に改行します。実行の仕組みは、まず表示する対象の レジスタをスタックに表示する順番とは逆順に詰め込んでいき、それをAXレジスタに ポップしてひとつずつtohexサブルーチンを使って表示していくようになっています。

register_dump:
        pusha
        push    sp
        push    es
        push    ds
        push    cs
        push    di
        push    si
        push    dx
        push    cx
        push    bx
        push    ax
        mov     cx,0x0a04
.reg:   pop     ax
        call    tohex
        dec     ch
        jnz     .reg
        mov     si,crlf
        call    putstring
        popa
        ret

readsector

readsectorサブルーチンはAXで指定されたセクタ番号からDIセクタ分読み取って ES:BXに格納していきます。このサブルーチンは直接フロッピーディスクの 1トラックあたりのセクタ数などの数値を用いているためフロッピーディスクでしか 使えません。また、読み込み開始位置を表わすAXはディスクのセクタの初めから0を 割り振っているLBA(Logical Block Address)で指定しますがBIOSファンクションコールでは CHS形式でしか受け付けないので次のような変換してからBIOSファンクションコールを 呼び出します。

cylinder = LBA / (SectorsPerTrack * NumHeads)
temp = LBA % (SectorsPerTrack * NumHeads)
head = temp / SectorsPerTrack
sector-1 = temp % SectorsPerTrack
;   ax = start sector, di = number of sectors to read
;   es:bx = read address (es = 64kb align, bx = 512 bytes align)
readsector:
        pusha
        push    es
        ;

ここでは1回の読み出しで実際に読み出せる大きさを決めています。 BIOSファンクションコールは1度に複数のセクターを読み込むことが出来ますが、 セグメントの境界をまたぐことが出来ません。オフセットが0xFFFFをまたぐことが 出来ないからです。そこで、1回の読みだしでセグメントの境界をまたがないように セクタ数を調節する必要がでてきます。読み出せるセクタの数は(0x10000 - BX) / 512 で求めることができるのでこれを計算し、一旦SIに格納します。もしこの値が与えられた DIより大きければ、つまり、余裕を持って読み出せるのならSIの値を変更します。 以降、SIが読み出すセクタの数になります。

_read0: mov     si,bx
        neg     si
        dec     si
        shr     si,9
        inc     si
        cmp     si,di
        jbe     _read1
        mov     si,di           ; di < si

読み出せるセクタ数というのは(0x10000 - BX) / 512で求めることができます。 しかし悲しい哉。リアルモードでは16ビットまでの数字しか扱えないため この計算を直接出来ません。そこでいくつかのテクニックを用いてこの計算を行うことに なります。まず、0x10000 - BXですがこれは2の補数表現です。つまりneg BXとすれば この式の結果が得られます。しかし、BX = 0のとき式の結果が式の結果が16ビットの 範囲を越えてしまうので正しく求めることができません。従ってどんなBXの値に対しても 16ビットの範囲で計算できるような方法を考えなければなりません。その答えが1の補数です。 これは0x10000 - BX - 1と言う形になってBXがどんな値でも必ず16ビットの範囲で 収まってくれます。次にこれを512で割って、その値を(0x10000 - BX) / 512になるように 変換しなければなりません。ここで次のように考えてみます。

まず、

(0x10000 - BX - 1) / 512 = Q
(0x10000 - BX - 1) % 512 = R

とします。そして、この関係を使って0x10000 - BX - 1を求めると

0x10000 - BX - 1 = Q * 512 + R

になり、

0x10000 - BX = Q * 512 + R + 1

になります。よって、

(0x10000 - BX) / 512 = Q + (R + 1) / 512

となります。つまり、求めるセクタ数はQがQ+1の値です。Q+1になる条件としては R = 511のときですが、これはBXが512の倍数になっていることです。従って、 BXが512の倍数と想定しているならばQに1加えなければなりません。逆にBXが512の 倍数でなければ格納できるセクタ数より1大きい数を求めてしまいます。

読み出すセクタの数が決まれば、次に実際にLBAからCHSへの変換をします。 変換方法は次のようになっています。

cylinder = LBA / (SectorsPerTrack * NumHeads)
temp = LBA % (SectorsPerTrack * NumHeads)
head = temp / SectorsPerTrack
sector-1 = temp % SectorsPerTrack

まず、xor dx, dxでDXレジスタをクリアします。 除数(割る数)が16ビットの場合被除数(割られる数)はDX:AXという指定になります。 この場合AXの数値だけを割りたいのでDXを0にします。そして、 CXにSectorsPerTrack * NumHeads = 0x0024の値を設定して div cxを実行すると商、つまりシリンダ番号がAXに格納され、余り、つまりtempの値は DXに格納されます。

次にxchgでAXとDXの内容を入れ替え、cxにSectorsPerFat = 0x0012を設定します。div clを 実行すると今度は8ビットの割り算なので商、つまりヘッド番号がALに格納され、 余り、すなわちセクタ番号-1の値がAHに格納されます。

ところで、ここで気をつけなければならないことがあります。BIOSファンクションコールで 複数のセクタを読み出すときトラックをまたがないようにすることです。セグメントを またがない、トラックをまたがない、とやたらと制限が多いですが仕方がありません。 読み出せるセクタ数はトラックあたりのセクタ数(18)- セクタ番号 - 1となります。 この計算はsub cl, ahでこの計算をしており、この値を一旦CLに入れます。もし、 読み出すセクタの数、つまりSIの値がこの値よりも小さければ、余裕を持って読み込めるので その値にします。

_read1: push    ax
        xor     dx,dx
        mov     cx,0x0024
        div     cx              ; ax = track number
        xchg    dx,ax
        mov     cl,0x12
        div     cl              ; ah = sector number, al = head number
        sub     cl,ah
        cmp     cx,si
        jbe     .read1
        mov     cx,si           ; si < cx

この段階でヘッド番号、シリンダ番号、セクタ番号は次の位置に格納されています。

ヘッド番号 AL
シリンダ番号 DX(8ビットの大きさに収まっています)
セクタ番号- AH

最後にBIOSファンクションコールを呼び出してディスクを読み取ります。もし、 エラーが生じたらキャリーフラグがたつのでそれを検知して_read_errorに飛びます。

.read1: mov     ch,dl           ; ch = track number
        mov     dh,al           ; dh = head number
        mov     al,cl           ; al = number of sectors to read
        mov     cl,ah           ; cl = sector number
        inc     cl
        xor     dl,dl           ; dl = drive number
        mov     ah,0x02
%ifdef DEBUG
        call    register_dump
%endif
        int     0x13
        jc      _read_error

ディスクの読み込みに成功すればAXには実際に読み込んだセクタの数が格納されます。 この数から読み出した大きさを読み込むオフセットを表わすBXに足し、読み出すセクタ (LBA)を表わすAXに足します。ちなみにこのAXはread_1の所でスタックにプッシュして いたものです。そして、DI、SIの値も読み出したトラックの数だけ引きます。 ここで、SIの値が0にならなかったと言うことはトラックの境界をまたいで読み出せなかった と言うことを表わします。そこでもう一度読み込むために_read1にジャンプします。

        mov     dx,ax
        mov     cl,0x09
        shl     ax,cl
        add     bx,ax
        pop     ax
        add     ax,dx
        sub     di,dx
        sub     si,dx
        jnz     _read1

SIが実際に読み込んだ大きさに等しければこの部分に移ります。ここでは セグメントを移してBXをクリアして新たに読み込む準備をしています。もし、 この段階でDIも0になっていれば全て読み込んだと言うことなので返りますが、 0でなければ新たなデータを読み出すために_read0に移ります。

        mov     cx,es
        add     ch,0x10
        mov     es,cx
        xor     bx,bx
        or      di,di
        jnz     _read0
        ;
        pop     es
        popa
        ret

最後に読み込みに失敗したときですがこのときはまずデバイスを初期化してから もう一度読み込もうとします。

_read_error:
%ifdef DEBUG
       call    register_dump
%endif
        xor     ax,ax
        xor     dl,dl
        int     0x13
        pop     ax
        jmp     _read1
戻る inserted by FC2 system