読者です 読者をやめる 読者になる 読者になる

yukke_longriders

ぼちぼちがんばる

LinuxでOS自作入門 3日目 (1)

OS自作

今回から1日分が長くなりそうなのでいくつかに分割します。

余談ですが、qemuをつかっていると突然マウス、キーボードが反応しなくなりました。これは単にエミュレータ側の操作に切り替わっただけなのでCtrl + Alt で操作が戻ります。とはいえ、急にPCが反応しなくなったので焦りました…

セクタを読み込む

前回まではブートセクタのみを読み込んでいましたが、3日目は読み込む領域を増やしていきます。

まず、フロッピーディスクの基本構造を説明します。これでプログラム本体のコメントの意味がいくつかわかります。
フロッピーディスクは今ではすでに全く見かけないものですが、見た目は四角くて薄いものです。その中にCDのような円盤状の磁気フィルムが入っています。
その円盤を同心円状に80コに分割した領域をシリンダといいます。一番外側を0として79まであります。さらにその同心円状の領域を18コに分割し、その一つ一つをセクタといいます。セクタは1から18までの番号です。フィルムを読み込むためにヘッドがあるのですが、両面にあるので表を0、裏を1とします。
今はデータを保存するのにSSDやHDDを利用しますが、HDDの基本構造はこれと似ています。

最初はブートセクタの次のセクタを読み込むプログラムです。
ほぼ前回と同じで、entry:の後半追加とのメッセージ内容の変更です。
ipl3a.s

.text
.code16
jmp entry
.byte 0x90
.ascii "HELLOIPL" # ブートセクタの名前(8byte)
.word 512 # 1セクタの大きさ(512固定) (16 * 512bit?)
.byte 1 # クラスタのセクタ数(1固定)
.word 1 # FAT開始のセクタ
.byte 2 # FATの個数(2固定)
.word 224 # ルートディレクトリ領域のエントリ数 (224推奨)
.word 2880 # ドライブのセクタ数(2880固定)
.byte 0xf0 # メディアのタイプ(0xf0固定)
.word 9 # FAT領域のセクタ数(9固定)
.word 18 # 1トラックのセクタ数(18固定)
.word 2 # ヘッドの数(2固定)
.int 0 # パーティション数(未使用0)
.int 2880 # ドライブのセクタ数
.byte 0, 0, 0x29 # ?
.int 0xffffffff # ボリュームシリアル番号
.ascii "HELLO-OS   " # ディスクの名前(11byte)
.ascii "FAT12   " # フォーマットの名前(8byte)
.skip 18, 0x00 # 18byte空ける

# プログラム本体
entry:
movw	$0, %ax # レジスタの初期化
movw	%ax, %ss
movw	$0x7c00, %sp
movw	%ax, %ds

movw	$0x0820, %ax
movw	%ax, %es
movb	$0, %ch # シリンダ0
movb	$0, %dh # ヘッド0
movb	$2, %cl # セクタ2

movb	$0x02, %ah # ディスクの読み込み
movb	$1, %al # 1セクタ
movw	$0, %bx
movb	$0x00, %dl # Aドライブ
int 	$0x13 # ディスクBIOS呼び出し
jc  	error

fin:
hlt # 何かあるまでcpuを停止させる
jmp 	fin # 無限ループ

error:
movw	$msg, %si

putloop:
movb	(%si), %al
addw	$1, %si
cmpb	$0, %al
je  	fin
movb	$0x0e, %ah # 一文字表示ファンクション
movw	$15, %bx # カラーコード
int 	$0x10 # ビデオBIOS呼び出し
jmp 	putloop

msg:
.byte	0x0a, 0x0a # 改行 改行(ascii)
.ascii	"error" # 出力
.byte	0x0a # 改行
.byte	0

追加部分の最後の行に jc がありますが、これは "jump if carry" の略です。キャリーフラグが1だったら飛ぶという命令です。
キャリーフラグというのは、EFLAGSレジスタという操作結果やプロセッサの状態を判断するために使われるレジスタのひとつです。EFLAGSレジスタは32bitのレジスタですが、1bit毎にそれぞれの役割で使われるので、状態は0か1の2つです。
このキャリーフラグは計算結果の桁上がりがあるか判断するために使われるのですが、他の用途にも使われるのでここではソフトウェア割り込みが正常に行われたか判断するために利用されています。

