Skip to content

Latest commit

 

History

History
321 lines (232 loc) · 20.3 KB

note.md

File metadata and controls

321 lines (232 loc) · 20.3 KB

1. 各ファイルの役割

1.1 ipl10.nas

IPL(Initial Program Loader) である. IPL とは,コンピュータの電源が投入された時に最初に起動するプログラムOSを読み込んでメモリに展開する.今回,IPLはフロッピーディスクから起動することを前提としている. ipl10.nas は,フロッピーディスクの情報をすべて書き出す(各セクタ,シリンダ,ヘッダ). ブートセクタが読み込まれるアドレスは,BIOSの仕様から以下のように定められている.

0x7c00〜0x7dff

今回のプログラムはメモリの0x7c00から読み込まれるように指定した.

ディスクの容量は,10シリンダ * 18セクタ * 2ヘッダ * 512byte = 180KBある.この内容を,メモリの 0x8000〜0x34fff までに書き込まれるよう指定した.この内,ブートセクタは 0x8000〜0x8200 に読み込まれる.ディスクにはOSの実態が保存されるわけだが,ディスク上のどこにプログラムが保存されるのかを知っておく必要がある.今回は以下のように指定した.

  • ファイル名は 0x2600 以降に入る
  • ファイルの中身は 0x4200 以降に入る

したがって,メモリ上では 0x8000 + 0x4200 = 0xc200 を見れば,OSのためのプログラムを読むことができる.そのため,IPLはディスクの内容をメモリに書き出した後に,0xc200に処理を移し,OSを起動する.また,OSのプログラムはHariMainから始まる.

1.2 Makefile

make run すると以下の順で処理が進む.

  1. まず,各ソースコードを機械語にする必要がある.
    • アセンブラnaskによって,ipl10.nasipl10.binにコンパイルされる.
    • 同様に,asmhead.nasnasmhead.binにコンパイルされる.
    • C言語で記述されたbootpack.cは,以下の手順で機械語にされる.
      1. cコンパイラccで,bootpack.cをアセンブリコードbootpack.gasにコンパイルする.
      2. gasという名前のアセンブラを前提とした機械語であるため,これをgas2naskによってbootpack.nasへ変換し,nask用アセンブリコードにする.
      3. アセンブラnaskによってアセンブルし,bootpack.objを作成する.
      4. obj2bimによって,他のobjファイルとリンクし,bootpack.bimを作成する.
      5. 「はりぼてOS」用の形式にするため,bim2hrbによってbootpack.hrbに変換する.
  2. asmhead.nasbootpack.hrbcatコマンドにより連結し,haribote.sysを作成する.これがOSの本体となる.
  3. haribote.sysをディスクイメージに保存する.edimgというツールを用いて,ipl10haribote.sysからharibote.imgを作成する.これで,ipl10を元にした?仮想のフロッピーディスクにharibote.sysを書き込んだことになる.その結果がharibote.imgとなる.
  4. イメージファイルがqemuでエミュレートされる

1.3 asmhead.nas

16bitモードでできることをやる. 今回使用するCコンパイラでは,32bitモード用の機械語しか生成できない.しかし,BIOSは16bitモードでしか利用できない.そのため,asmhead.nasでBIOSの設定を行い,その後32bitモードに切り替えて,C言語での処理に移る.

BIOSにやってもらうことは以下

  • ビデオモードの設定 VRAM(video RAM) の場所を設定する.
    各番地が画面上の画素に対応する.VRAMの場所は,各画面モード毎に決まっている.
    今回は「VGAグラフィクス 3202008bitカラー」モードである.
    この時のVRAMは「0xa0000〜0xaffff」の64KBを使う.
    ここに値を書き込むと,画面に描画される.
  • キーボード状態の取得
  • 各状態のメモ
    画面の画素数やその幅,キーボードのLED状態等をメモリ上にメモしている.
    今回は,「0x0ff0〜0x0ff8」周辺にメモすることにする(空いていればどこでもよい).
    ちなみに,これは32bitモードでもできるが,関連することなので書いておく.
  • その他100行くらい
    C言語で書いた部分を呼び出すための処理
  1. CPUの切り替え,PICの初期化を行う.そのため,割り込みが生じると困る.したがって,PIC0とPIC1のIMRをそれぞれ割り込み禁止(0xffを出力)に設定する.さらに,CPUレベルでも割り込みを禁止する(CLI).
  2. 初期のPCは16bitモードのみであり,最大 1MB しかメモリを使用できなかった.現在では多くのメモリを使用できるようになったが,古いOSとの互換性の問題から,これを有効化する命令を送信する必要がある.そこで,A20GATE信号線をONにする命令( 0xdf )をKBCに送る.
  3. GDTを用いて番地指定を行うために,プロテクトモード(保護付き32bit)へ移行する必要がある.このために,CR0レジスタにページングの禁止とプロテクトモード移行のフラグを立てる.この時,パイプライン(命令実行中にまだ先にある命令の解釈を開始する)をリセットするためにJMP命令を行う.また,機械語の解釈が変わるため,セグメントレジスタの値を初期化し直す.
  4. メモリ上にプログラムを転送する.
    1. bootpack(512KB)を,0x00280000番地へコピー(512KBは適当.多い分には問題無い)
    2. ブートセクタ(512byte)を,0x00100000番地へコピー
    3. ブートセクタを除いたディスクの中身を,0x00100200番地へコピー
  5. 最後に,bootpack.hrbを実行する.実行前に,実行に必要なデータをメモリ上にコピーする.

