いものやま。

雑多な知識の寄せ集め

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

前回からだいぶ間が空いてしまったけど、続き。

前回はディレクティブを使ってただデータを作っただけだったけど、それではアセンブラを使ったとは言い難いので、今回はちゃんとしたアセンブラを書いてみる。

ちゃんとしたアセンブラのコード

と言うことで早速、ちゃんとしたアセンブラのコード。

    .file   "hello-os.s"

    // 16-bit code
    .code16

    // partition boot record (1sector)
pbr:
    // jump to boot code
    jmp     entry
    nop

    // OEM name (8byte)
    .ascii  "HELLOIPL"

    // BIOS Parameter Block (FAT12/FAT16)
    .short  512                 // bytes per sector
    .byte   1                   // sectors per cluster
    .short  1                   // reserved sectors
    .byte   2                   // number of file allocation table
    .short  224                 // root directory entries
                                // (32byte/entry * 224entry = 7168byte = 512byte/sector * 14sector)
    .short  2880                // total sectors
    .byte   0xf0                // media type (0xf0: floppy disk, 0xf8: hard disk)
    .short  9                   // sectors per file allocation table
    .short  18                  // sectors per track
    .short  2                   // number of heads
    .int    0                   // hidden sectors
    .int    2880                // large total sectors
    .byte   0x00                // physical disk number
    .byte   0x00                // current head
    .byte   0x29                // extended boot signature (0x29 means DOS 4.0 EBPB)
    .int    0xffffffff          // volume serial number
    .ascii  "HELLO-OS   "       // volume label (11byte)
    .ascii  "FAT12   "          // file sytem type (8byte)

    .skip 18, 0x00

entry:
    mov     $0, %ax
    mov     %ax, %ss
    mov     $0x7c00, %sp
    mov     %ax, %ds
    mov     %ax, %es
    mov     $message, %si
putloop:
    movb    (%si), %al
    add     $1, %si
    cmp     $0, %al
    je      fin
    mov     $0x0e, %ah
    mov     $15, %bx
    int     $0x10
    jmp     putloop
fin:
    hlt
    jmp     fin

message:
    .string "\n\nhello, world\n"

    .org    0x0200 - 0x02
    .byte   0x55, 0xaa  // boot signature

    // file allocation table (9sectors = 512byte/sector * 9sector = 4608byte)
fat:
    .byte   0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00
    .skip   0x200 * 9 - 8, 0x00

    // file allocation table (copy)
fat_copy:
    .byte   0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00
    .skip   0x200 * 9 - 8, 0x00

    // root directory entries (14sectors = 512byte/sector * 14sector = 7168byte)
root_directories:
    .skip   0x200 * 14, 0x00

    // data area (2847sectors = 2880 - 1 - 9 * 2 - 14)
files:
    .skip   0x200 * 2847, 0x00

本のコードを参考にしてるけど、だいぶ違ってる。
というのも、まず本はNASMに似たnaskという著者作のアセンブラを使っていて、Intel構文だけど、自分はGNU asを使っているので、AT&T構文になっているから。
それに、いろいろ分からなかったことを調べた結果が盛り込まれているというのもある。

FATファイルシステム

まず、FATファイルシステムについてから。

フロッピーディスクはセクタと呼ばれる区画で区切られていて、セクタ単位でデータを読み書きする。
1つのセクタが512バイトで、フロッピーディスクの場合、2880セクタ用意されている。
このセクタがディスク上にどう並んでいるのかはあとで扱うとして、論理的にはセクタが配列のように一列に並んでいると考えておいていい。
そう考えたときに、このセクタの配列は、FATフォーマットではいくつかの領域に分けられる:

  • ブートセクタ
    PCが起動したときにBIOSによってメモリにロードされて実行されるセクタ
  • FAT(File Allocation Table)領域
    クラスタ(1つ以上のセクタの集まり)の情報を管理するための領域
  • ルートディレクトリ領域
    ルート直下のディレクトリの情報が置かれる領域
  • データ領域
    各ファイルの情報が置かれる領域

上のコードだと、以下のように分けられている:

pbr:

    // ブートセクタ

fat:

    // FAT領域

root_directories:

    // ルートディレクトリ領域

files:

    // データ領域

ブートセクタは1セクタ、FAT領域は1つのFile Allocation Tableが普通9セクタで、冗長化でコピーが用意されているので合計18セクタ、ルートディレクトリ領域はFAT12だと普通は14セクタ、そして、残りがデータ領域となるので、データ領域は2847セクタということになる。

Cでイメージを書いておくと、以下のような感じ:

// セクタは512バイトのデータ
typedef struct sector_ {
    unsigned char data[512];
} sector_t;

