|
ブートローダその10 フロッピーからデータを読み込む
|
前回までの内容
これまでで、
- 文字の表示にはBIOSが用意した割り込み処理を使う
- INT0x10命令を実行することで文字表示の割り込み処理を実行できる
- INT0x10命令を実行する前にAH=0x0E、AL=アスキーコード、BH=0x00、BL=文字の色を
引数として指定する
- アセンブラの繰り返し処理には計算結果が0かどうかを判別するEFLAGSレジスタのZFフラグを利用する
ということがわかりました。それではブートローダの開発を進めていきましょう!
フロッピーからデータを読み込む
今回はフロッピーからデータを読み込む方法についてです
フロッピーで扱う最小単位はセクタで512バイトでした。ですので、読み込むときもセクタ単位(512バイト単位)で行います
今回もやはりBIOSの割り込み処理を使ってセクタを読み込みます
使用する割り込み処理はINT0x13命令を実行することで呼び出せます
読み込みで使用するINT0x13命令には2つのモードがあります
1つ目はフロッピードライブの初期化モード、2つ目が当然セクタ読み込みモードです
INT0x13命令のフロッピードライブ初期化モード
フロッピードライブの初期化を行うことで読み取る機械のヘッドを最初のセクタ(セクタ番号1)の位置に戻すことができます
では初期化モードの引数をみてみましょう
- AH:0x00を入れます。0x00は初期化モードです
- DL:0x00を入れます。リセットするドライブ番号を入れます(0から始まります。今回はAドライブなので0をいれました)
また割り込み処理を行った後の結果(返り値)が汎用レジスタに格納されます
- AH:ステータスコードが入ります
- 成功の場合:EFLAGSレジスタのCF(Carry Flag)が0になります
失敗の場合:EFLAGSレジスタのCF(Carry Flag)が1になります
では簡単な関数を作ってみます
ResetFloppyDrive:
MOV AH, 0x00
MOV DL, 0x00
INT 0x13
JC FAILURE
HLT
FAILURE:
HLT
この例ではJNC命令が新しくでてきました。JC(Jump if Carry)命令はEFLAGSのCF(Carry Flag)が
0の場合FAILUREラベルにジャンプします。ついでに、逆の命令としてJNC(Jump if Not Carry)命令が
あります。JNC命令は逆にCFが1の場合にジャンプするという命令となります。FAILUREラベル以降ではエラー
処理を行うようにします
(この例の場合成功、失敗でもHLT命令が実行されるだけですが。。。)
これまでやってきたこととほとんど同じで、簡単にできたかと思います
これでフロッピードライブの初期化ができました。次にセクタ読み込みを見ていきます
INT0x13命令のセクタ読み込みモード
指定したセクタを読み込むには同様にINT0x13命令を使います。ただし指定する引数が違います
- AH:0x02を入れます。0x02はセクタ読み込みモードです
- AL:何セクタ読み込むのかを入れます。読み込みたいセクタ数を入れてください
- CH:シリンダー(トラック)番号の16ビットの内下位1バイト(8ビット)を入れます
(フロッピーのシリンダー(トラック)番号は0から80です)
- CL:読み込むセクタ番号を入れます(セクタの番号は1から18を指定します。
フロッピーの1つのシリンダー(トラック)に18個のセクタがありました)
- DH:ヘッド番号を入れます(フロッピーのディスクは両面読み取りですのでヘッドは2つあります。
ですので0から1をいれてください)
- DL:ドライブ番号をいれてください(今回はAドライブを想定していますので0x00をいれてください)
- ES:BX 読み込んだデータを格納するアドレスを指定してください
ESセグメントのオフセットBXを指定します(もう簡単ですね)
そしてこの割り込み処理も返り値が汎用レジスタに格納されます
- AH:ステータスコードが入ります
- AL:読み込んだセクタ数が入ります
- 成功の場合:EFLAGSレジスタのCF(Carry Flag)が0になります
失敗の場合:EFLAGSレジスタのCF(Carry Flag)が1になります
これでフロッピーディスクから好きなセクタを好きなアドレスに読み込むことができます
ここで指定する引数のセクタ、シリンダ(トラック)、ヘッドのおさらいです
- セクタ:フロッピーディスクの1つのセクタの大きさは512バイトあります
- シリンダ:フロッピーディスクの1つのシリンダの大きさは18セクタです
- ヘッド:フロッピーディスクのヘッドは2つありディスクの両面を読み込むことができます
1つのヘッドにつき80シリンダあります
ではフロッピーディスクの総セクタはいくつになるでしょうか。
フロッピーディスクの総セクタ数 =ヘッド数×ヘッダあたりのシリンダ数×シリンダあたりのセクタ数
=2×80×18
=2880セクタ(0X0E04セクタ)
あります。FAT12のブートセクタのに書き込んだ総セクタ数に一致しています。
(他のディスク情報についても確認してみてください)
それではフロッピーディスクからセクタを読み込んでみましょう。
ReadSectors:
MOV AH, 0x02 ;セクタ読み込みモード
MOV AL, 0x01 ;1つのセクタだけ読み込み
MOV CH, 0x01 ;2つ目のシリンダ(トラック)上にあるセクタを読み込みます
MOV CL, 0x02 ;指定したシリンダ上の2つ目のセクタを読み込みます
MOV DH, 0x00 ;1番目のヘッドから読み込みます(フロッピーには2つのヘッドがある)
MOV DL, 0x00 ;1番目のドライブから読み込みます(フロッピーのAドライブは1番目)
MOV BX, 0x1000
MOV ES, BX ;アドレス0x10000から開始するセグメントに読み込みます
MOV BX, 0x0000 ;セグメントの最初に読み込みます。オフセットに0x0000を指定。
INT 0x13 ;セクタを読み込みます
このBIOSの割り込み処理でフロッピーディスクの最初のセクタ(ブートセクタ)をアドレス0x10000に読み込みます
一般的なOSは0x10000にカーネルが読み込まれますので、今回はそれを真似てみました
このINT0x13の処理ではヘッド、シリンダ、セクタを指定して読み込みましたが、
どこのヘッドでどこのシリンダで。。。と考えていくとちょっとどのセクタまで読み込んだのかソフトウェア上の管理も
やりにくかったりします。そこでATAの仕様ではLBA(Logical Block Addressing)が導入されていますので
LBAを使ってディスクにアクセスしようと思います
(HDDやCDとかも同じ考え方で通常LBAを扱います)
LBA(Logical Block Addressing)
LBAはその名が示しているように論理アドレス的な役割で、メモリのアドレスは1バイト目、
2バイト目〜4Gバイト目と1バイトずつ数えていましたが、LBAでは1セクタ目、
2セクタ目〜最大セクタ数目とセクタ単位で数えていきます(すべてのセクタに0から番号をつけていきます)
ソフトウェア上では、ヘッドやシリンダというこを気にすることなく論理的なセクタを指定することで
ディスクにアクセスできます
例えば、0番目のヘッド、1番目のシリンダ、1番目のセクタにアクセスしたいときにLBAは、
LBA = ヘッド番号 × ディスク上のシリンダ数 × シリンダ毎のセクタ数 + シリンダ番号 × シリンダ毎のセクタ数 + セクタ番号
= 0 × 80 × 18 + 2× 18 + 1
= 37(セクタ目)
となります。これは例で、こんなややこしい計算はしなくていいと思います
さきほどヘッド数とシリンダ数とシリンダ毎のセクター数から
フロッピーディスクの総セクタ数は2880セクタであることがわかりましたので、
実際は直感的に(直感では困りますが。。。)
LBA 0-2879セクタのアドレス範囲でアクセスすることになります
これでヘッド、シリンダを指定しなくてよくなったので、ずっと楽になったかと思います
(直線的な一本のアドレスになりますので、
Linear Addressing:ライナーアドレッシングといいます。メモリもそうですね。)
しかし、LBAでアクセスできるからと言いましても、実際にはINT0x13命令でBIOSの割り込み処理を使いますので
最終的には元のヘッド、シリンダの単位に直さないといけません
ここで注意していただきたいのですが、物理セクタの番号は1から始まりでした(ヘッド、シリンダは0から始まる)
しかし、LBAで指定する倫理セクタは0から始まりますので、この点だけ気をつけてください
LBAから物理ヘッド番号に変換する
物理ヘッド番号は次のように計算します。(時間のある方は式の意味も考えてみるとより深く理解できます)
物理ヘッド番号 = ( LBA / シリンダ毎のセクタ数 ) mod ヘッド数
となります。フロッピーディスクの場合シリンダ毎のセクタ数は18、ヘッド数は2です。
この式のmodは割り算した結果の余りです
LBAから物理シリンダ(トラック)番号に変換する
物理シリンダ番号は次のように計算します
物理シリンダー番号 = LBA / ( シリンダ毎のセクタ数 × ヘッド数 )
LBAから物理セクタ番号に変換する
物理セクタ番号は次のように計算します
物理セクタ番号 = ( LBA mod シリンダ毎のセクタ数 ) + 1
+1していることに注意してください。物理セクタ番号は1始まり、LBAである論理セクタ番号は0始まりでした
ではアセンブラで変換してみましょう
アセンブラでLBAを物理ヘッド番号、シリンダ(セクタ)番号、セクタ番号に変換してみる
それではまず変換関数を見てみます
;/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
; LBA2CHS
; input AX:sector number(LBA)
; output AX:quotient, DX:Remainder
; convert physical address to logical address
; physical sector = (logical sector MOD sectors per track) + 1
; physical head = (logical sector / sectors per track) MOD number of heads
; physical track = logical sector / (sectors per track * number of heads)
;/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
LBA2CHS:
XOR DX, DX ; initialize DX
DIV WORD [BPB_SecPerTrk] ; calculate
INC DL ; +1
MOV BYTE [physicalSector], DL
XOR DX, DX ; initialize DX
DIV WORD [BPB_NumHeads] ; calculate
MOV BYTE [physicalHead], DL
MOV BYTE [physicalTrack], AL
RET
physicalSector DB 0x00
physicalHead DB 0x00
physicalTrack DB 0x00
物理セクタ番号の計算
ここでDIV命令が出てきました。DIV命令は割り算(DIVide)です(符号無しで割り算します)
ただし、注意点があります。DIV命令では変数WORD [BPB_SecPerTrk]しかありません
割る数、割られる数の関係はどうなっているのでしょうか
DIV命令は16ビットの変数を指定するとそのAXレジスタの値割る変数を計算します
計算した結果、AXレジスタに商を、DXレジスタに余りを格納します
この例ではAXレジスタの値割るシリンダ(セクタ)毎のトラック数を計算しています
余りを格納しているDXに1を足せば物理セクタ番号が取得できます
次の命令のINC DLで、DLレジスタ(余りが格納されている)に1を足します
これでDLレジスタ(フロッピーでは最大2880セクタですので、
結果はDLレジスタの1バイト(最大値255)に収まります)に
物理セクタ番号が格納されますので、absoluteSector変数に
MOV命令でDLレジスタの値を格納します
DIV WORD [BPB_SecPerTrk] ; AXレジスタの値(LBA)÷シリンダ(セクタ)毎のセクタ数
INC DL ; (LBA mod シリンダ毎のセクタ数)余りに1を足す
MOV BYTE [physicalSector], DL ;計算した物理セクタ番号を変数に格納
物理ヘッド番号の計算
先ほどの計算結果で、AXには最初にAXに格納されていました
LBA÷シリンダー(トラック)毎のセクター数の商が格納されいます
ですので、AXの値は( LBA / シリンダ毎のセクタ数 )となっていますので、
これをヘッド数BPB_NumHeadsで割ってやります
その余りがDXに格納されますので(同じく1バイトに収まります)、
DLの値を変数physicalHeadに格納します
DIV WORD [BPB_NumHeads] ; calculate
MOV BYTE [physicalHead], DL
物理シリンダ(トラック)番号の計算
ここまでの計算でAXの値には( LBA / シリンダ毎のセクタ数 / ヘッド数 )が格納されいますので
これを変数physicalTrackに格納します
MOV BYTE [physicalTrack], AL
これで物理ヘッド番号、物理シリンダ(トラック)番号、物理セクタ番号が計算できました
この関数をINT0x13命令の前に呼んでやれば自動的にLBAから物理アドレスに変換することができます
LBA論理セクタ2000を読み込んでみる
では2000論理セクタを読み込んでみましょう(何も記録されていないので意味はありませんが。。。軽く流してください)
MOV AX, 2000 ; 10進数
CALL ReadSectors
ReadSectors:
CALL LBA2CHS
MOV AH, 0x02 ; 読み込みモード
MOV AL, 0x01 ; 1セクタだけ読み込み
MOV CH, BYTE [physicalTrack] ; トラック
MOV CL, BYTE [physicalSector] ; セクタ
MOV DH, BYTE [physicalHead] ; ヘッド
MOV DL, BYTE [BS_DrvNum] ; ドライブ(BS_DrvNumにはx00をいれておきます)
MOV BX, 0x1000
MOV ES, BX ; アドレス0x10000から開始するセグメントに読み込みます
MOV BX, 0x0000 ; セグメントの最初に読み込みます。オフセットに0x0000を指定。
INT 0x13 ; ディスク読み込み
RET
これで、AXに読みたい論理セクタを指定してReadSectors関数を呼び出せばアドレス0x10000に
読み込みたいセクタが読み込まれます(この例では1セクタのみ)