Tsurugidake's diary

プログラミング学習の記録,備忘録

Linuxで書くOS自作入門 3日目(前半)

「30日でできる!OS自作入門」の2日目
環境はUbuntu, アセンブラはNASMで開発中
長くなりそうなのでharib00d, 10シリンダ読み込むプログラムまでをここに書きます.

書き換えた部分の概要

BPB(BIOS Parameter Block)の後にディスクをメモリに読み込むプログラムを書く.ディスク読み込みはINT命令によって実行される.

  • INT命令は割り込み(Interrupt)処理の呼び出しの命令であり,BIOSの機能を呼び出す.
  • そもそもBIOSBasic Input/Output Systemの略で,ハードウェアとの最も低レベルな入出力を行うプログラム. コンピュータのモデルごとに設計され,マザーボード上のメモリに埋め込まれている.

セクタごとにディスクを読み込んで行く.メモリのオフセットにはヘッド,シリンダ,セクタのサイズを考える必要があるためメモ.
(下記はフロッピーディスクの場合)

  • 1ディスク = 2ヘッド = 160シリンダ = 2,880セクタ = 1,474,560バイト
  • 1ヘッド = 80シリンダ = 1,440セクタ = 737,280バイト
  • 1シリンダ = 18セクタ = 9,216バイト
  • 1セクタ = 512バイト

読み込みループ部分

    CYLS  EQU      10             ; 読み込むシリンダ数

EQUは定数を定義できる.アセンブラが後で数字に書き換えてくれる.

BPB部分は前回と同じなので省略.

entry:
        MOV      AX,0        ; レジスタ初期化
        MOV      SS,AX
        MOV      SP,0x7c00   ; ブートセクタは0x00007c00に読み込まれる
        MOV      DS,AX

; ディスクを読む

        MOV      AX,0x0820
        MOV      ES,AX         ; セグメントレジスタ(ES:BX)なのでメモリ上では0x8200
        MOV      CH,0        ; シリンダ番号
        MOV      DH,0        ; ヘッド番号
        MOV      CL,2            ; セクタ番号
readloop:
        MOV      SI,0        ; エラー回数リセット
retry:
        MOV      AH,0x02         ; AH=0x02 : ディスク読み込み
        MOV      AL,1            ; 処理セクタ数
        MOV      BX,0        ; バッファアドレス
        MOV      DL,0x00         ; ドライブ番号 0->Aドライブ
        INT      0x13           ; ディスクBIOS呼び出し
        JNC      next         ; エラーがおきなければnextへ
        ADD      SI,1            ; SI(エラー回数)に1を足す
        CMP      SI,5            ; SIと5を比較
        JAE      error            ; エラー回数5回以上 だったらerrorへ
        MOV      AH,0x00
        MOV      DL,0x00         ; ドライブ番号 0->Aドライブ
        INT      0x13           ; ドライブのリセット
        JMP      retry
next:
        MOV      AX,ES         ; アドレスを0x200進める 0x200は1セクタ分
        ADD      AX,0x0020       ; セグメントレジスタ(ES:BX)なので0x200->0x20
        MOV      ES,AX         ; ADD ES,0x020 という命令がないのでこうしている
        ADD      CL,1            ; CLに1を足す
        CMP      CL,18           ; CLと18を比較
        JBE      readloop     ; CL(セクタ番号) <= 18 だったらreadloopへ
        MOV      CL,1            ; CLを1に戻す
        ADD      DH,1
        CMP      DH,2
        JB       readloop     ; DH(ヘッド番号) < 2 だったらreadloopへ
        MOV      DH,0
        ADD      CH,1
        CMP      CH,CYLS
        JB       readloop     ; CH(シリンダ番号) < CYLS だったらreadloopへ
        JMP      success

INT命令を呼び出す前に,各レジスタの値を設定する必要がある.(AT)BIOS - os-wiki
ヘッド,シリンダ,セクタの値を指定することでディスクの読み込み部分は確定する. メモリ上への読み込み部分はセグメントレジスタ[ES:BX]を指定する.実際のメモリアドレスはES * 16 + BXとなるので,ESに代入するときは16進数だと下一桁の0が無くなるので注意する.
読み込みのエラーが起こる場合があるので,エラー回数をレジスタSIでカウントし,5回までならやり直すように設定した.(QEMUでエミュレートするだけなら特に意味はない) 後は,セクタ,シリンダ,ヘッドの順に,比較命令とジャンプ命令の組み合わせによって繰り返しをプログラムする.

