2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

「自作OS入門」0日目から5日目まで

Last updated at Posted at 2023-02-27

最近徹夜で記事を書いていて昼夜逆転しています。なっしゅです。
「自作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で埋まっているので、なんてことないです。コピペを駆使します。
image.png
フロッピーディスクにマウントして実機で動かしてもいいのですが、少し面倒なのでQEMUというエミュレータを使って起動させることにします。

上手くいくとこのようにBIOSが起動します。
image.png
これでOSは完成です(!)
結局のところ、IMGが完成すればOSができたも同然なので、あとはこれをアセンブリ言語やC言語で拡張していくだけの話です。最終的にはIMGを作る、ということを念頭に置いて開発していきます。

Tips
最終的にはIMGを作ることを念頭に置く。

naskの命令とレジスタ

今度はバイナリエディタで直接IMGを編集するのではなく、naskというNASMを参考にして作られたアセンブラを使ってIMGを作っていきます。先ほどと全く同じIMGを作ります。

hanpenos.nas
	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命令を使って大幅にコードを削っています。まだ全くコードの意味が分からないので、さらに改良を加えます。

hanpenos.nas
; 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 ブートセクタ

あと意味不明なのはブートセクタを除いてプログラム本体なので、こちらも人間にわかりやすく書き換えます。

hanpenos.nas
; 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でフラグをスタックに入れる。

次に、AXSSなどはレジスタと呼ばれるものであり、機械語にとっての変数です。

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というページで上手くまとまっていますので、こちらを参考にしてください。

例えば、先ほどのコードに次のような部分があります。

hanpen.nasの一部
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にファイル名を変更しています。

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という名前に変更しました。

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を作ってみます。

com2txt.bat
doskey /h > history.txt

このバッチファイルを実行してみると、このコマンドがターミナル上で実行されます。要するに、バッチファイルを利用することで、コマンドの入力時間を削減でき、関数のように扱うことができます。

そして、Makefileは条件を加えてコマンドを実行、また変数を使用して記述することができます。
次のコードはipl10.nasをアセンブルし、hanpen.imgとして出力するために作ったMakefileです。

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本体のプログラムです。

asmhead.nas
; 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の割り込みなど、煩雑なので割愛します。

bootpack.c
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を編集してこれを可能にします。

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.nas
; 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で扱いますが、プロトタイプ宣言をしなければなりません。

bootpack.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*型を使っています。
image.png
このように、ポインタを使うことによってVRAMを直接書き込めます。
では、終点と始点の範囲の長方形を塗り潰すboxfill8()関数を作ります。引数はvramの開始アドレス、画面の横幅(画素数)、色、始点と終点それぞれのx座標とy座標です。

bootpack.cの一部
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()関数がこちらです。

bootpack.cの一部
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()関数を作ります。

bootpack.cの一部
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;
}

特に難しい部分はありません。

bootpack.cの一部
putfonts8_asc(binfo->vram, binfo->scrnx, 31, 31, COL8_000000, "HanpenOS");
putfonts8_asc(binfo->vram, binfo->scrnx, 30, 30, COL8_FFFFFF, "HanpenOS");

のように書いてしまえば(binfoはBOOTINFOのオブジェクト)、以下のように出力できます。
image.png
これでぐっとOS感が増したと思います。

さいごに

今回はこの辺で終わりにしようと思います。ここまで自分の手でぱっと作れてしまえそうなので、とてもわくわくします。今度解説するとしたら割り込み......私も半ば理解していないので記事にするのは当分先ですね。

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?