最近徹夜で記事を書いていて昼夜逆転しています。なっしゅです。
「自作OS入門」が12日目まで終わって知識が滾ってきたので(?)、0日目から5日目までの内容を嚙み砕いて説明します。
目次
title | content | |
---|---|---|
1 | 目的はIMGファイル | バイナリエディタを使ってIMGファイルを作成します。 |
2 | naskの命令とレジスタ | アセンブリ言語の命令とレジスタを紹介します。 |
3 | BIOS interrupt call | 割り込み命令によって呼び出されるBIOS interrupt callについて説明します。 |
4 | Makefile | 便利なMakefileの使い方を説明します。 |
5 | OBJの統合まで | C言語でアセンブリ言語の命令を使えるようなファイルの統合方法を紹介します。 |
6 | 描画関数とフォント | 画面描画の仕方を説明します。 |
7 | さいごに |
目的はIMGファイル
世の中にはmacOSやWindows、Linuxなど、多くのOSが出回っています。これらのOSは一般にストレージドライブと呼ばれる記憶媒体に入っており、例えばハードディスクやフロッピーディスクといったものが代表的です。そして、ISOやIMGというバイナリファイルとしてインストールされます。今回はFD、フロッピーディスクにIMGファイルとして自作OSを書き込み、そこからQEMUというエミュレータで起動させる手筈で開発をしていきます。
IMGはバイナリファイルなので、手打ちでOSを作ることが可能です。
なので、バイナリエディタというバイナリファイルを編集するためのエディタでIMGファイルを作っていきます。使用するエディタはStirlingやFavBinEditがありますが、今回は「自作OS入門」に合わせてBzを使います。
細かい内容は省きますが、このように編集します。これを0016:8000まで入力します。
「その量を手打ちは絶対だめだ......」と思われがちですが、実はこのIMGはほとんど0x00で埋まっているので、なんてことないです。コピペを駆使します。
フロッピーディスクにマウントして実機で動かしてもいいのですが、少し面倒なのでQEMUというエミュレータを使って起動させることにします。
上手くいくとこのようにBIOSが起動します。
これでOSは完成です(!)
結局のところ、IMGが完成すればOSができたも同然なので、あとはこれをアセンブリ言語やC言語で拡張していくだけの話です。最終的にはIMGを作る、ということを念頭に置いて開発していきます。
Tips
最終的にはIMGを作ることを念頭に置く。
naskの命令とレジスタ
今度はバイナリエディタで直接IMGを編集するのではなく、naskというNASMを参考にして作られたアセンブラを使ってIMGを作っていきます。先ほどと全く同じIMGを作ります。
DB 0xeb, 0x4e, 0x90, 0x48, 0x45, 0x4c, 0x4c, 0x4f
DB 0x49, 0x50, 0x4c, 0x00, 0x02, 0x01, 0x01, 0x00
DB 0x02, 0xe0, 0x00, 0x40, 0x0b, 0xf0, 0x09, 0x00
DB 0x12, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00
DB 0x40, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x29, 0xff
DB 0xff, 0xff, 0xff, 0x48, 0x45, 0x4c, 0x4c, 0x4f
DB 0x2d, 0x4f, 0x53, 0x20, 0x20, 0x20, 0x46, 0x41
DB 0x54, 0x31, 0x32, 0x20, 0x20, 0x20, 0x00, 0x00
RESB 16
DB 0xb8, 0x00, 0x00, 0x8e, 0xd0, 0xbc, 0x00, 0x7c
DB 0x8e, 0xd8, 0x8e, 0xc0, 0xbe, 0x74, 0x7c, 0x8a
DB 0x04, 0x83, 0xc6, 0x01, 0x3c, 0x00, 0x74, 0x09
DB 0xb4, 0x0e, 0xbb, 0x0f, 0x00, 0xcd, 0x10, 0xeb
DB 0xee, 0xf4, 0xeb, 0xfd, 0x0a, 0x0a, 0x68, 0x65
DB 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x77, 0x6f, 0x72
DB 0x6c, 0x64, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00
RESB 368
DB 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xaa
DB 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00
RESB 4600
DB 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00
RESB 1469432
ここではRESB
命令を使って大幅にコードを削っています。まだ全くコードの意味が分からないので、さらに改良を加えます。
; hanpen-os
; TAB=4
; 標準的なFAT12フォーマットフロッピーディスクのための記述
DB 0xeb, 0x4e, 0x90
DB "HELLOIPL" ; ブートセクタの名前(8byte)
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 "HANPEN-OS " ; ディスクの名前(11byte)
DB "FAT12 " ; フォーマットの名前(8byte)
RESB 18 ; とりあえず18byte予約する
; プログラム本体
DB 0xb8, 0x00, 0x00, 0x8e, 0xd0, 0xbc, 0x00, 0x7c
DB 0x8e, 0xd8, 0x8e, 0xc0, 0xbe, 0x74, 0x7c, 0x8a
DB 0x04, 0x83, 0xc6, 0x01, 0x3c, 0x00, 0x74, 0x09
DB 0xb4, 0x0e, 0xbb, 0x0f, 0x00, 0xcd, 0x10, 0xeb
DB 0xee, 0xf4, 0xeb, 0xfd
; メッセージ部分
DB 0x0a, 0x0a ; 改行を2つ
DB "Welcome to HanpenOS!!!"
DB 0x0a ; 改行を1つ
DB 0
RESB 0x1fe-$ ; 0x001feまで0x00で埋める
DB 0x55, 0xaa
; ブートセクタ以外
DB 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00
RESB 4600
DB 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00
RESB 1469432
これで以前よりも意味の分かるコードになりました。
また、RESB 0x1fe-$
でメッセージを自由に変えられるようにしています。
専門用語については私が調べて表にしましたので参考にしてみてください。
name | content |
---|---|
FAT12 | クラスタ番号を12bitで管理する方式 |
フォーマット | 初期化すること |
セクタ | ディスクにおける最小単位 |
トラック | セクタの一周分 |
クラスタ | セクタがいくつか組み合わさったもの |
エントリ | プログラムが実行する際に指定される開始アドレス |
ドライブ | ディスクなどのデータの読み書きをする装置(FFDやHDDなど) |
ヘッド | レコード針みたいなやつ。これで読み書きをする |
パーティション | ディスクを区切った領域 |
ボリュームシリアル番号 | WindowsやMS-DOSがボリュームを識別するための番号 |
12 | ブートセクタ |
あと意味不明なのはブートセクタを除いてプログラム本体なので、こちらも人間にわかりやすく書き換えます。
; hanpen-os
; TAB=4
ORG 0x7c00 ; このプログラムがメモリのどこに読み込まれるのか
; 標準的なFAT12フォーマットフロッピーディスクのための記述
JMP entry
DB 0x90
DB "HELLOIPL" ; ブートセクタの名前(8byte)
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 "HANPEN-OS " ; ディスクの名前(11byte)
DB "FAT12 " ; フォーマットの名前(8byte)
RESB 18 ; とりあえず18byte予約する
; プログラム本体
entry:
MOV AX, 0 ; レジスタの初期化
MOV SS, AX
MOV SP, 0x7c00
MOV DS, AX
MOV ES, AX
MOV SI, msg
putloop:
MOV AL, [SI]
ADD SI, 1 ; SIに1を足す
CMP AL, 0
JE fin
MOV AH, 0x0e ; 1文字表示ファンクション
MOV BX, 15 ; カラーコード
INT 0x10 ; ビデオBIOS呼び出し
JMP putloop
fin:
HLT ; 何かあるまでCPUを停止させる
JMP fin ; 無限ループ
msg:
DB 0x0a, 0x0a ; 改行を2つ
DB "Welcome to HanpenOS!!!"
DB 0x0a ; 改行を1つ
DB 0
RESB 0x1fe-$ ; 0x001feまで0x00で埋める
DB 0x55, 0xaa
; ブートセクタ以外
DB 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00
RESB 4600
DB 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00
RESB 1469432
さて、新出命令がたくさん出てきたので、紹介します。
name | stands for | content |
---|---|---|
DB |
data byte | 1byte確保する。 |
DW |
data word | 2byteの領域を確保する |
DD |
data double-word | 4byteの領域を確保する |
RESB n |
reserve byte | nbyte予約する(空ける)。 |
ORG |
origin | プログラムがメモリのどこに読み込まれるかをアセンブラに伝える。 |
JMP |
jump | 指定したラベルに移動する。C言語にいうところのgoto文。 |
MOV |
move | 代入命令。MOV AX, 0 をC言語風(?)に書くとAX = 0;
|
ADD |
add | 加算を行う。ADD AX, 2 をC言語風に書くとAX += 2;
|
CMP |
compare | 比較演算子、C言語の条件式にあたる。 |
JE |
jump if equal | 直前のCMP の結果が等しい場合、指定したラベルにJMP する。 |
JA |
jump if above | 直前のCMP の結果がより大きい場合、指定されたラベルにJMP する。 |
JB |
jump if below | 直前のCMP の結果がより小さい場合、指定されたラベルにJMP する。 |
JAE |
jump if above or equal | 直前のCMP の結果がより大きいもしくは等しい場合、指定されたラベルにJMP する。 |
JBE |
jump if above or equal | 直前のCMP の結果がより小さいもしくは等しい場合、指定されたラベルにJMP する。 |
JC |
jump if carry | キャリーフラグが1の場合、指定したラベルにJMP する。 |
JNC |
jump if not carry | キャリーフラグが0の場合、指定されたラベルにJMP する。 |
EQU |
equal | 定数を宣言する。 |
HLT |
halt | CPUを待機状態にする。節電に役立つ。 |
INT |
interrupt | 割り込み命令。INT 0x10 でBIOS interrupt callを呼び出すために使う。 |
CLI |
clear interrupt flag | 割り込みフラグを0にする。 |
STI |
set interrupt flag | 割り込みフラグを1にする |
IN |
input | 指定した装置からデータを取得する。 |
OUT |
output | 指定した装置にデータを送る。 |
POP |
pop | スタックから値を取り出す。 |
PUSH |
push | スタックに値を入れる。 |
POPFD |
pop flags double-word | 2byteでフラグをスタックから取り出す。 |
PUSHFD |
push flags double-word | 2byteでフラグをスタックに入れる。 |
次に、AX
やSS
などはレジスタと呼ばれるものであり、機械語にとっての変数です。
Tips
レジスタとは、機械語における変数のようなもの。CPU内部にあるため、RAMより高速にデータを受送信できる。
レジスタは数が限られており、次のように表で表すことができます。
type \ size | 64bit | 32bit | 16bit | 8bit |
---|---|---|---|---|
アキュムレータ | RAX |
EAX |
AX |
AH , AL
|
ベース | RBX |
EBX |
BX |
BH , BL
|
カウンタ | RCX |
ECX |
CX |
CH , CL
|
データ | RDX |
EDX |
DX |
DH , DL
|
スタックポインタ | RSP |
ESP |
SP |
|
ベースポインタ | RBP |
EBP |
BP |
|
ソースインデックス | RSI |
ESI |
SI |
|
デスティネーションインデックス | RDI |
EDI |
DI |
特に注目すべきレジスタは16bitのものであり、32bitや64bitは16bitのレジスタを拡張したものだと考えるとよいです。また、64bitレジスタに関しては本記事で取り扱いません。
8bitレジスタは、実は16bitレジスタの一部であり、例えばMOV AX, 0xe2f1
した時のAX
レジスタを表に表すとこのように構成されています。
上位8bit | 下位8bit | |
---|---|---|
レジスタ | AH | AL |
16進数 | 0xe2 | 0xf1 |
2進数 | 0b11100010 | 0b11110001 |
32bitレジスタや64bitレジスタも同様の構造になっています。
また、セグメントレジスタと呼ばれる8bitレジスタが独立してあり、これらは普通のレジスタの補助的な役割を果たしています。
name | register |
---|---|
エクストラセグメント | ES |
コードセグメント | CS |
スタックセグメント | SS |
データセグメント | DS |
Fセグメント | FS |
Gセグメント | GS |
BIOS interrupt call
アセンブリ言語では割り込み命令によってBIOS interrupt callと呼ばれるBIOSの関数を呼び出すことができます。BIOS interrupt callには次の関数があります。
Vector | Service |
---|---|
0x10 | Video Services |
0x13 | Low Level Disk Services |
0x14 | Serial port services |
0x16 | Keyboard services |
関数の使い方は(AT)BIOSというページで上手くまとまっていますので、こちらを参考にしてください。
例えば、先ほどのコードに次のような部分があります。
putloop:
MOV AL, [SI]
ADD SI, 1 ; SIに1を足す
CMP AL, 0
JE fin
MOV AH, 0x0e ; 1文字表示ファンクション
MOV BX, 15 ; カラーコード
INT 0x10 ; ビデオBIOS呼び出し
JMP putloop
ここではINT 0x10
をしているのでBIOS interrupt callのVideo Serviceと呼ばれる関数を呼び出しています。また、(AT)BIOSより、MOV AH, 0x0e
をしているので、これが1文字表示ファンクションとして機能することになります。
では、BIOS interrupt callでLow Level Disk Serviceを呼び出して、フロッピーディスクにIMGの内容を読み込みます。次はipl.nasの一部です。
hanpen.nasからipl.nasにファイル名を変更しています。
; プログラム本体
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
MOV AH, 0x02 ; ディスク読み込み
MOV AL, 1 ; 1セクタ
MOV BX, 0
MOV DL, 0x00 ; Aドライブ
INT 0x13 ; ディスクBIOS呼び出し
JC error
fin:
HLT ; 何かあるまでCPUを停止させる
JMP fin ; 無限ループ
error:
MOV SI, msg
putloop:
MOV AL, [SI]
ADD SI, 1 ; SIに1を足す
CMP AL, 0
JE fin
MOV AH, 0x0e ; 1文字表示ファンクション
MOV BX, 15 ; カラーコード
INT 0x10 ; ビデオBIOS呼び出し
JMP putloop
msg:
DB 0x0a, 0x0a ; 改行を2つ
DB "load error"
DB 0x0a ; 改行を1つ
DB 0
RESB 0x7dfe-$ ; 0x7dfeまで0x00で埋める
DB 0x55, 0xaa ; 有効なMBRのシグネチャ
INT 0x13
はLow Level Disk Servicesで、(AT)BIOSより、MOV AH, 0x02
をしているのでこれはやはりディスクを読み込んでいることがわかります。
BIOSの機能でディスクの読み込めることが理解できたので10シリンダ分読み込むコードを書きます。
10シリンダ分しか読み込んでいないことを明示するため、ipl10.nasという名前に変更しました。
; hanpen-os
; TAB=4
CYLS EQU 10 ; CYLS = 10 とする
ORG 0x7c00 ; このプログラムがどこに読み込まれるのか
; 標準的なFAT12フォーマットフロッピーディスクのための記述
JMP entry
DB 0x90
DB "HELLOIPL" ; ブートセクタの名前(8byte)
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 "HANPEN-OS " ; ディスクの名前(11byte)
DB "FAT12 " ; フォーマットの名前(8byte)
RESB 18 ; とりあえず18byte予約する
; プログラム本体
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 ; Aドライブ
INT 0x13 ; ディスクBIOS
JNC next ; エラーが発生しなければnextへ
ADD SI, 1 ; SIに1を足す
CMP SI, 5 ; SIと5を比較
JAE error ; SI >= 5 だったらerrorへ
MOV AH, 0x00
MOV DL, 0x00 ; Aドライブ
INT 0x13 ; ドライブのリセット
JMP retry
next:
MOV AX, ES ; アドレスを0x200進める
ADD AX, 0x0020
MOV ES, AX ; ADD ES, 0x020 という命令がないので
ADD CL, 1 ; CLに1を足す
CMP CL, 18 ; CLと18を比較
JBE readloop ; CL <= 18 だったらreadloop
MOV 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へ
; OS本体のプログラムにJMPする
MOV [0x0ff0], CH
JMP 0xc200
fin:
HLT ; 何かあるまでCPUを停止させる
JMP fin ; 無限ループ
error:
MOV SI, msg
putloop:
MOV AL, [SI]
ADD SI, 1 ; SIに1を足す
CMP AL, 0
JE fin
MOV AH, 0x0e ; 1文字表示ファンクション
MOV BX, 15 ; カラーコード
INT 0x10 ; ビデオBIOS呼び出し
JMP putloop
msg:
DB 0x0a, 0x0a ; 改行を2つ
DB "load error"
DB 0x0a ; 改行を1つ
DB 0
RESB 0x7dfe-$ ; 0x7dfeまで0x00で埋める
DB 0x55, 0xaa ; 有効なMBRのシグネチャ
Makefile
OSを効率よくビルドするために、Makefileを利用します。
Makefileの説明で、「自作OS入門」の著者さんがわかりやすい説明をしているので引用します。
Makefileというのは、かなりかしこいバッチファイルのようなものです。
バッチファイルとは、ターミナルで使うコマンドを1つにまとめたファイルです。
例えば、コマンドの履歴をTXTとして保存するBATを作ってみます。
doskey /h > history.txt
このバッチファイルを実行してみると、このコマンドがターミナル上で実行されます。要するに、バッチファイルを利用することで、コマンドの入力時間を削減でき、関数のように扱うことができます。
そして、Makefileは条件を加えてコマンドを実行、また変数を使用して記述することができます。
次のコードはipl10.nasをアセンブルし、hanpen.imgとして出力するために作ったMakefileです。
TOOLPATH = ../z_tools/
MAKE = $(TOOLPATH)make.exe -r
NASK = $(TOOLPATH)nask.exe
EDIMG = $(TOOLPATH)edimg.exe
IMGTOL = $(TOOLPATH)imgtol.com
COPY = copy
DEL = del
# 目的のファイル
default :
$(MAKE) img
# 必要なファイルがあるか確認する
ipl.bin : ipl10.nas Makefile
$(NASK) ipl10.nas ipl10.bin ipl10.lst
hanpen.img : ipl10.bin Makefile
$(EDIMG) imgin:../z_tools/fdimg0at.tek \
wbinimg src:ipl10.bin len:512 from:0 to:0 imgout:hanpen.img
# ファイルの生成規則
asm :
$(MAKE) ipl10.bin
img :
$(MAKE) hanpen.img
run :
$(MAKE) img
$(COPY) hanpen.img ..\z_tools\qemu\fdimage0.bin
$(MAKE) -C ../z_tools/qemu
install :
$(MAKE) img
$(IMGTOL) w a: hanpen.img
# cleanコマンドで削除するファイルを指定
clean :
-$(DEL) *.bin
-$(DEL) *.lst
src_only :
$(MAKE) clean
-$(DEL) hanpen.img
このようにMakefileを記述することによって、cmdでmake run
を実行するだけでhanpen.imgが生成されるようになりました。また、make clean
ですべてのBINとLSTを消すことができます。
OBJの統合まで
C言語を使うには、CPUを32bitに移行しなければならなく、BIOSは16bitにしか対応していないのでBIOSでできることはasmhead.nasの前半ですべて終わらせました。
asmhead.nasはipl10.nasとは別に作られたOS本体のプログラムです。
; hanpen-os bootinfo
; TAB=4
BOTPAK EQU 0x00280000
DSKCAC EQU 0x00100000
DSKCAC0 EQU 0x00008000
; BOOT_INFO関係
CYLS EQU 0x0ff0 ; ブートセクタが設定する
LEDS EQU 0x0ff1
VMODE EQU 0x0ff2 ; 色数に関する情報。bit数。
SCRNX EQU 0x0ff4 ; 解像度のX
SCRNY EQU 0x0ff6 ; 解像度のY
VRAM EQU 0x0ff8 ; グラフィックバッファの開始番地
ORG 0xc200 ; このプログラムがどこに読み込まれるか
MOV AL, 0x13
MOV AH, 0x00 ; VGAグラフィックス, 320x200x8bitカラー
INT 0x10
MOV BYTE [VMODE], 8 ; 8bitカラー
MOV WORD [SCRNX], 320 ; 画面幅
MOV WORD [SCRNY], 200 ; 画面の高さ
MOV DWORD [VRAM], 0x000a0000
; キーボードのLED状態
MOV AH, 0x02
INT 0x16 ; keyboard BIOS
MOV [LEDS], AL
; PICが一切の割り込みを受け付けないようにする
; AT互換機の使用ではPICの初期化をするなら
; こいつをCLI前にやっておかないのと、たまにハングアップする
; PICの初期化は後でやる
MOV AL, 0xff
OUT 0x21, AL
NOP ; 念のため1クロック何もしない
OUT 0xa1, AL
CLI ; CPUレベルで割り込み禁止
; CPUから1MG以上のメモリにアクセスできるように、A20GATEを使用
CALL waitkbdout
MOV AL, 0xd1
OUT 0x64, AL
CALL waitkbdout
MOV AL, 0xdf ; enable A20
OUT 0x60, AL
CALL waitkbdout
; プロテクトモード移行
[INSTRSET "i486p"] ; i486までの設定を使いたいという記述
LGDT [GDTR0] ; 暫定GDTを設定
MOV EAX, CR0
AND EAX, 0x7fffffff ; bit31を0にする(ページング禁止)
OR EAX, 0x00000001 ; bit0を1にする(プロテクトモード移行)
MOV CR0, EAX
JMP pipelineflush
pipelineflush:
MOV AX, 1*8 ; 読み書き可能セグメント32bit
MOV DS, AX
MOV ES, AX
MOV FS, AX
MOV GS, AX
MOV SS, AX
; bootpackの転送
MOV ESI, bootpack
MOV EDI, BOTPAK
MOV ECX, 512*1024/4
CALL memcpy
; ついでにディスクデータも本来の位置に転送
; まずはブートセクタから
MOV ESI, 0x7c00 ; 転送元
MOV EDI, DSKCAC ; 転送先
MOV ECX, 512/4
CALL memcpy
; 残り全部
MOV ESI, DSKCAC0+512
MOV EDI, DSKCAC+512
MOV ECX, 0
MOV CL, BYTE [CYLS]
IMUL ECX, 512*18*2/4 ; シリンダ数からバイト数に変換
SUB ECX, 512/4 ; IPLの分だけ差し引く
CALL memcpy
; asmheadでしなければならないことは全部終わったので
; あとはbootpackに任せる
; bootpackの起動
MOV EBX, BOTPAK
MOV ECX, [EBX+16]
ADD ECX, 3
SHR ECX, 2
JZ skip ; 転送すべきものがない
MOV ESI, [EBX+20] ; 転送元
ADD ESI, EBX
MOV EDI, [EBX+12] ; 転送先
CALL memcpy
skip:
MOV ESP, [EBX+12] ; スタック初期値
JMP DWORD 2*8:0x0000001b
waitkbdout:
IN AL, 0x64
AND AL, 0x02
IN AL, 0x60 ; から読み(受信バッファが悪さしないように)
JNZ waitkbdout ; ANDの結果が0でなければwaitkbdoutへ
RET
memcpy:
MOV EAX, [ESI]
ADD ESI, 4
MOV [EDI], EAX
ADD EDI, 4
SUB ECX, 1
JNZ memcpy ; 引き算した結果が0でなければmemcpyへ
RET
ALIGNB 16
GDT0:
RESB 8 ; ヌルセレクタ
DW 0xffff, 0x0000, 0x9200, 0x00cf ; 読み書き可能セグメント32bit
DW 0xffff, 0x0000, 0x9a28, 0x0047 ; 実行可能セグメント32bit(bootpack用)
DW 0
GDTR0:
DW 8*3-1
DD GDT0
ALIGNB 16
bootpack:
前半の30行はCで使う情報(BOOT_INFO)をメモリに保存しています。BIOS interrupt callを使っているので中身が気になる方は(AT)BIOSを参照してご確認ください。
残りの100行はPICの割り込みなど、煩雑なので割愛します。
void HariMain(void) {
fin:
/* ここにHLTを入れたいが、C言語にはHLTがない */
goto fin;
}
32bitに移行して、C言語が使えるようになりましたが、上のコードからわかるようにC言語ではアセンブラのHLT
が使えないので次のような手順でIMGを出力します。
まず、cc1.exeを使って、bootpack.cからbootpack.gasを作る。
次にgas2nask.exeを使って、bootpack.gasからbootpack.nasを作る。
それでnask.exeを使って、bootpack.nasからbootpack.objを作る。
さらにobj2bim.exeを使って、bootpack.objからbootpack.bimを作る。
最後にbim2hrb.exeを使って、bootpack.bimからbootpack.hrbを作る。
これで機械語になったので、copyコマンドでasmhead.binとbootpack.hrbを単純にくっつけてharibote.sysとする。
簡潔には、CとNASをOBJに落とし込んでBIMに統合し、機械語にした後にSYSにする、ということです。Makefileを編集してこれを可能にします。
TOOLPATH = ../z_tools/
INCPATH = ../z_tools/haribote/
MAKE = $(TOOLPATH)make.exe -r
NASK = $(TOOLPATH)nask.exe
CC1 = $(TOOLPATH)cc1.exe -I$(INCPATH) -Os -Wall -quiet
GAS2NASK = $(TOOLPATH)gas2nask.exe -a
OBJ2BIM = $(TOOLPATH)obj2bim.exe
BIM2HRB = $(TOOLPATH)bim2hrb.exe
RULEFILE = $(TOOLPATH)haribote/haribote.rul
EDIMG = $(TOOLPATH)edimg.exe
IMGTOL = $(TOOLPATH)imgtol.com
COPY = copy
DEL = del
# 目的のファイル
default :
$(MAKE) img
# 必要なファイルがあるか確認する
ipl10.bin : ipl10.nas Makefile
$(NASK) ipl10.nas ipl10.bin ipl10.lst
asmhead.bin : asmhead.nas Makefile
$(NASK) asmhead.nas asmhead.bin asmhead.lst
bootpack.gas : bootpack.c Makefile
$(CC1) -o bootpack.gas bootpack.c
bootpack.nas : bootpack.gas Makefile
$(GAS2NASK) bootpack.gas bootpack.nas
bootpack.obj : bootpack.nas Makefile
$(NASK) bootpack.nas bootpack.obj bootpack.lst
naskfunc.obj : naskfunc.nas Makefile
$(NASK) naskfunc.nas naskfunc.obj naskfunc.lst
bootpack.bim : bootpack.obj naskfunc.obj Makefile
$(OBJ2BIM) @$(RULEFILE) out:bootpack.bim stack:3136k map:bootpack.map \
bootpack.obj naskfunc.obj
# 3MB+64KB=3136KB
bootpack.hrb : bootpack.bim Makefile
$(BIM2HRB) bootpack.bim bootpack.hrb 0
hanpen.sys : asmhead.bin bootpack.hrb Makefile
copy /B asmhead.bin+bootpack.hrb hanpen.sys
hanpen.img : ipl10.bin hanpen.sys Makefile
$(EDIMG) imgin:../z_tools/fdimg0at.tek \
wbinimg src:ipl10.bin len:512 from:0 to:0 \
copy from:hanpen.sys to:@: \
imgout:hanpen.img
# ファイルの生成規則
img :
$(MAKE) hanpen.img
run :
$(MAKE) img
$(COPY) hanpen.img ..\z_tools\qemu\fdimage0.bin
$(MAKE) -C ../z_tools/qemu
install :
$(MAKE) img
$(IMGTOL) w a: hanpen.img
clean :
-$(DEL) *.bin
-$(DEL) *.lst
-$(DEL) *.gas
-$(DEL) *.obj
-$(DEL) bootpack.nas
-$(DEL) bootpack.map
-$(DEL) bootpack.bim
-$(DEL) bootpack.hrb
-$(DEL) hanpen.sys
src_only :
$(MAKE) clean
-$(DEL) hanpen.img
Cにアセンブラの命令を加えることができるようになったのでnaskfunk.nasでCに加える関数を作ります。
; naskfunc
; TAB=4
[FORMAT "WCOFF"] ; オブジェクトファイルを作るモード
[BITS 32] ; 32bitモード用の機械語を作らせる
; オブジェクトファイルのための情報
[FILE "naskfunc.nas"] ; ソースファイル名表示
; このプログラムに含まれる関数名(プロトタイプ宣言のようなもの)
GLOBAL _io_hlt
; 以下は実際の関数
[SECTION .text] ; オブジェクトファイルはこれを書いてからプログラムを書く
_io_hlt: ; void io_hlt(void);
HLT
RET
Cに加える関数はnaskfunc.nasの中ではアンダーバーを先頭につけて宣言します。
RET
はC言語のreturn;
と同じ意味で返り値を持ちません。
そうしてできた関数をCで扱いますが、プロトタイプ宣言をしなければなりません。
/* プロトタイプ宣言 */
void io_hlt(void);
void HariMain(void) {
// for文を使うことにした
for(;;) {
io_hlt();
}
return;
}
描画関数とフォント
VRAMに直接値を書き込み、画面を描画するためにポインタを使います。次のコードで画面を白で塗り潰します。
char *i = 0xa0000;
for(i=0xa0000;i<=0xaffff;i++) {
*i = 15; // 15は色コードで白
}
画面モードでは8bitカラーなので8bit型のchar*
型を使っています。
このように、ポインタを使うことによってVRAMを直接書き込めます。
では、終点と始点の範囲の長方形を塗り潰すboxfill8()
関数を作ります。引数はvramの開始アドレス、画面の横幅(画素数)、色、始点と終点それぞれのx座標とy座標です。
void boxfill8(unsigned char *vram, int xsize, unsigned char c, int x0, int y0, int x1, int y1) {
int x, y;
for(y=y0;y<=y1;y++) {
for(x=x0;x<=x1;x++) {
vram[y*xsize+x] = c;
}
}
}
描画の過程については、まず始点(x0+0, y0)から順に(x0+1, y0), (x0+2, y0)...とx方向に埋めていき、(x1, y0)を埋めたら(x0, y0+1)に飛びます。ここに飛ぶには、まずアドレスを(x0, y0+1)のアドレスを取得する必要があるので、画面の横幅とy座標の積をとります。最終的には(x1, y1)を埋めます。これで終了です。
最後に、テキストを描画します。テキストはboxfill()
関数のように数学的に表現しやすい図形ではなく、それぞれ固有の形を持つので、c言語内で1文字ずつ描画するための関数を作る必要があります。しかしそれでは非常に非効率なので、TXTファイルで作成したフォントをある規則に従って関数にコンバートします。フォントをまとめたTXTはhankaku.txtというOSASKで使われているものを使用します。それを表示するためのputfont8()
関数がこちらです。
void putfont8(char *vram, int xsize, int x, int y, char c, char *font) {
int i;
char *p, d /* data */;
for(i=0;i<16;i++) {
p = vram + (y + i) * xsize + x;
d = font[i];
if((d & 0x80) != 0) { p[0] = c; }
if((d & 0x40) != 0) { p[1] = c; }
if((d & 0x20) != 0) { p[2] = c; }
if((d & 0x10) != 0) { p[3] = c; }
if((d & 0x08) != 0) { p[4] = c; }
if((d & 0x04) != 0) { p[5] = c; }
if((d & 0x02) != 0) { p[6] = c; }
if((d & 0x01) != 0) { p[7] = c; }
}
return;
}
フォントは16x8のサイズで作られており、同様に16x8の0と1からなる配列で表現されています。したがって、1bitごとにAND演算を行い、その結果が0でないなら塗り潰す、という手法が使えます。また、if文でAND演算に使っている16進数はそれぞれ0桁目~7桁目を表しています。
また、1文字ずつ出力するのではなく、文字列で表示させるためputfont8()
関数を利用してputfonts8_asc()
関数を作ります。
void putfonts8_asc(char *vram, int xsize, int x, int y, char c, unsigned char *s) {
extern char hankaku[4096];
for (;*s!=0x00;s++) {
putfont8(vram, xsize, x, y, c, hankaku + *s * 16);
x += 8;
}
return;
}
特に難しい部分はありません。
putfonts8_asc(binfo->vram, binfo->scrnx, 31, 31, COL8_000000, "HanpenOS");
putfonts8_asc(binfo->vram, binfo->scrnx, 30, 30, COL8_FFFFFF, "HanpenOS");
のように書いてしまえば(binfoはBOOTINFOのオブジェクト)、以下のように出力できます。
これでぐっとOS感が増したと思います。
さいごに
今回はこの辺で終わりにしようと思います。ここまで自分の手でぱっと作れてしまえそうなので、とてもわくわくします。今度解説するとしたら割り込み......私も半ば理解していないので記事にするのは当分先ですね。