OS自作~バグ修正その他~
前回のあらすじ
前回はFATを実装して、ファイルシステムが使えるようになりました。
ただ、前回の記事の内容を実装をしている時、僕がアホな間違いをしたせいでデバッグに時間を浪費したので、今回はバグ修正のやり方等少し開発そのものについて書こうと思います。
ファイルシステムのバグ修正
OSのデバッグの難しさ
最初に結論を言っておくと、ファイルシステムではなく、前前回に実装したIDE Controllerがぶっ壊れてました。しかもたちが悪いことにコードを追っているだけでは何も問題ないように見えるので、GDBを何度も使いました。OS開発が難しい所以はデバッグするのに必要な基盤が整っていないことにあると思いますが、逆にこんな環境でもデバッグできればちょっと自信になりますよね。
ヤバすぎるバグの症状
症状も本当に意味不明で、ディスクコントローラーからデータを読み出すと、うまく読み出せる時と0埋めされたデータが返ってくることがあり、更にprint文をポートの入力関数に噛ませて上げるとバグが起きないという完全にデバッグさせる気のないバグでした(ついでにGDBでステップ実行しても正常に動作するw)。
解決
では何が行けなかったかというと、ポートからの読み出しに使っていたのが、inslだったのが原因でした。つまり、ワードサイズのポートからロングサイズで読み出していたのです。逆になんでうまく読み出せていることがあったのか謎ですが、そこはもう気にしないことにします。
ちなみに、なぜこれがアホな間違いかというと、OSdevのページではinwが使われているのに、僕は「なんで32bitで読み込んでんだ?64bitで読み込んだ方が早いやん」と重いinslを使ったのが発端だからです(まさかと思ってOSdevのページを読み直して気づいた)。
せっかくなのでメモリマップを読み出した記念写真をどうぞ:
Bing Chatの力
さて、こっからは開発の様子についての話です。Bing Chatを僕が本格的に使うようになったのはおそらく今年の夏休み明けぐらいからですが、これはほんとに強力で、使う前より開発スピードが速いと思います。OSのコードは低レベルなので、ネットでもあまり情報がないわけですが、BingChatはその高い言語理解能力から低レベルのコードの解釈・生成もある程度こなせます。例えば、例のOSdevのページを読み込ませて、ide_ata_accessを書かせると…
// IDEコントローラのレジスタのアドレス
const ATA_PRIMARY: u16 = 0x1F0;
const ATA_SECONDARY: u16 = 0x170;
const ATA_READ: u8 = 0x20;
const ATA_WRITE: u8 = 0x30;
// IDEコントローラのレジスタにアクセスする関数
// drive: 0 or 1, master or slave drive
// lba: logical block address, 28 bits
// numsects: number of sectors to read/write
// read: 0 or 1, read or write operation
// buf: pointer to the buffer to read/write data
unsafe fn ide_ata_access(drive: u8, lba: u32, numsects: u8, read: u8, buf: *mut u8) {
// ドライブが0または1であることをチェック
assert!(drive == 0 || drive == 1);
// 読み書きが0または1であることをチェック
assert!(read == 0 || read == 1);
// ドライブごとにベースアドレスを決める
let base = if drive == 0 { ATA_PRIMARY } else { ATA_SECONDARY };
// IDEコントローラがビジーでないことを確認
while core::ptr::read_volatile((base + 0x07) as *const u8) & 0x80 != 0 {}
// ドライブとLBAモードを選択レジスタに書き込む
core::ptr::write_volatile(
(base + 0x06) as *mut u8,
0xE0 | (drive << 4) | ((lba >> 24) & 0x0F),
);
// セクタ数をセクタカウントレジスタに書き込む
core::ptr::write_volatile((base + 0x02) as *mut u8, numsects);
// LBAの下位24ビットを各レジスタに書き込む
core::ptr::write_volatile((base + 0x03) as *mut u8, (lba & 0x000000FF) as u8);
core::ptr::write_volatile((base + 0x04) as *mut u8, ((lba & 0x0000FF00) >> 8) as u8);
core::ptr::write_volatile((base + 0x05) as *mut u8, ((lba & 0x00FF0000) >> 16) as u8);
// 読み書きコマンドをコマンドレジスタに書き込む
core::ptr::write_volatile(
(base + 0x07) as *mut u8,
if read == 1 { ATA_READ } else { ATA_WRITE },
);
// セクタごとにデータを読み書きする
for i in 0..numsects {
// IDEコントローラの状態をチェック
let status = core::ptr::read_volatile((base + 0x07) as *const u8);
// エラーがあればパニック
if status & 0x01 != 0 {
panic!("IDE error: {}", status);
}
// 準備完了でなければ待つ
while status & 0x08 == 0 {
status = core::ptr::read_volatile((base + 0x07) as *const u8);
}
// データレジスタからバッファにデータを読み込むか、バッファからデータレジスタにデータを書き込む
if read == 1 {
// 読み込みの場合
for j in 0..256 {
// データレジスタから16ビットずつ読み込む
let data = core::ptr::read_volatile((base + 0x00) as *const u16);
// バッファに8ビットずつ書き込む
core::ptr::write_volatile(buf.offset((i * 512 + j * 2) as isize), data as u8);
core::ptr::write_volatile(
buf.offset((i * 512 + j * 2 + 1) as isize),
(data >> 8) as u8,
);
}
} else {
// 書き込みの場合
for j in 0..256 {
// バッファから8ビットずつ読み込む
let data1 = core::ptr::read_volatile(buf.offset((i * 512 + j * 2) as isize));
let data2 = core::ptr::read_volatile(buf.offset((i * 512 + j * 2 + 1) as isize));
// データレジスタに16ビットずつ書き込む
core::ptr::write_volatile(
(base + 0x00) as *mut u16,
data1 as u16 | ((data2 as u16) << 8),
);
}
}
}
}
これが返ってきます。え、もうこれ近い将来OSまるごとかけるよね?そんなわけで、100%解決するわけじゃないけど、ちゃんと状況説明すれば、そこそこちゃんとバグ修正に役立つ回答が得られる感じ。
GDBの使い方
GDBって何ぞ?
GDBはLinuxなら必ず入ってるデバッガで、ソースコードレベルのデバッグだけでなく、アセンブラレベルでもデバッグできて、かつソースコードのどの行に相当するかを表示してくれる協力なデバッガ。以前にもOSのデバッグに使いましたが、今回のバグ修正でも何度も使ったので、使い方を残しておこうと思います。
QEMU起動〜GDB接続
OS開発におけるGDBの使い方としては、まずQEMUの起動時に-s -Sオプションをつけることで、GDBから実行できるようにします(QEMUを起動すると、GDBから支持を出すまで停止します)。
QEMUを起動したら、以下ようにGDBのセットアップをします:
// QEMUに接続
(gdb) target rmeote :1234
// デバッグ用にバイナリを読み込む
(gdb) file ./kernel/horse-kernel
// アセンブラのダンプをintel記法に設定
(gdb) set disassembly-flavor intel
// 実行が停止するごとにアセンブラを5行分表示する
(gdb) disp/5i $pc
これでcまたはcontinueコマンドを送ると、実行を開始しますが、その前に次のセクションに書くブレークポイントの設定等を行いましょう。
レジスタ表示等
「i r」:全レジスタの表示
p $(レジスタ名):特定のレジスタの表示
x/10xw (アドレス):アドレスのメモリから10ワード分表示。アセンブリならディスアセンブリして見せてくれる。
disp ~:これで設定すると、実行を停止するたびに毎回表示する
b (関数名・アドレス):ブレークポイントの設定
i b:ブレークポイントの一覧。これで番号を確認
del (ブレークポイントの番号):ブレークポイントの削除
ここにあるやつを使えば、だいたいのデバッグはできますが、必要に応じて調べましょう。
次回にすること
本当は今回のバグ修正が大変だったので一度OS開発は休むつもりでしたが、ファイルアクセスができるようになったことで返ってできることの妄想が膨らんでしまったので、この記事を書いてる今はマルチタスクの実装をしています。
というわけで次回はマルチタスクを実現させようと思います。
追記:この次の記事は実装はせずLinuxのスケジューラーについて下調べをしています。