自作OSに挑戦する日記 3日目

 「30日でできる!OS自作入門」を読んで分かったことや、とりあえず書いておきたいことなどを書いていきます。
 この本はChapterが1から30まであるので、各チャプター毎に1記事書いていきます。

Chapter 3 「32ビットモード突入とC言語導入」

今回やった内容

  • ディスクの仕組みについての勉強
  • ディスクの内容を読み込んでみる
  • ディスク上の機械語を実行する
  • IPL完成
  • 32ビットモード切り替え
  • C言語導入

環境

作業記録

 今回で、IPL(初期プログラムローダ)を完成させます。 今まで作っていたやつは本物じゃないらしい…。他のプログラムをロードしてこそIPL。
 まずはディスクの仕組みについて勉強。本の説明はフロッピーディスクについてです(ちょっと内容が古い)。ディスクの仕組みのほか、バッファアドレスやメモリの番地指定の方法についても学びました。


 勉強が終わったところで、実際にディスクの中身を読み込むようなアセンブリを書きます。

 エラーの時はエラーメッセージが出るのですが、成功時に何も出ないのは悲しいということで、自分でちょっと改造してみました。

 自分で考えて書いたコードがちゃんと動くことに感動…。アセンブリについてちゃんと理解できている証拠です、えらい。


 さっき書いたディスクの中身を読み込むようなアセンブリは、1セクタしか読み込まないようになっているので、10シリンダ・2ヘッダ・18セクタ分読めるようにしていきます。やっている内容は、ループしてレジスタの値を変えながらディスク関数を呼んでいるだけです。ループが入ると急にプログラムっぽくなりますね、楽しい。
 完成コードがこれです。10 x 2 x 18 x 512(1セクタ=512バイト) = 184320バイト=180KB分、ディスクから読み込めるようになりました。

// ipl.asm 

; haribote-ipl
; TAB=4

CYLS    EQU     10              ; 何シリンダディスクからOSを読み込むかを設定する定数

        ORG     0x7c00          ; このプログラムがどこに読み込まれるのか

; 以下は標準的なFAT12ディスクのための記述

        JMP     entry
        DB      0x90
        DB      "HARIBOTE"      ; ブートセクタの名前を自由に書いてよい(8バイト)
        DW      512             ; 1セクタの大きさ(512にしなければいけない)
        DB      1               ; クラスタの大きさ(1セクタにしなければいけない)
        DW      1               ; FATがどこから始まるか(普通は1セクタ目からにする)
        DB      2               ; FATの個数(2にしなければいけない)
        DW      224             ; ルートディレクトリ領域の大きさ(普通は224エントリにする)
        DW      2880            ; このドライブの大きさ(2880セクタにしなければいけない)
        DB      0xf0            ; メディアのタイプ(0xf0にしなければいけない)
        DW      9               ; FAT領域の長さ(9セクタにしなければいけない)
        DW      18              ; 1トラックにいくつのセクタがあるか(18にしなければいけない)
        DW      2               ; ヘッドの数(2にしなければいけない)
        DD      0               ; パーティションを使ってないのでここは必ず0
        DD      2880            ; このドライブ大きさをもう一度書く
        DB      0,0,0x29        ; よくわからないけどこの値にしておくといいらしい
        DD      0xffffffff      ; たぶんボリュームシリアル番号
        DB      "HARIBOTEOS "   ; ディスクの名前(11バイト)
        DB      "FAT12   "      ; フォーマットの名前(8バイト)
        RESB    18              ; とりあえず18バイトあけておく

; プログラム本体

entry:
        MOV     AX,0            ; レジスタ初期化
        MOV     SS,AX
        MOV     SP,0x7c00
        MOV     DS,AX

; ディスクを読む
        MOV     AX,0x0820
        MOV     ES,AX
        MOV     CH,0            ; シリンダ0
        MOV     DH,0            ; ヘッド0
        MOV     CL,2            ; セクタ2

readloop:
        MOV     SI, 0           ; 読み込み失敗回数カウンタ初期化

retry:
        MOV     AH, 0x02
        MOV     AL, 1           ; 1セクタだけ読み込む
        MOV     BX, 0
        MOV     DL, 0x00
        INT     0x13            ; ディスクBIOS呼び出し
        JNC     next            ; エラーがなければnextへ
        ADD     SI, 1           ; 失敗カウンタ+1
        CMP     SI, 5           ; 失敗回数が5なら…エラー表示
        JAE     error
        MOV     AH, 0x00
        MOV     DL, 0x00
        INT     0x13            ; システムリセット
        JMP     retry           ; もう一度読み込みに挑戦する