終了処理

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

終了時のプログラムは前回と同じ.

本誌では読み込み成功時には何も表示されないプログラムとなっているが,本当に読み込めているか不安になってしまうため,オリジナルで成功時のメッセージを表示するように書き換えた.

success:
        MOV      SI,msg2
        JMP      putloop

error:
        MOV      SI,msg

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

msg:
        DB       0x0a, 0x0a        ; 改行を2つ
        DB       "load error"
        DB       0x0a           ; 改行
        DB       0

msg2:
        DB       0x0a, 0x0a        ; 改行を2つ
        DB       "load success"
        DB       0x0a           ; 改行
        DB       0

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

        DB       0x55, 0xaa    ;ブートセクタが有効であることを示す2バイト

実行

従来のMakefileで実行すると失敗した.

f:id:tsurugidake:20170826001953p:plain:w400

QEMUのオプションでフロッピーディスクのブートを明示してないことが原因だった.原因の特定にかなりの時間を消費した.

    qemu-system-i386 -fda haribote.img    # -fdaオプションをつける

f:id:tsurugidake:20170826002531p:plain:w400

今度はちゃんと成功表示が出た.

Linuxで書くOS自作入門 2日目

「30日でできる!OS自作入門」の2日目
環境はUbuntu, アセンブラはNASMで開発中

アセンブリの更新

  • プログラム本体部分が意味を持つ命令で置き換えられた.
  • ラベルが追加された.
  • レジスタ名が記述されるようになった.
  • 新しいアセンブリ命令
    • ORG 機械語をメモリ中の指定された部分に読み込む origin
    • JMP 指定のラベルやアドレスにジャンプする jump
    • MOV 代入文 レジスタやアドレス,即値を指定可能 MOV AX,0でAXに0を代入 move
  • 今まで空き部分を0で埋めるために使用していたRESB命令は警告が出るので,TIMES命令によって置き換えることにした.
    • TIMES 指定回数直後の命令を繰り返す
      RESB 18TIMES 18 DB 0

Makefileの作成

Win版のMakefileをもとに,Linuxで動作するように書き換えた.本では,独自ツールedimg.exeなどが登場するが,これらに頼らずに自分でディスクイメージを作成することにした.とはいっても今日の範囲ではブートセクタの部分とそれ以外の部分をcatで結合しているだけである.

ここでは,ファイル名を変更し,ブートセクタのアセンブリipl.asm,それ以外のアセンブリtail.asmとしている.

default  :
  make img

ipl.bin : ipl.asm Makefile
  nasm ipl.asm -o ipl.bin -l ipl.lst

tail.bin : tail.asm Makefile
  nasm tail.asm -o tail.bin -l tail.lst

helloos.img : ipl.bin tail.bin Makefile
  cat ipl.bin tail.bin > helloos.img

asm :
  make -r ipl.bin

img :
  make -r helloos.img

run :
  make img
  qemu-system-i386 helloos.img

clean :
   -rm ipl.bin
   -rm tail.bin
   -rm ipl.lst
   -rm tail.lst

src_only :
  make clean
  rm helloos.img

ブートセクタの簡単な解釈

さて,ブートセクタ部分に関してはアセンブリが読めるようになってきたため,ここでアセンブリが何をしているのか読んで見る.その前に用語をいったん整理する.

  • ブートセクタ ディスクの最初のセクタ.そもそもセクタはディスクの読み書きの最小単位であり,512バイトである.最初にここを読み込むことによって,OSは起動する.
  • FAT12フォーマット フロッピーディスクのフォーマット形式
     ORG      0x7c00

