いものやま。

雑多な知識の寄せ集め

『OS自作入門』を読んでみた。(その4)

またまた間が空いてしまったけど、前回の続き。

今日は実際のブートコードの部分を見ていく。

Intel構文とAT&T構文

前回も少し触れた通り、本はNASMに似た筆者作のアセンブラを使っていて、その構文はIntel構文になっている。
一方、自分の使っているGNU asは、AT&T構文で、いろいろ違ってくる。
詳細は以下のページなどを参照:

Linux のアセンブラー: GAS と NASM を比較する

基本的なところを押さえておくと、以下のとおり:

  • オペランドのディスティネーションとソースが逆
    • NASMはディスティネーション、ソースの順
    • GNU asは、ソース、ディスティネーションの順
  • 即値の指定の仕方が違う
    • NASMは数字やラベルをそのまま書く
    • GNU asは数字やラベルの前に$をつける
  • レジスタの指定の仕方が違う
  • 数字やラベルによるメモリの参照の仕方が違う
    • NASMは数字やラベルを[]で囲う
      バイト幅の指定が必要な場合、[]の前にbyte(1-byte)、word(2-byte)、dword(4-byte)をつける
      なお、NASMは数字やラベルの前にbyte ptr(1-byte)、word ptr(2-byte)、dword ptr(4-byte)をつけるのでもOKっぽい(※)
    • GNU asは数字やラベルをそのまま書き、バイト幅を示すためにオペコードにb(1-byte)、w(2-byte)、l(4-byte)をつける
  • レジスタによるメモリの参照の仕方が違う
    • NASMは
      [セグメントレジスタ:ベースレジスタ + インデックスレジスタ * スケーラ + オフセット]
      とする(使わないところは省略可能)
      (セグメントレジスタ x 16 + ベースレジスタ + インデックスレジスタ x スケーラ + オフセット)を参照することになる
    • GNU asは
      セグメントレジスタ:オフセット(ベースレジスタ, インデックスレジスタ, スケーラ) とする(使わないところは省略可能)
      (セグメントレジスタ x 16 + ベースレジスタ + インデックスレジスタ x スケーラ + オフセット)を参照することになる

※上記のIBMのページだとこう書かれているけど、なんか怪しい・・・

なお、NASMに関してと、レジスタによるメモリの参照は、ネット上の資料を漁っただけなので、正しくないかも・・・
(しっかりした資料が見つかってないので、自信がない・・・ちなみに、これを調べてたせいで更新が遅くなった)

16ビットモード

まず、x86のCPUには、16ビットモード(リアルモード)と32ビットモード(プロテクトモード)がある。
起動した直後は16ビットモードになっているので、アセンブラで出力されるコードも16ビットモード用のものになっていないと困る。

そこで、次のように、擬似命令を使ってこのコードが16ビットモード用のものであることを示してやる:

    // 16-bit code
    .code16

ブートコード

そして、前回見た通り、ジャンプコード、BIOSパラメータブロックが続いて、そのあとにブートコードが来ることになる。
ジャンプコードでは、このブートコードの先頭に飛んでくるようにジャンプを行なっている。

最初にやっているのは、レジスタの初期化。

entry:
    mov     $0, %ax
    mov     %ax, %ss
    mov     $0x7c00, %sp
    mov     %ax, %ds
    mov     %ax, %es
    // 続く

まずアキュムレータレジスタ(ax)を0で初期化し、さらにスタックのセグメントレジスタ(ss)、データのセグメントレジスタ(ds)、追加のセグメントレジスタ(es)をaxを使って0に初期化。
そして、スタックポインタ(sp)を0x7c00で初期化している。

ここからは文字列の出力。

まず、文字列が置かれているmessageというラベルのアドレスをソースインデックスレジスタ(si)に代入。

    // 続き
    mov     $message, %si
    // 続く

アドレスの内容ではなく、アドレスの値をそのまま入れるので、即値になるように$をつける必要があるのに注意。
(最初、つけるのを忘れて動かなかった)

そしたら、siの指す内容をaxの下位8ビット(al)に読み込む。

    // 続き
putloop:
    movb    (%si), %al
    // 続く

最初はsiの値はmessageのアドレスになっているので、改行文字¥nが読み込まれることになる。
そして、次々と読み込んで出力したいので、siの値を1つずつ増やしていくことになる。

    // 続き
    add     $1, %si
    // 続く

ここで、もし読み込んだ文字がヌル文字¥0だった場合、文字列の終端になっているので、ループを抜け出すことになる。

    // 続き
    cmp     $0, %al
    je      fin
    // 続く

あとは、実際に文字を出力する処理。
これにはBIOSAPIを利用する。

    // 続き
    mov     $0x0e, %ah
    mov     $15, %bx
    int     $0x10
    // 続く

決められたレジスタに値をセットして、ソフトウェア例外を起こしている(int命令)。
これで制御がBIOSに移って、文字を出力したらまた戻ってきてくれる。

最後に、次の文字を出力するために、ループの先頭に戻る。

    // 続き
    jmp     putloop
    // 続く

一つ一つ読み解くと、こんな感じ。

Cで書いてみると、次のようなイメージ:

char* si = message;
while (1)
{
    char al = *si;
    si++;
    if (al == '¥0')
    {
        break;
    }
    putchar(al);
}

文字列の出力が終わったら、あとは無限ループ。

    // 続き
fin:
    hlt
    jmp     fin

hlt命令はCPUを休止状態にする命令で、例外が起きるまでCPUを休ませておいてくれる。


さて、これでブートコードも見たわけだけど、実はこれではまだ正しく動かなかったり。
というのも、ブートセクタはBIOSによってメモリの0x7c00に読み込まれるのだけど、このコードだと先頭が0x0000だと思ってリンクが解決されてしまうから。
なので、アドレスが正しくなっていない。

このアドレスの問題を解決するには、もう一工夫必要になってくる。
それについては、また次で。

今日はここまで!

30日でできる! OS自作入門

30日でできる! OS自作入門