next:
        MOV     AX, ES
        ADD     AX, 0x0020      ; アドレスを0x200進める
        MOV     ES, AX          ; ADD ES, 0x200という命令がないのでAXレジスタを経由している
        ADD     CL, 1           ; 1セクタ進める
        CMP     CL, 18
        JBE     readloop        ; 読み込みセクタが18以下ならreadloopへ
        MOV     CL, 1
        ADD     DH, 1
        CMP     DH, 2
        JB      readloop        ; 読み込みヘッダが<2ならreadloopへ
        MOV     DH, 0
        ADD     CH, 1
        CMP     CH, CYLS
        JB      readloop        ; 読み込みシリンダが<CYLS(定数)ならreadloopへ
        JMP     success

fin:
        HLT                     ; 何かあるまでCPUを停止させる
        JMP     fin             ; 無限ループ

; 読み込み成功メッセージがあるメモリのアドレスを記録
success:
        MOV     SI, successmsg
        JMP     putloop

; エラーメッセージがあるメモリのアドレスを記録
error:
        MOV     SI, errormsg

; 出力ループ
putloop:
        MOV     AL,[SI]
        ADD     SI,1            ; SIに1を足す
        CMP     AL,0
        JE      fin
        MOV     AH,0x0e         ; 一文字表示ファンクション
        MOV     BX,15           ; カラーコード
        INT     0x10            ; ビデオBIOS呼び出し
        JMP     putloop

; 読み込み成功メッセージ
successmsg:
        DB      0x0a, 0x0a      ; 改行2つ
        DB      "load some sector success"
        DB      0x0a            ; 改行
        DB      0

; 読み込み失敗メッセージ
errormsg:
        DB      0x0a, 0x0a      ; 改行を2つ
        DB      "load error"
        DB      0x0a            ; 改行
        DB      0

        RESB    0x7dfe-($-$$)-0x7c00        ; 0x7dfeまでを0x00で埋める命令

        DB      0x55, 0xaa

 ブートセクタ以外を読み込めるようになったので、OS本体をブートセクタ以外のディスク上に配置して、それを実行するということに挑戦します。とりあえず本に従って、HLTするだけのOSコードを用意しコンパイルします。そしてディスクイメージに保存しておきます。

 hariboteos.imgをバイナリエディタで開いて、OS本体が配置されているアドレスを調べます。

f:id:guguru0014:20190117232209p:plain

 ディスクイメージ内のアドレス0x5600にOS本体が配置されているみたいです。現在のプログラムだとブートセクタの先頭アドレスが0x8000となるなので、この2つを足し合わせることで実際にPCのメモリ上で展開されたときのアドレスを求めることができます。

[追記 : 2019/1/18]
 ディスクイメージに保存するやり方が間違っていたらしく、0x5600に配置されたというのは間違いでした。正しくは0x4200に配置されます。なので、実際にPCのメモリ上で展開された時のアドレスは0xC200になります。

 以下のコードは、求めたアドレスを使って、実際にOS本体を実行するようにしたものです。

// ipl.asm

; haribote-ipl
; TAB=4

CYLS    EQU     10              ; 何シリンダディスクからOSを読み込むかを設定する定数

        ORG     0x7c00          ; このプログラムがどこに読み込まれるのか

; 以下は標準的なFAT12ディスクのための記述

        JMP     entry
        DB      0x90
        DB      "HARIBOTE"      ; ブートセクタの名前を自由に書いてよい(8バイト)
        DW      512             ; 1セクタの大きさ(512にしなければいけない)
        DB      1               ; クラスタの大きさ(1セクタにしなければいけない)
        DW      1               ; FATがどこから始まるか(普通は1セクタ目からにする)
        DB      2               ; FATの個数(2にしなければいけない)
        DW      224             ; ルートディレクトリ領域の大きさ(普通は224エントリにする)
        DW      2880            ; このドライブの大きさ(2880セクタにしなければいけない)
        DB      0xf0            ; メディアのタイプ(0xf0にしなければいけない)
        DW      9               ; FAT領域の長さ(9セクタにしなければいけない)
        DW      18              ; 1トラックにいくつのセクタがあるか(18にしなければいけない)
        DW      2               ; ヘッドの数(2にしなければいけない)
        DD      0               ; パーティションを使ってないのでここは必ず0
        DD      2880            ; このドライブ大きさをもう一度書く
        DB      0,0,0x29        ; よくわからないけどこの値にしておくといいらしい
        DD      0xffffffff      ; たぶんボリュームシリアル番号
        DB      "HARIBOTEOS "   ; ディスクの名前(11バイト)
        DB      "FAT12   "      ; フォーマットの名前(8バイト)
        RESB    18              ; とりあえず18バイトあけておく

