起動するまでの長い道のり IPL編(8) カーネル読み込み実装の巻
前回でディスクからのデータ読み込み方法と、その時にぶつかる壁について解説した。今回はいよいよデータの読み込み処理を実装していく。
ソースファイルを追加する
さて、読み込み処理を書くに当たって読み込むデータ(プログラム)が何か無いと始まらない。今までコードは全てipl.sという1つのソースファイルに書いていた。これに加えて、setup.sというソースファイルを別に用意する。この後機能拡張をしていくときは、ipl.sではなくsetup.sの方にコードを追加していく。因みに、わざわざsetupなんて名前にしたのは、今後ここに色々な初期化処理を書くよという布石だ。
とりあえず最初は無限ループで止まるようにしておけばいいだろう。
# setup.s .code16 begin: jmp begin
リンカスクリプトを書く
setup.oは、今まで書いたIPLのすぐ後ろに追加したい。そのためには、リンカ(GNU ld)とリンカスクリプトというものを使ってオブジェクト・コードの配置を制御する必要がある。ついでにカーネル終端のアドレスもソースコードから参照できるようシンボルを定義しておく。
リンカスクリプトファイルは以下のような感じになる。拡張子は別に何でも良いようだがとりあえずlsとしておく。
/* ipl.ls */ SECTIONS { . = 0x1000; _kernel_begin = .; .ipl 0x1000 : { ipl.o; setup.o; } _kernel_end = .; _kernel_end_sector = ((_kernel_end - _kernel_begin) + 511) / 512; }
見て分かる通り「SECTIONS」という部分に色々書くことで生成されるオブジェクト=実行ファイルの構成を変えることができる。
最初の「. = 0x1000」という変な行は、現在位置(.)を0x1000番地に指定している。これを指定することで、プログラムの開始アドレスがメモリの0x1000番地にあるものとしてプログラムが生成される。が、以前に書いたとおり実際には起動時は0x7c00から始まるので、0x1000にコードを移動する処理が必要となる。
次の「_kernel_begin = .」は、_kernel_beginというシンボルに現在位置(0x1000)を指定して定義するという行だ。これでアセンブラからラベルと同じように_kernel_beginが使えるようになる。
「.ipl」と書かれている部分はセクションと呼ばれるもので、プログラムコードのセクションだったりデータのセクションだったりする。今回はプログラムもデータも一緒になっている単一のセクションを使う。
ipl.oやsetup.oはセクションに取り込むオブジェクト・ファイルを指定している。ここで指定した順番に並べられる。つまり、ipl.sのコードの直後にsetup.sのコードが続くことになる。
「_kernel_end = .」もシンボル定義で、セクションが終わった後・プログラムの末尾を指すようになる。
「_kernel_end_sector」は、カーネルが何セクタあるかを定義している。_kernel_end - _kernel_beginで全体のサイズを得て、それをセクタサイズ(512)で切り上げつつ割っている。
IPLを移動させる。
開始アドレスを0x1000としてしまったので、それにあわせてコードを移動させたりスタックの開始位置を変えなければならない。
ipl.sを編集する。
# ipl.s # constants boot_begin = 0x7c00 ipl_size = 0x200 fd_sector_per_track = 0x12 # generate real mode code .code16 jmp begin nop # 中略 # setup registers cld xorw %ax, %ax movw %ax, %ss movw %ax, %es movw %ax, %fs # setup stack pointer movw $_kernel_begin, %sp # move ipl to kernel begin xor %ax, %ax # source movw %ax, %ds movw $boot_begin, %si #dest movw %ax, %es movw $_kernel_begin, %di # count movw $(ipl_size / 2), %cx # move rep movsw # setup code segment ljmp $0x0000, $set_cs set_cs: # show message movw $BOOT_MSG, %si call print # 中略 # boot signature. . = 510 .short 0xaa55 ipl_end:
主な変更点は以下の通り。
- 「boot_begin」と「ipl_size」という定数を追加した。それぞれ起動時のIPLのアドレスとサイズ。
- 「setup stack pointer」でスタックボトムを_kernel_beginにした。ssは「setup registers」で0x0000に設定。
- 「move ipl to kernel begin」以下でIPL全体を_kernel_begin以降に移動させた。
- 「setup code segment」でcsに0を入れるようにした。(set_csラベルは既に_kernel_beginを基準としたアドレスになっている)
- 「ipl_end」というラベルを定義した。ディスクから読み込んだコードはこれ以降に配置される。
の2点だ。
この時点でちゃんと実行できるか試して見た方がいい。メッセージが出なくなったりしたらどこか間違っている。
ディスクアクセス関数を書く
上記コードでちゃんと動くようになったら、いよいよsetup.sのコードをディスクから読み込むようにする。そのために、ディスクからの読み込み関数を作る。
さて、前回説明したようにディスクからの読み込みには色々面倒なパラメータがある。セクタ・ヘッド・トラックなどなどだ。こんなのを毎回指定していたのでは面倒極まりない。
そこで、ディスクをフラットなセクタの集まりとしてアクセスできるように関数を作ってしまうことにする。ディスクが合計2880セクタある場合、0〜2879をレジスタで指定することでその番号に該当するセクタが読み込まれるようにするのだ。関数を呼び出す場合はセクタの番号とコピー先アドレスだけを指定すれば良いことにする。
で、またいきなりコード。
# reset disk system # params: # dl = drive no # returns: # al = error code. 0 if succeeded. reset_disk: xor %al, %al xor %ah, %ah int $0x13 ret # read disk sector # params: # es:di = dest buffer address # si = source sector no (0 - 2879) # dl = drive no # returns: # al = error code. 0 if succeeded. read_disk_sector: # save drive no pushw %dx xorw %dx, %dx movw %si, %ax # logical sector to track no movb $fd_sector_per_track, %dl divb %dl # track no movb %al, %ch shrb $0x01, %ch # head no jnc head_0 mov $0x01, %dh head_0: # sector no movb %ah, %cl incb %cl # dest buffer movw %di, %bx # drive no popw %ax movb %al, %dl # sector count movb $0x01, %al # read command movb $0x02, %ah # execute int $0x13 ret
「reset_disk」ではディスク・システムのリセットを行う。起動時に1回だけ呼ぶので別関数にしてある。
「read_disk_sector」が今回のキモとなるディスク読み込み関数だ。これは以下の処理を順に行っている。
- 引数に指定されたドライブ番号をスタックに退避する。
- コピー元のセクタ番号をaxレジスタに移す。
- セクタ番号を1トラック当たりのセクタ数で割り(divb)、トラック番号を得る。余りも同時に得られる。余りは、トラック内でのセクタ番号(ヘッド跨ぎ含む)となる。
- トラック番号をchレジスタに移す。
- トラック番号で割った余りを1ビット右シフト(つまり2で割る)し、ヘッド内でのセクタ番号を得る。
- トラック内でのセクタ番号が偶数かどうか(jnc)によりヘッド番号を得る。
- セクタ番号をclレジスタに移す。セクタ番号は1から始まるので同時にインクリメントも行う。
- コピー先バッファを指定する。
- ドライブ番号をスタックから元に戻す。
- 1セクタ読み込むよう指定する。
- 読み込みコマンドを指定する。
- 読み込み処理を行う。
divb命令についてはIntelのリファレンス参照。加減算とかとはだいぶ動きが違う。余りが同時に求められたりするので、ある意味C言語の除算より便利(笑)。
上記コードを「boot signature」より前のどこかに書き込むこと。後に書いてしまうと512バイト以降になってしまってコード自体が読み込まれない(汗)。
起動ドライブを得る
上記関数を使ってディスクから読み込んで行くわけだが、1つの疑問が生じる。
ドライブ番号って、何。
いや当然、ドライブが複数あった場合、起動ディスクの入っているドライブ番号を指定して読み込まなければならないわけだが、その起動ディスクの入っているドライブってどうやって知るんだろうか。
実は、今まで黙っていたが(またかよ)、起動時にdlレジスタの値として渡される。
なので、適当な場所にデータ領域を用意して起動時にコピーするようにすれば後から参照できる。
# 前略 # setup registers cld xorw %ax, %ax movw %ax, %sp movw %ax, %es movw %ax, %fs movb %dl, boot_drive # save boot drive # 中略 BOOT_MSG: .string "Hello,World!\r\n" boot_drive: .byte 0x00 # 後略
読み込み処理を書く
いよいよ本チャンの読み込み処理を書く。流れは以下の通り。
- ディスクシステムのリセット。(reset_disk)
- 成功メッセージ表示。
- 読み込み元セクタ・読み込み先バッファを指定する。
- ディスクからの読み込み。(read_disk_sector)
- セクタ・バッファを進める。
- 全セクタを読み終えたら終了。
- 成功メッセージ表示。
で、コードは以下の通り。
#前略 fd_sector_length = 512 kernel_begin_sector = 0x01 read_kernel_es_offset = 0x1000 # 中略 # show message movw $BOOT_MSG, %si call print # reset disk drive movb %dl, boot_drive call reset_disk jc error # success movw $DISK_RESET_MSG, %si call print
ここまででディスク・システムのリセットが終わる。続いてディスクからの読み込み処理を行う。
# read kernel image from disk pushw $0x00 popw %es xorw %di, %di movw $ipl_end, %di movb boot_drive, %dl movw $kernel_begin_sector, %si read_kernel: call read_disk_sector jc error addw $fd_sector_length, %di jnc advance_read_kernel_sector movw %es, %ax addw $read_kernel_es_offset, %ax movw %ax, %es advance_read_kernel_sector: incw %si cmpw $_kernel_end_sector, %si jb read_kernel # success movw $DISK_READ_MSG, %si call print # jump to setup jmp ipl_end # error error: movw $ERROR_MSG, %si call print error_end: jmp error_end # 中略 BOOT_MSG: .string "Hello,World!\r\n" DISK_RESET_MSG: .string "Success reset disk.\r\n" DISK_READ_MSG: .string "Success read disk.\r\n" ERROR_MSG: .string "Disk error!\r\n" boot_drive: .byte 0x00 # 後略
ディスクからの読み込み処理(read_kernel以下)では、read_disk_sectorを呼び、成功したらコピー先を指すdiをセクタ分進め、もしdiがオーバーフローしたらesを進めるという処理を行っている。advance_read_kernel_sector以下では全セクタ読み込んだかチェックし、まだ読み込み終えていなければ引き続きread_kernelを実行するようにしている。
ビルドする
$ as -o ipl.o ipl.s $ as -o setup.o setup.s $ ld -T ipl.ls -o kernel.exe ipl.o setup.o $ objcopy -S -O binary kernel.exe kernel.bin $ dd if=/dev/zero of=fd.img count=2880 $ dd if=kernel.bin of=fd.img conv=notrunc $ ./qemu/qemu.exe -L ./qemu -m 128 -fda fd.img
IPLのサイズを越えるので、もうカッコつけてkernelという名前にしてしまった。