// フロッピーディスク
typedef union floppy_ {
    // 2880セクタからなる
    sector_t sectors[2880];

    // 領域
    struct {
        // ブートセクタ
        sector_t boot_sector;

        // FAT領域
        struct {
            // File Allocation Table
            sector_t fat[9];

            // File Allocation Table (copy)
            sector_t fat_copy[9];
        } fat_area;

        // ルートディレクトリ領域
        sector_t root_directories_area[14];

        // データ領域
        sector_t data_area[2847];
    } areas;
} floppy_t;

アセンブラのコードを見てみると、今のところちゃんとしたデータが置いてあるのはブートセクタだけで、他の領域は.skipを使ってほぼ0にしていることが分かると思う。
これは、今書いているのがブート時に読み込まれて実行されるプログラムで、他にファイルをディスク上に作っていないから。

ブートセクタ

さらに、ブートセクタを見てみる。

パーティションが1つの場合、ブートセクタはただ1つなんだけど、ハードディスクとかだと複数のパーティションに分かれている場合があって、その場合、各パーティションの1つめのセクタが論理的にブートセクタになる。
そこで、ディスクの先頭にあるブートセクタを、マスターブートレコードMBR)、各パーティションの先頭にあるブートセクタをパーティションブートレコード(PBR)と言ったりする。
フロッピーディスクの場合、パーティションは分けられていない(=パーティションは1つだけ存在する)ので、MBRがそのままPBRになっている。

そして、MBRとPBRでは微妙に構造が違っていて、MBRはセクタの最後の方にパーティションの情報を持つパーティションテーブルというのが置かれることになっている。
もちろん、フロッピーディスクの場合、パーティションテーブルは不要なので、構造的にはPBRになっている。
ということで、ラベルはmbrではなくpbrにしている。

    // partition boot record (1sector)
pbr:
    ...

PBR(およびMBR)は、以下のような構造になっている:

  • ジャンプコード (3byte)
    以下のデータを飛び越して実際のブートコードに行くためのジャンプ命令+α
  • OEM (8byte)
    ブートセクタの名前とか
  • BIOSパラメータブロック
    ディスクの情報をまとめた領域
  • ブートコード
    ブート処理を行うコード
  • パーティションテーブル (64byte) ※MBRのみ
    パーティションの情報
  • ブートシグニチャ (2byte)
    このセクタがブートセクタであることを示す

まずは、ジャンプコード。

    // jump to boot code
    jmp     entry
    nop

見ての通り、実際のブート処理を行うコード(entry以下)へジャンプを行なっている。
なお、ジャンプ命令には2byteのものと3byteのものがあるので、データの位置を揃えるために、2byteのジャンプ命令の後にはnop命令を入れている。
(本だとニーモニックnopとは書かず、直接機械語DB 0x90と書いている)

次はOEM名で、特に書くことなし。

    // OEM name (8byte)
    .ascii  "HELLOIPL"

その次がBIOSパラメータブロック。

    // BIOS Parameter Block (FAT12/FAT16)
    .short  512                 // bytes per sector
    .byte   1                   // sectors per cluster
    .short  1                   // reserved sectors
    .byte   2                   // number of file allocation table
    .short  224                 // root directory entries
                                // (32byte/entry * 224entry = 7168byte = 512byte/sector * 14sector)
    .short  2880                // total sectors
    .byte   0xf0                // media type (0xf0: floppy disk, 0xf8: hard disk)
    .short  9                   // sectors per file allocation table
    .short  18                  // sectors per track
    .short  2                   // number of heads
    .int    0                   // hidden sectors
    .int    2880                // large total sectors
    .byte   0x00                // physical disk number
    .byte   0x00                // current head
    .byte   0x29                // extended boot signature (0x29 means DOS 4.0 EBPB)
    .int    0xffffffff          // volume serial number
    .ascii  "HELLO-OS   "       // volume label (11byte)
    .ascii  "FAT12   "          // file sytem type (8byte)

ここが正直大変だったところで、本だと「その値にしておくものだから」で片付けられている値がけっこうある(^^;
しかも、ネットで調べても仕様がハッキリしない記述が多くて、かなり困った。

以下のwikipedia(en)の記述が結構役に立った感じ:

BIOS parameter block - Wikipedia

ここから18byteほど隙間を空けて、適当にアラインを揃えたところから、実際のブートコードは始まっている。
その詳細はまた後で。

そして、ブートセクタの一番最後にあるのが、ブートシグニチャ

    .org    0x0200 - 0x02
    .byte   0x55, 0xaa  // boot signature

ブートセクタの最後の方までスキップさせるために、.orgを使っている。
この擬似命令は、ファイルの先頭のアドレスを0として、引数で指定したアドレスまでスキップをしてくれる。
ということで、1セクタが512(= 0x200)バイトなので、そこからブートシグニチャの2バイトを引いたところまで、一気にスキップ。
そして、ブートシグニチャ0x55, 0xaaを書き込んでいる。
BIOSはこのブートシグニチャが入っていないセクタはブートセクタとして扱わないらしい。

さて、あとは肝心のブートコードの部分だけど、けっこう長くなったので、一旦区切り。

今日はここまで!

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

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