機械語翻訳はされない,アセンブラのための命令.ブートセクタは0x00007c00 - 0x00007dffに読み込まれるので,これを明示化している1.絶対アドレスを正しく計算するために使っているのだと思う.

     JMP      entry
        DB       0x90
        DB       "HELLOIPL"     ; ブートセクタの名前を自由に書いてよい(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       "HELLO-OS   "   ; ディスクの名前(11バイト)
        DB       "FAT12   "     ; フォーマットの名前(8バイト)
        TIMES    18 DB 0            ; とりあえず18バイトあけておく

FAT12フォーマットフロッピーディスクのための記述であり,BPB(BIOS Parameter Block)と呼ばれるらしい.FATボリュームの認識のための記述.明確に定められた部分が多い.
ここに詳しく書いてあった.ELM FATファイル システムのしくみと操作法

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

最初の命令,JMP entryによってジャンプして最初に実行されるこのプログラムをブートストラッププログラムと呼ぶ.最初はレジスタの初期化が行われている.特筆すべきはスタックポインタSPはブートセクタの開始位置0x7c00に,ソースインデックスSIは表示文字列を指すラベルmsgが代入されている.

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

ソースインデックスSIの指すアドレス部分をALに読み込み,0x0eをAHに読み込んだ後,INT 0x10命令を使うことによって文字が表示される.文字列が終わるってALがゼロになるまでループしている.
ここに詳しく書いてあった.(AT)BIOS - os-wiki

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

HLTは次の外部割り込みが発生するまでCPUを停止させる命令でこれを無限ループしている.これ無しで実行するとCPUのファンがうるさくなった.

msg:
        DB       0x0a, 0x0a        ; 改行を2つ
        DB       "Hello, world! "
        DB       0x0a           ; 改行
        DB       0

メッセージ部分.なんでも書ける.

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

ブートセクタの最後0x7dffから最後の2バイトを除いた0x7dfeまでを0で埋める.

     DB       0x55, 0xaa

最後の2バイトをこれにすることによって,有効なブートセクタであることを示す.

感想

Makefile初めて書いたけど便利.
ブートセクタのプログラムについてはざっくり分かったかな.

Linuxで書くOS自作入門 1日目

低レイヤの勉強として,「30日でできる!OS自作入門」を購入した.

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

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

せっかくなので本で紹介されているWindowsではなく,Linux(Xubuntu16.04)を使ってOSを書いてみようと思う.

バイナリ写経

イメージファイルのバイナリ写経する.Linuxにはbviとかghexといったバイナリエディタがあるので使ってみたが退屈なので完成物をコピーしてきた. PCエミュレータQEMUは簡単にインストールできた.

sudo apt-get install qemu
qemu-system-i386 helloos.img

f:id:tsurugidake:20170811104117p:plain:w400

ひとまず動作確認.

アセンブラ

先ほどと同じバイナリを生成させるためにアセンブラを用いる.この本では独自のアセンブラNASMが用いられているが,Linuxで使用できる標準的なものを使いたい.
調べてみると,NASKの元となっているNASMの他に,GNUアセンブラGasなどが主流であることが分かった.NASMはIntel構文,GasはAT&T構文といった構文の違いがあるらしい.1
移植の作業量が少ないため,ひとまずはNASMを用いていくことにした.

しばらくは同じバイナリを生成するアセンブリなので,helloos1を飛ばし,helloos2をコンパイルする.

  • 新しいアセンブリ命令
    • DB 指定の1バイト記入 文字列も可 Data Byte
    • DW 指定の2バイト記入 Data Word
    • DD 指定の4バイト記入 Data Double-Word
    • RESB 指定バイト数0で初期化 REServe Bytes

nasmのインストール

sudo apt-get install nasm

アセンブル

nasm helloos.asm helloos.img

実行すると,helloos.asm:41: error: invalid operand typeというエラー.

調べてみると,RESB 0x1fe-($-$$)とすべきようだ.2
nasmでは,$はその行のアドレス,$$はそのセクションの最初のアドレスを指す.($-$$)とすることでセクション内のアドレスを求められるようだが,なぜこれが必要なのか,そもそも0x1feの指すアドレスがどこなのか,1日目の分では分からないのでひとまず読み進めていくことにした.

せっかくなので表示文字列を変えてコンパイル,リンク f:id:tsurugidake:20170812204513p:plain:w400

今度は成功.


余談1

アセンブル時の命令を

nasm helloos.asm helloos.img -l helloos.lst

と付け加えることで,
f:id:tsurugidake:20170812205255p:plain:w400
アセンブリに対するバイナリの対応を見ることができる.そのうち便利になりそう.

余談2

OSはおろか,アセンブリにもブログにも慣れていないため非常に時間がかかってしまった.