; プログラム本体

entry:
        MOV     AX,0            ; レジスタ初期化
        MOV     SS,AX
        MOV     SP,0x7c00
        MOV     DS,AX

; ディスクを読む
        MOV     AX,0x0820
        MOV     ES,AX
        MOV     CH,0            ; シリンダ0
        MOV     DH,0            ; ヘッド0
        MOV     CL,2            ; セクタ2

readloop:
        MOV     SI, 0           ; 読み込み失敗回数カウンタ初期化

retry:
        MOV     AH, 0x02
        MOV     AL, 1           ; 1セクタだけ読み込む
        MOV     BX, 0
        MOV     DL, 0x00        ; USBはHDDとして認識される
        INT     0x13            ; ディスクBIOS呼び出し
        JNC     next            ; エラーがなければnextへ
        ADD     SI, 1           ; 失敗カウンタ+1
        CMP     SI, 5           ; 失敗回数が5なら…エラー表示
        JAE     error
        MOV     AH, 0x00
        MOV     DL, 0x00
        INT     0x13            ; システムリセット
        JMP     retry           ; もう一度読み込みに挑戦する

next:
        MOV     AX, ES
        ADD     AX, 0x0020      ; アドレスを0x200進める
        MOV     ES, AX          ; ADD ES, 0x200という命令がないのでAXレジスタを経由している
        ADD     CL, 1           ; 1セクタ進める
        CMP     CL, 18
        JBE     readloop        ; 読み込みセクタが18以下ならreadloopへ
        MOV     CL, 1
        ADD     DH, 1
        CMP     DH, 2
        JB      readloop        ; 読み込みヘッダが<2ならreadloopへ
        MOV     DH, 0
        ADD     CH, 1
        CMP     CH, CYLS
        JB      readloop        ; 読み込みシリンダが<CYLS(定数)ならreadloopへ

        MOV     [0x0ff0], CH
        JMP     0xC200          ; OS本体のアドレスまでジャンプ

; 終了、CPU待機
fin:
        HLT                     ; 何かあるまでCPUを停止させる
        JMP     fin             ; 無限ループ

; 読み込み成功メッセージがあるメモリのアドレスを記録
success:
        MOV     SI, successmsg
        JMP     putloop

; エラーメッセージがあるメモリのアドレスを記録
error:
        MOV     SI, errormsg

; 出力ループ
putloop:
        MOV     AL,[SI]
        ADD     SI,1            ; SIに1を足す
        CMP     AL,0
        JE      fin
        MOV     AH,0x0e         ; 一文字表示ファンクション
        MOV     BX,15           ; カラーコード
        INT     0x10            ; ビデオBIOS呼び出し
        JMP     putloop

; 読み込み成功メッセージ
successmsg:
        DB      0x0a, 0x0a      ; 改行2つ
        DB      "load some sector success"
        DB      0x0a            ; 改行
        DB      0

; 読み込み失敗メッセージ
errormsg:
        DB      0x0a, 0x0a      ; 改行を2つ
        DB      "load error"
        DB      0x0a            ; 改行
        DB      0

        RESB    0x7dfe-($-$$)-0x7c00        ; 0x7dfeまでを0x00で埋める命令

        DB      0x55, 0xaa

これでディスク上に配置したOS本体を実行できるようになったのですが、HLTを実行し続けるような処理だと画面上に変化がなく面白くないので、画面を真っ暗にするようなコードを付け加えておきます。

// hariboteos.asm

; haribote-os-body

    ORG     0xC200

    MOV     AL, 0x13
    MOV     AH, 0x00
    INT     0x10

fin:
    HLT
    JMP     fin

 できた2つのコードをコンパイルして、hariboteos.imgにharibote_hlt.imgを保存・実行すると…

f:id:guguru0014:20190118001012p:plain