1.4 naskfunc.nas

C言語では処理できないものはアセンブラで書けば良い.最初に対象CPUやbitモードの指定をして,関数を宣言する.アセンブラでしかできないことは,例えばHLT命令とか,特別なレジスタへのアクセスとか,外部装置との信号のやりとりとか.

1.5 bootpack.c

OSを起動するための処理を1パックにまとめるという意味で,bootpack.C言語を用いる場合は,EAXECXEDXの三つしか使ってはいけない(その他はC言語自体が使う).マウスやキーボードを利用するためには,割り込み処理が必要となる.そして,そのためにはCDTIDTPICを初期化しなくてはならない.

PICは,CPUが外部装置から割り込み信号を受け取るための補助チップ. この補助チップの設定として,以下の2つを設定する

  • IMRの設定:割り込み信号の受付禁止と解放の設定
  • ICWの設定:どのIRQを受け取った時にどんな命令を実行するかの設定
    • 今回は,IRQ 0〜15 を INT 0x20〜0x2f で受け取るよう設定した
    • また,IRQ12がマウス,IRQ1がキーボードを表すため,対応する割込みハンドラを作成した.

GDTは,セグメントを利用するためにその設定を保持するメモリの領域.領域は 0x270000〜0x17ffff 番地を利用する セグメントの設定として,以下を設定した

  • セグメント番号1:全メモリを指すセグメント
  • セグメント番号2:bootpackを読み込むセグメント:0x280000〜0x2ffff

IDTは,割り込み発生時にどの関数を呼び出すかの設定を保持するメモリの領域. 領域は 0x26f800〜0x26ffff 番地を利用する.PICから受け取った命令と,実際の関数の組み合わせを設定する

  • IRQ12 から信号を受け取ると 0x2c が実行されるため,これとマウスからの割り込みハンドラを対応づける.
  • IRQ1 から信号を受け取ると 0x21 が実行されるため,これとキーボードの割込みハンドラを対応づける.

2. 特別なレジスタやメモリ領域の詳しい説明

2.1 EFLAGS

32bitのレジスタ.16bitのFLAGSの拡張.

15  14  13  12  11  10  09  08  07  06  05  04  03  02  01  00
[  ][NT][ IOPL ][OF][DF][IF][TF][SF][ZF][  ][AF][  ][PF][  ][CF]
  • IF:interrupt flag:割り込みフラグ
    • CPUに割り込み要求信号が来たときに,割り込み応対回路が操作するか
    • CLI(clear interrupt flag):割り込みフラグを0にする
    • STI( set interrupt flag):割り込みフラグを1にする
  • AC:
    • 486以降のCPUについている
    • 386ではフラグ自体が存在せず,常に0
      • わざと1を格納しても,0に戻ってしまう
      • 486以降のCPUであるか判断するために使用できる

2.2 GDT(global (segment) descriptor table)

セグメントの設定テーブル. セグメントは,メモリを切り分けて,各セグメントの最初の番地を0として扱えるようにする機能.セグメントの設定は8byte(64bit)で表されるが,各設定につきセグメント番号が付与される.セグメント番号は0〜8191(0x0000〜0x1fff)が使える.セグメントレジスタ(16bit)にはセグメント番号(セグメントセレクタ)が格納される. セグメントの設定とセグメント番号の対応表が,GDTに格納される.GDTがメモリ上のどこにあり,どのような状態であるのかはレジスタ**GDTR(GDT register)**に格納する.GDTには,以下のような8byteの形式で書き込む