その一行前の割り込み命令は、Interrupt Jump Tableがいい資料だと思います。
今回は13のah = 0x02 を見ます。読み込みの時の指定番号だとわかります。その他の説明です(本も参考に)。
al = 処理するセクタ数
ch = シリンダ番号
cl = セクタ番号
dh = ヘッド番号
dl = ドライブ番号
es:bx = バッファアドレス
戻り値 cf = 0 :エラーなし ah = 0, cf = 1 :エラーあり ah にエラーコード

ドライブ番号はいくつかのフロッピーディスクがあったときにどれかを指定する番号です。

バッファアドレス(バッファメモリのアドレスという意味なのでしょう)には二つのレジスタが使われていますが、一つではアドレスを指定できる範囲が小さいので二つ利用します。その際 es * 0x10 + bx の計算結果を利用します。これでも約1MB分までしかアドレス指定できませんが、それでいいそうです。(相対アドレスとか絶対アドレスとかはこれに関係ありそうです。)今回はes = 0x0820, bx = 0 なので0x8200 ~0x831ff番地の 512byte(1セクタ分) が読み込まれます。
セグメントレジスタはメモリ空間のあるひとまとまりの領域の先頭アドレスを指定するのに使われます。配列と同じイメージです。まずセグメントレジスタで大まかなアドレスを指定し、あとは汎用レジスタで細かいアドレスを指定します。こちらは配列の添え字にあたります、なので今回は先頭アドレスを指定することで1セクタ分のメモリの内容が読み込まれます。
putloopラベルの一行目にある(%si) は本来はds にも値を入れなければなりませんが、この値は0なので省略されています。
主なセグメントレジスタです。

  • SS:スタックセグメント。スタックへのポインタ。
  • CS: コードセグメント。コードへのポインタ。
  • DS: データセグメント。データへのポインタ。
  • ES: エクストラセグメント。追加のデータへのポインタ。(ExtraのE)

    追加分の説明は終わったので実行してみます。

    $ make rungas

    今回は正しく動作するとなのも起こりません。
    どこかおかしいと "error" と表示されます。

  • 読み込みエラーの処理

    読み込み失敗時に何度も読み込もうとするのですが、それの回数を制限します。

    ipl3b.s

    .code16
    jmp entry
    .byte 0x90
    .ascii "HELLOIPL" # ブートセクタの名前(8byte)
    .word 512 # 1セクタの大きさ(512固定) (16 * 512bit?)
    .byte 1 # クラスタのセクタ数(1固定)
    .word 1 # FAT開始のセクタ
    .byte 2 # FATの個数(2固定)
    .word 224 # ルートディレクトリ領域のエントリ数 (224推奨)
    .word 2880 # ドライブのセクタ数(2880固定)
    .byte 0xf0 # メディアのタイプ(0xf0固定)
    .word 9 # FAT領域のセクタ数(9固定)
    .word 18 # 1トラックのセクタ数(18固定)
    .word 2 # ヘッドの数(2固定)
    .int 0 # パーティション数(未使用0)
    .int 2880 # ドライブのセクタ数
    .byte 0, 0, 0x29 # ?
    .int 0xffffffff # ボリュームシリアル番号
    .ascii "HELLO-OS   " # ディスクの名前(11byte)
    .ascii "FAT12   " # フォーマットの名前(8byte)
    .skip 18, 0x00 # 18byte空ける
    
    # プログラム本体
    entry:
    movw	$0, %ax # レジスタの初期化
    movw	%ax, %ss
    movw	$0x7c00, %sp
    movw	%ax, %ds
    
    # ディスクを読む
    movw	$0x0820, %ax
    movw	%ax, %es
    movb	$0, %ch # シリンダ0
    movb	$0, %dh # ヘッド0
    movb	$2, %cl # セクタ2
    movw    $0, %si # 失敗回数のカウンター
    
    retry:
    movb	$0x02, %ah # ディスクの読み込み
    movb	$1, %al # 1セクタ
    movw	$0, %bx
    movb	$0x00, %dl # Aドライブ
    int 	$0x13 # ディスクBIOS呼び出し
    jnc     fin # エラーがなければfinへ
    addw    $1, %si # 
    cmpw    $5, %si # 5回失敗でエラー
    jae     error
    movb    $0x00, %ah
    movb    $0x00, %dl # Aドライブ
    int     $0x13 # ドライブのリセット
    jmp     retry
    
    fin:
    hlt # 何かあるまでcpuを停止させる
    jmp 	fin # 無限ループ
    
    error:
    movw	$msg, %si
    
    putloop:
    movb	(%si), %al
    addw	$1, %si
    cmpb	$0, %al
    je  	fin
    movb	$0x0e, %ah # 一文字表示ファンクション
    movw	$15, %bx # カラーコード
    int 	$0x10 # ビデオBIOS呼び出し
    jmp 	putloop
    
    msg:
    .byte	0x0a, 0x0a # 改行 改行(ascii)
    .ascii	"error" # 出力
    .byte	0x0a # 改行
    .byte	0

    ドライブのリセットというところが新しいところです。他は特に無いです。
    今回も表示画面は上と同じです。

    さらにセクタを読み込む

    最初はブートセクタの次のセクタのみを読み込みましたが、その読み込むセクタ数を増やしていきます。18セクタつまり1シリンダ分読み込みます。

    ipl3c.s

    .code16
    jmp entry
    .byte 0x90
    .ascii "HELLOIPL" # ブートセクタの名前(8byte)
    .word 512 # 1セクタの大きさ(512固定) (16 * 512bit?)
    .byte 1 # クラスタのセクタ数(1固定)
    .word 1 # FAT開始のセクタ
    .byte 2 # FATの個数(2固定)
    .word 224 # ルートディレクトリ領域のエントリ数 (224推奨)
    .word 2880 # ドライブのセクタ数(2880固定)
    .byte 0xf0 # メディアのタイプ(0xf0固定)
    .word 9 # FAT領域のセクタ数(9固定)
    .word 18 # 1トラックのセクタ数(18固定)
    .word 2 # ヘッドの数(2固定)
    .int 0 # パーティション数(未使用0)
    .int 2880 # ドライブのセクタ数
    .byte 0, 0, 0x29 # ?
    .int 0xffffffff # ボリュームシリアル番号
    .ascii "HELLO-OS   " # ディスクの名前(11byte)
    .ascii "FAT12   " # フォーマットの名前(8byte)
    .skip 18, 0x00 # 18byte空ける
    
    # プログラム本体
    entry:
    movw	$0, %ax # レジスタの初期化
    movw	%ax, %ss
    movw	$0x7c00, %sp
    movw	%ax, %ds
    
    # ディスクを読む
    movw	$0x0820, %ax
    movw	%ax, %es
    movb	$0, %ch # シリンダ0
    movb	$0, %dh # ヘッド0
    movb	$2, %cl # セクタ2
    
    readloop:
    movw    $0, %si # 失敗回数のカウンター
    
    retry:
    movb	$0x02, %ah #ディスクの読み込み
    movb	$1, %al # 1セクタ
    movw	$0, %bx
    movb	$0x00, %dl # Aドライブ
    int 	$0x13 # ディスクBIOS呼び出し
    jnc     next # エラーがなければnextへ
    addw    $1, %si 
    cmpw    $5, %si # 5回失敗でエラーへ
    jae     error
    movb    $0x00, %ah
    movb    $0x00, %dl # Aドライブ
    int     $0x13 # ドライブのリセット
    jmp     retry
    
    next:
    movw    %es, %ax # アドレスを0x0020 進める
    addw    $0x0020, %ax
    movw    %es, %ax
    # addw    $0x512, %bx
    addb    $1, %cl # セクタ
    cmpb    $18, %cl #
    jbe     readloop  # (18 >= cl) then readloop
    
    fin:
    hlt # 何かあるまでcpuを停止させる
    jmp 	fin # 無限ループ
    
    error:
    movw	$msg, %si
    
    putloop:
    movb	(%si), %al
    addw	$1, %si
    cmpb	$0, %al
    je  	fin
    movb	$0x0e, %ah # 一文字表示ファンクション
    movw	$15, %bx # カラーコード
    int 	$0x10 # ビデオBIOS呼び出し
    jmp 	putloop
    
    msg:
    .byte	0x0a, 0x0a # 改行 改行(ascii)
    .ascii	"error" # 出力
    .byte	0x0a # 改行
    .byte	0

    セクタ番号の cl を1増やし、読み込み番地を指定する es を0x20 進めれば次のセクタを読み込めます。
    next:の最初の3行は esを 一発で0x20増やす方法がないので他のレジスタを経由して計算しています。(実際にやってみるとミスマッチのエラーが出ます。)これは先ほど確認したように 0x20 * 0x10 + 0x00 = 0x200 = 512 なのでこれで1セクタ分アドレスを進めたことになります。
    これは アドレス指定に使う bx を直接増やしても同じです。

    また、17セクタ(ビートセクタ以外)を読み込むのにループにせず al を17にすれば簡単に済みますが、これでは次の場合に問題があるみたいです。

    いくつかのシリンダを読み込む

    今日の分はキリがいいのでこれで最後にします。
    次はシリンダをさらに読み込んでいきます。その際読み込む領域が裏側にいく場合のことも考えます。
    2行目と、next: に追加部分があります。

    ipl3d.s

    .code16
    .equ    cyls, 10
    jmp     entry
    .byte   0x90
    .ascii  "HELLOIPL" # ブートセクタの名前(8byte)
    .word   512 # 1セクタの大きさ(512固定) (16 * 512bit?)
    .byte   1 # クラスタのセクタ数(1固定)
    .word   1 # FAT開始のセクタ
    .byte   2 # FATの個数(2固定)
    .word   224 # ルートディレクトリ領域のエントリ数 (224推奨)
    .word   2880 # ドライブのセクタ数(2880固定)
    .byte   0xf0 # メディアのタイプ(0xf0固定)
    .word   9 # FAT領域のセクタ数(9固定)
    .word   18 # 1トラックのセクタ数(18固定)
    .word   2 # ヘッドの数(2固定)
    .int    0 # パーティション数(未使用0)
    .int    2880 # ドライブのセクタ数
    .byte   0, 0, 0x29 # ?
    .int    0xffffffff # ボリュームシリアル番号
    .ascii  "HELLO-OS   " # ディスクの名前(11byte)
    .ascii  "FAT12   " # フォーマットの名前(8byte)
    .skip 18, 0x00 # 18byte空ける
    
    # プログラム本体
    entry:
    movw	$0, %ax # レジスタの初期化
    movw	%ax, %ss
    movw	$0x7c00, %sp
    movw	%ax, %ds
    
    # ディスクを読む
    movw	$0x0820, %ax
    movw	%ax, %es
    movb	$0, %ch # シリンダ0
    movb	$0, %dh # ヘッド0
    movb	$2, %cl # セクタ2
    
    readloop:
    movw    $0, %si # 失敗回数のカウンター
    
    retry:
    movb	$0x02, %ah # ディスクの読み込み
    movb	$1, %al # 1セクタ
    movw	$0, %bx
    movb	$0x00, %dl # Aドライブ
    int 	$0x13 # ディスクBIOS呼び出し
    jnc     next # エラーがなければnextへ
    addw    $1, %si 
    cmpw    $5, %si # if ( 5 <= si)
    jae     error
    movb    $0x00, %ah
    movb    $0x00, %dl # Aドライブ
    int     $0x13 # ドライブのリセット
    jmp     retry
    
    next:
    movw    %es, %ax # アドレスを0x0020 進める
    addw    $0x0020, %ax
    movw    %es, %ax
    # addw    $0x512, %bx
    addb    $1, %cl # セクタ
    cmpb    $18, %cl # 
    jbe     readloop  # (18 >= cl) then readloop
    movb    $1, %cl
    addb    $1, %dh #ヘッド
    cmpb    $2, %dh 
    jb      readloop # (2 > dh) then readloop
    movb    $0, %dh
    addb    $1, %ch # シリンダ
    cmpb    cyls, %ch 
    jb      readloop # (clys > ch) then readloop
    
    fin:
    hlt # 何かあるまでcpuを停止させる
    jmp 	fin # 無限ループ
    
    error:
    movw	$msg, %si
    
    putloop:
    movb	(%si), %al
    addw	$1, %si
    cmpb	$0, %al
    je  	fin
    movb	$0x0e, %ah # 一文字表示ファンクション
    movw	$15, %bx # カラーコード
    int 	$0x10 # ビデオBIOS呼び出し
    jmp 	putloop
    
    msg:
    .byte	0x0a, 0x0a # 改行 改行(ascii)
    .ascii	"load error" # 出力
    .byte	0x0a # 改行
    .byte	0

    2行目は読み込むシリンダ数を指定するための変数を宣言しています。
    これで実行してエラーがなければ今までと同じ特に何もない画面が表示されます。

    最後に

    これでとりあえず読み込みができるようになり、3日目前半(?)が終わりました。
    続きはとうとうOS本体に取り掛かっていきます。