起動するまでの長い道のり IPL編(3) アセンブラ解説の巻

 ひたすら打つべし! 打つべし! だったアセンブラのソースについていい加減解説する。

ディレクティブ・コメント

 ここについて説明する。

# ipl.s

# generate real mode code
.code16

 「#」で始まる行はコメントだ。「#」から行の終りまでがコメントと見なされる。

「.code16」は、16ビットの実行コードを生成するよう指示するためのディレクティブ(指示語)だ。前に解説したとおり、起動直後のマシンは16ビットのリアルモードで動いている。その状態で実行できるコードを生成させるために、このディレクティブは必要となる。

ジャンプ・空の命令

 ここについて説明する。

    jmp begin
    nop
    
# 中略
    
    # boot begin
begin:

 マシン起動時、最初にこのjmp命令が実行される。(先述の.code16はアセンブラに指示を与えるディレクティブなので実行コードとしては現われない)

「jmp」は見たまんまbeginにジャンプする命令だ。「begin:」はラベルと呼ばれるもので、実行コード上の位置(アドレス)に名前を付ける。ジャンプに使うばかりではなく、データのアドレスを指定する場合などにも使う。

「nop」はNo operationの略で、何もしない。実行コードには何もしない命令が挿入される。どうして何もしない命令なんて必要になるかと言うと、色々理由がある。例えば速く実行したい処理の部分(ループ等)をメモリの速くアクセスできる位置までずらしたりするのに使ったりする。*1

BPB・データ定義

# BPB
name:           .ascii  "Name    "
sector_size:    .word   0x0200
cluster_size:   .byte   0x01
fat_pos:        .word   0x0001
fat_cnt:        .byte   0x02
root_size:      .word   0x00e0
sector_cnt:     .word   0x0b40
media_type:     .byte   0xf0
fat_size:       .word   0x0009
sector_cnt_pt:  .word   0x0012
head_cnt:       .word   0x0002
bpb_pos:        .long   0x0000
sector_cnt_l:   .long   0x00000b40
drive_no:       .byte   0x00
reserved:       .byte   0x00
ext_boot_code:  .byte   0x29
volume_serial:  .long   0xffffffff
disk_name:      .ascii  "DISK       "
fat_name:       .ascii  "FAT12   "

 ジャンプで飛ばしている箇所は、BPB(Bios Parameter Block)と呼ばれるデータ領域で、ディスクの物理的な情報(セクタ数やサイズ)を納めている。これはFAT12という規格で決まっている(らしい)ので、その通りデータが埋め込まれるよう書いた。

 それぞれの行はラベル・データ型・値という形で書かれている。ラベルは先ほど出てきたbeginラベルと同じくアドレスに名前を付ける。ただ今度のラベルは実行コード中の位置ではなく、データの在り処を示すためのものだ。

 .xxxxという形で書かれているディレクティブは、後に続くデータ値の型を示す。byte・word・longはそれぞれ1バイト・2バイト・4バイトの整数データであることを示す。asciiはASCII形式の文字列データであることを示す。ディレクティブの後には実際のデータ値が書かれる。

 データ型のディレクティブとデータ値をソースコード中に書くことで、その位置にデータ値が挿入され、アセンブルした後のオブジェクトファイルにも書き出される。単にデータを挿入したいだけならそれで良いが、後でデータにアクセスしたりアドレスを取る場合はラベルを貼っておく必要がある(ラベルを貼っても出力される実行コードそのものに変化はない)。今回は単に分かりやすくするためにラベルを貼った。

割り込み禁止・レジスタ初期化

    # deny interrupt
    cli
    
    # setup registers
    cld
    xorw    %ax, %ax
    movw    %ax, %ss
    movw    %ax, %es
    movw    %ax, %fs

# setup stack pointer
    movw    $0x7c00, %ax
    movw    %ax, %sp

cli」(CLear Interrupt flag)は割り込みを禁止するための命令だ。割り込みって何さ、という件についてはまたいつか詳しくやると思う。大雑把に言えばIOの入出力やプログラムのエラーが起きたとき、CPUが行っている処理に割り込んで別の処理を行わせるという仕組みだ。ただ、マシンを初期化している最中にこの割り込みが発生すると色々ヤヴァイことが起きる可能性があるので、ここでは禁止する。