[       4byte       ][   20bit   ][4bit][   8bit     ]
[2byte][1byte][1byte][2byte][   1byte  ][   1byte    ]
[       base        ][   limit   ][exar][access right]
[ low ][ mid ][high ][ low ][high]
  • limit_highの上位4bitは管理用属性(exar:拡張アクセス権)が書き込まれる
    • [GD00]という形式であり,それぞれの意味は以下
      • G:granularity:リミットの単位をbyteでなくpageにするフラグ(1page = 4kb)
      • D:セグメントモード.16bitか32bitを選べる.この16bitモードは80286のプログラムを動かすためだけのもの.普通は1.
      • アクセス権
        以下がある
        • 00000000(0x00):未使用のdescriptor table
        • 10010010(0x92):システム専用の読み書き可能なセグメント.実行不可.
        • 10011010(0x9a):システム専用の実行可能セグメント.読み込みOK.書き込み不可
        • 11110010(0xf2):アプリケーション用の読み書き可能なセグメント.実行不可.
        • 11111010(0xfa):アプリケーション用の実行可能セグメント.読み込みOK.書き込み不可.

32bitモードでは以下の2つのモードがある

  • システムモード(リング0)
    OS等の管理する側のプログラムが走っている状態
  • アプリケーションモード(リング1)
    アプリケーション等の管理される側のプログラムが走っている状態
    実行中のプログラムが置いてあるセグメントによって,CPUの実行モードは変化する.

2.3 GDTR

セグメントの設定が格納されるレジスタ.MOV命令では値を書き込むことができない.LGDT命令を用いて番地を指定すると,指定番地から6byteがGDTRに書き込まれる.

下位                       上位
[ 2byte  ][ 4byte             ]
[リミット][GDTの置いてある番地]

2.4 IDT(interrupt descriptor table)

割り込み設定表. 割り込み発生時には,CPUが現在の処理を一時中断し,あらかいめ設定された関数を呼び出す.IDTには,割り込み番号に応じて呼び出す関数を設定する.割り込み番号は 0〜255 ( 0x00〜0xff )が設定できる.レジスタIDTRに,IDTの置いてある番地などを格納しておく.

2.5 PIC(programmable interrupt controller)

8個の割り込み信号(interrupt request:IRQ)を1つの割り込み信号にまとめる補助チップ.PICのためのレジスタにはIMRICWがある

2.6 IMR(interrupt mask register)

割り込み目隠しレジスタ. 8bitがそれぞれ各IRQ信号に対応していて,bitが1になっているIRQ信号は割り込み信号を受け付けない.これは,割り込みの設定時や,なんの装置も繋がっていない場合の誤作動を防止するためのもの.

2.7 ICW(initial control word)

初期化制御データ. ICW1〜ICW4 の,合計で4byteのデータ.

  • ICW1,ICW4 は,割り込み信号の伝記的な特性や基板上の配線に関わるもの
  • ICW2は,受け取ったIRQと割り込み番号を対応付けてCPUに通知するための設定
  • ICW3は,マスタースレーブ接続に関する設定

ICW2について,CPUはPICに2byteのデータを送信させるが,その内容は「0xcd 0x??」の2byte.ここで,「0xcd」は「INT」命令の機械語.よって,受け取った信号に応じてINT命令が実行されることになる. ICW3について,マスタは,どこにスレーブが繋がっているかを指定する.スレーブは,マスタの何番に自分が繋がっているかを指定する.

2.8 OCW(operation command words)

通常動作時にPICを制御するコマンド.OCW1〜OCW3の三種類がある.

http://softwaretechnique.jp/OS_Development/kernel_development03.html

特に,OCW2のEOIコマンドを書き込まない限り,ISRのビットがクリアされない.そのため,割り込み中のIRQより低いIRQが入ってこなくなる.

2.9 ISR(interrupt register)

割り込み中レジスタ.CPUがそのIRQ番号に対応する割り込みを処理中であるか記憶している.PICは,このビットで処理中のIRQを判断する.

2.10 CR0(controll register 0)

OS以外がいじってはいけない大事なレジスタ 今回は,最上位を0,最下位を1に設定することで,「ページングを利用しないプロテクトモード」に設定する

  • プロテクトモード(protected virtual address mode)
    GDTを用いて,実際の番地ではないセグメント番号で仮想的にメモリ番地を指定するモード
  • リアルモード(real address mode)
    セグメントレジスタの値で直接番地の一部を指定するモード

また,0x60000000とのORをとることで,キャッシュモードを禁止する.許可する場合には,~0x60000000とのANDをとる.

2.10.1 リアルモードとプロテクトモードのセグメント機構について

  • リアルモード:メモリへのアクセス範囲は 1MB ( 0x00000〜0xfffff ) に限られる.セグメントレジスタにはセグメントアドレスをセットする
  • プロテクトモード:メモリのアクセス範囲は 4GB ( 0x00000000〜0xffffffff ) に拡張される.セグメントレジスタにはGDTのセレクタ値をセットする
    • プロテクトモードでのアクセス手順
      1. セグメントレジスタには,セレクタ値( GDTのインデックス)が格納されている
      2. GDTRに,GDTのアドレスが格納されている
      3. これらの情報を組み合わせて,GDTに格納されたセグメントのベースアドレスを取得する
      4. メモリにアクセスする
    • リアルモードでのアクセス手順
      1. セグメントレジスタには,セグメントのベースアドレスが格納されている
      2. メモリにアクセスする