ちゃんと画面が真っ暗になることが確認できました!!OS本体をちゃんと実行できているみたいです!


 3日目の目玉であるC言語の導入を行います。とりあえず、何もせずループするだけのCを書きます。

// bootpack.c

void HariMain(void){
fin:
    goto fin;
}

 3日目はここからが一番大変です。先ほど書いたアセンブリとCのファイルをそれぞれオブジェクトファイルへコンパイルしてリンクさせる、という作業を行うのですが、この部分は全て筆者の方の自作ツールを使うように書かれているのでwindows環境以外だと自分でやり方を探すしかありません。

 まず、アセンブリとCのコンパイル・リンクを行います。gccを使うとコマンド1つで終わるという記事があったのでありがたく使わせてもらいます。また、リンクを行う際にリンカファイルが必要なので、これも記事に書かれていたものを使わせてもらいます。

https://syusui.tumblr.com/post/110447016498/30日でできるos自作入門をlinuxでやってみる-32日目
syusui.tumblr.com

vanya.jp.net

 記事に書かれているようにコンパイルするコマンドを実行しようとしますが、macgccだと「-T」オプションが存在しないので怒られてしまいます。

 なので、macでも「-T」オプションが使えるようにクロスコンパイラのgccをビルドします。

qiita.com

 ビルドしたgccコンパイルをしてbootpack.hrbを生成します。そして、hariboteos.imgと結合すると完成です。

i386-elf-gcc -march=i486 -m32 -nostdlib -T har.ld -o bootpack.hrb bootpack.c
cat bootpack.hrb >> hariboteos.img

 実行すると先ほどと同じく真っ暗な画面になりました。ちゃんと出来ているみたいです。


 HLT命令はC言語から呼ぶ事が出来ないので、アセンブリでHLTを実行する関数を書き、それをCから呼ぶようにます。本に書かれているコードでは動かないので少し修正しました。

// naskfunc.asm

; nasmfunc

[BITS 32]               ; 32ビットモード用に機械語を生成する

; オブジェクトファイルのための情報
        GLOBAL  io_hlt     ; このプログラムに含まれる関数名


; 関数
[SECTION .text]
io_hlt:
        HLT
        RET
// bootpack.c

void io_hlt(void);

void HariMain(void){
fin:
    io_hlt();
    goto fin;

}

 nasmfunc.asmのオブジェクトファイルを生成して、bootpack.cとリンクしてコンパイルします。これで、nasmfunc.asmに書いたio_hlt関数がCからでも呼び出せるようになりました。

nasm -f elf -o nasmfunc.o nasmfunc.g
i386-elf-gcc -march=i486 -m32 -nostdlib -T har.ld -o bootpack.hrb -g bootpack.c nasmfunc.o

 最後は、いつも通りにhariboteos.imgを生成します。実行すると真っ暗な画面が出てきました、成功です!(たぶん…)  

まとめ

 IPL完成とC言語実行まで出来ました。内容の理解はすぐ出来たのですが、コンパイルやビルドに手間を取られ、終えるのに3日もかかってしまいました。もっと他のところで時間使いたいな〜。

おまけ

 3日目終了時点でのMakefileを貼っておきます。

other_disk.img : other_disk.asm Makefile
    nasm -f bin -o other_disk.img other_disk.asm

hariboteos.img : other_disk.img ipl10.asm Makefile
    nasm -f bin -o hariboteos.img ipl10.asm
    cat other_disk.img >> hariboteos.img

nasmfunc.o : nasmfunc.asm Makefile
    nasm -f elf -o nasmfunc.o nasmfunc.asm

hariboteos.sys : asmhead.asm bootpack.c har.ld Makefile
    nasm -f bin -o hariboteos.sys asmhead.asm
    i386-elf-gcc -march=i486 -m32 -nostdlib -T har.ld -o bootpack.hrb -g bootpack.c nasmfunc.o
    cat bootpack.hrb >> hariboteos.sys

img :
    make -r hariboteos.img
    make -r nasmfunc.o
    make -r hariboteos.sys
    hdid hariboteos.img
    cp ./hariboteos.sys /Volumes/HARIBOTEOS
    diskutil umount /dev/disk3
    diskutil eject /dev/disk3
    make clean

write :
    diskutil unMountDisk /dev/disk3
    sudo dd if=./hariboteos.img of=/dev/disk3

clean :
    rm *.sys
    rm *.o
    rm *.hrb
    rm other_disk.img