「cld」(CLear Direction flag)はストリング命令の方向を設定するための命令だ。ストリング命令とは、文字列や配列みたいなメモリ上に連続したデータを扱う命令だ。cldを実行すると、連続したデータを先頭から順に処理するよう設定される。

 その後に続く「xorw」「movw」はそれぞれレジスタを使ってXOR計算と値のコピーを行う命令だ。XXXwのwの部分は扱うデータのサイズを表していて、この場合はWordつまり2バイトのデータを扱っている。カンマで区切られた%XXの部分は、命令で操作するレジスタを指している。

 レジスタとは、計算や処理のために一時的にデータを置いておく変数で、人に例えれば「手」のようなものだ。物を移動させたり、いじって形を変えたりするのに使う。人間の手が2本しかないのと同じく、レジスタも決められた数しかない。

「xorw %ax, %bx」とある場合、レジスタAXの値とレジスタBXの値をXORし、結果をBXに格納するという意味になる。C言語風に書けば「BX ^= AX」だ。ここではAX自身がXORされているので、AXの値はゼロになる。レジスタをゼロでクリアするときの慣用句らしい。「movw %ax, %bx」ならばレジスタAXの値をレジスタBXにコピーする(BX = AX)という命令になる。

セグメント設定

    # setup segment
    ljmp    $0x7c0, $set_cs
set_cs:
    movw    %cs, %ax
    movw    %ax, %ds

 ここではセグメントというものの設定を行っている。また新しい用語が出てきてしまった。困った。

 以前、リアルモードでは最大64KBまでしかメモリにアクセスできないと言った。実はそれは語弊がある言い方で、正確には最大64KBまでのメモリにしか連続してアクセスできない、というのが正しい。では64KBを超える部分はどうアクセスするのか。それにはこのセグメントというものを使う。

 セグメントとは、メモリ領域をある範囲で区切った領域のことだ。どうしてわざわざメモリ領域を区切るのかと言うと、プログラムのコードを置く領域とデータの領域を分けたり、OSが使う領域とユーザのプログラムが使う領域を分けたりするためだ。

 で、上記コードではメモリをどうセグメントに分けるかを指定している。「%ds」といったように末尾が「s」になっているレジスタは、セグメントレジスタと呼ばれるセグメントを設定するためのレジスタだ。

 セグメントレジスタは幾つかあり、それぞれに用途がある。ここで設定しているcsはコード用のセグメント・レジスタで、dsはデータ用のセグメント・レジスタだ。

 セグメントに値を設定すると、アドレスを指定してメモリアクセスするときにそのアドレスが変換される。具体的には以下のようになる。

アクセスされるアドレス = セグメントレジスタ値 × 16 + 指定アドレス

 ここではdsに0x7c0を指定しているので、あるアドレスXを指定してアクセスした場合は0x7c00 + Xにアクセスすることになる。指定アドレスはセグメント内におけるオフセット(差分)として解釈されるようになるのだ。このセグメントをもっと高位(0xffff = 0xffff0とか)に指定しなおすことで、64KBの壁は越えられることになる。

 で、どうして0x7c0(つまり実際は0x7c00)なんていう変な値をセグメントに設定しているのか。実は、今まで黙っていたが、IPLのコードはメモリの0x7c00番地に置かれて実行される。そこで各セグメントが0x7c00から始まるように設定してやれば、後のコードは自分が0x7c00以降に置かれていると意識しないで書ける。後のljmp(Long JuMP)命令やdsレジスタの設定も、各セグメントが0x7c00から始まるようにしてやっているのだ。

 ljmpの部分だけ他と違うのでちょっと戸惑うかもしれない。これはジャンプ命令を利用してcsレジスタを設定している。csとはCode Segmentの略で、プログラムのコードのある領域を指す。逆に言えば、csを設定しなおすと実行されるコードが次の瞬間から変わる。だから普通のmovw命令等で設定してしまうと、その後にはソース上の次の命令ではなく、全然関係ないよく分からない領域のコードが実行されることになる。それは困るので、わざわざset_csというラベルを使って次の命令のアドレスを取り、ljmp命令でセグメントレジスタの値と一緒に指定する必要がある、のだと思う。

 最後のdsレジスタには、設定済みのcsレジスタの値をコピーしている。dsはData Segmentの略で、movwなどデータを参照するときのアドレス変換に用いられる。これを忘れると画面表示メッセージが崩れるので試してみよう。(私は忘れた)


 長くなったので以下次号(汗)。

*1:CPUの特性により、実行コードのあるメモリ領域のアドレスが4の倍数だったりすると速く実行できたりする。