http://caspar.hazymoon.jp/OpenBSD/annex/intel_segment.html

3. メモリ管理

3.1 メモリマップ

番地 概要 容量
0x00000000 - 0x000FFFFF 起動中に利用.起動後は空く 1MB
0x00100000 - 0x00267FFF フロッピーディスクの内容記憶用 1440KB
0x00268000 - 0x0026F7FF 空き 30KB
0x0026f800 - 0x0026FFFF IDT 2KB
0x00270000 - 0x0027FFFF GDT 64KB
0x00280000 - 0x002FFFFF bootpack.hrb 512KB
0x00300000 - 0x003FFFFF スタックなど 1MB
0x00400000 - 空き

3.2 メモリ容量の確認

メモリ容量は,PCの起動時にBIOSがチェックする.しかし,BIOSに聞こうとすると

  • asmhead.nasが長くなる
  • BIOSのバージョンにより呼び出し方が異なる

等の問題点があるので,自力でチェックする.

  • キャッシュ機能
    • 486以降のCPUについている
    • そもそも,メモリ - CPU 間はとても遠い
      • レジスタメモリへのMOVを比較すると,前者が圧倒的に早い
      • しかし,CPUの記憶力がひどすぎるため,遅くてもメモリを頻繁に使用せざるを得ない,という問題がある.
    • キャッシュメモリ
      • CPUの中に設置されたメモリ
      • CPUの速度についてこられるメモリで,とても高価
      • キャッシュ制御回路は,メモリの同じ番地の値がなんども書き換えられる場合(for(i=1;i<100;i++)みたいな時とか)には,いちいちメモリを用いずにキャッシュの中だけで処理しようとする
      • 8〜16KBキャッシュがついただけでも,6倍以上の性能差がうまれたという.

確認の仕方は単純.以下のようにする

  1. キャッシュ機能をオフにする
  2. メモリに適当な値を書く
  3. 直後に,そこを読み込む
  4. 書き込んだ値と比較する
    1. 等しい場合,メモリとつながっている
    2. 異なる場合,メモリが存在しないため,探索を打ち切る
  5. あらかじめ指定した回数分,2.〜4.を繰り返す

3.3 メモリ管理とは

メモリのどこが使用済みで,どこが未使用なのかを管理する.基本は 確保解放

メモリ管理の例

例1:配列で管理する

128MB( 0x8000000 byte )のメモリを 0x1000 byte 毎に管理する. 0x8000000 / 0x1000 = 0x8000 = 32,768 byte のため...

  1. 32,768 byte 分の配列を作成
    • 使用中の場合は1,未使用の場合は0を格納することで管理する
  2. 確保するメモリ分,0が連続で並んでいる場所を探す
  3. 使用中のマークをつける
  4. 配列の要素数から実際に割り当てるメモリ番地を計算し,返す
  • 問題
    • 確保,解放するメモリ量に関わらず,固定長の配列を用意する必要がある
    • 膨大なメモリ領域の確保,解放が低速

例2:メモリの空き情報のリストを作成して管理する

  1. メモリのどこが空いているかを示す空き情報を格納した構造体を作成
    • 空き情報の数
    • 空き情報の配列
      • 空いているメモリのアドレス
      • 空いているメモリのサイズ
  2. 確保したいメモリ分を確保可能な空き情報を探す
  3. 空き情報のアドレスを確保するメモリ分ずらし,サイズも減らす
    • サイズが0になった場合は,空き情報を削除する
  4. メモリの解放の場合,空き情報を追加する
    • 隣の空き情報と結合できるか調べて,できそうであれば結合する
  • 利点
    • 管理に必要な容量が少ない
    • 大きなメモリ領域の確保・解放が容易
  • 問題
    • プログラムが複雑になる

4. タイマ処理

時計なしに時間を測ることはできない.というわけで,タイマ割り込みを実装する必要がある.タイマ割り込みが起きた回数を割り込み処理ルーチンで数えておくことで,経過時間を判断できる.

4.1 PID(Programmable Interval Timer)

タイマのためには,PIT という装置の設定が必要になる.IRQ の0番につながっている.調べてみると,8254 というチップ(もしくはその互換品) が使われているらしい. PITは,割り込みの周期を設定できる.とりあえず,11932 と設定すると,1秒間に 100回割り込みが発生するらしい.

(PIT)8254