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