30日でできる!OS自作入門 まとめ

ようやく最後まで実装することができました。
今回は今まで書いてきた記事の目次のように使ってもらえるよう、
この本のレビューを兼ねて、書こうと思います。
 
 

 

まず、なんと言ってもこの書籍はかなりの良書です。
お持ちでない方のために簡単に内容を紹介すると、
シンプルなOS(HariboteOSと呼んでいます)
を30日間かけてつくり上げるという本です。

書籍にもありますが、付録にソースコード一式がついていて、
書籍はあくまでもその解説といった雰囲気です。

この本を買った当初、実装するのがめんどうだった私は、
「本を読む」ということでのみ、内容を理解しようと試みましたが、
無謀でした。

今思えば、手を動かして、ソースコードを読みながらでないと
理解は到底不可能だと思います。
現実、書籍ですべてが解説されているわけではないので、
当たり前なのですが。

その30日間で失敗も含めて徐々に作り上げていって、
OSって結局なんなの?
どんなことをしているの?
どういうソースコードで動いているの?
という疑問を解決してくれる本でした。

この本を読み始める前、私はプログラミングを初めて、
コンソールアプリ、ウインドウズアプリを少し作れるように
なったようなレベルでした。

一方、情報処理の勉強で、
・AND回路、OR回路
・2進数
・CPU、メモリ、ハードディスクの役割
・OSの役割
このような内容をうっすら学んだのですが、
プログラミングという行為と、これらの知識が
結びついておらず、間にぽっかり穴が開いているような状態でした。

HariboteOSを作り終えた今では
printfでコンソール画面に表示したり、
ウィンドウにボタンを作ったり、
mallocでメモリを確保したり、
このようなプログラミングをするなかで、
OSが内部でどのような動きをしているのかがイメージできるようになりました。

このような知識は何か問題が起こった時、
予期せぬ動作だった時に必要なんだと思います。

 
 
私はこのHariboteOSを
Linux上で更に筆者さんが作ったツールは使わないと決めて望みました。
※Linux(Ubuntu)、GCC、nasm、を使いました。

このおかげで時間を費やしてしまいましたが、
結果として自分の成長につながりました。

特にLinuxの実行フォーマットであるELFについては
我ながらかなりの知識が付いたと思っています。

おそらく、付録の通りにやっていれば、
大幅に短い時間で作れたかもしれませんが、
やることはひたすら「make run」だったと思います。

多くの人がおそらく私と同じような疑問を抱いていることだと思いますが、
自身を持ってオススメできる一冊です。
そして私と同じように、ぜひ異なる環境で実装することを強く推奨します。

以下は過去の記事の目次に使ってもらうために、
日毎にその概要を書いておきます。
 



 
 


1日目

結局、実行ファイルもテキストファイルもみな
バイナリ(0と1だけのデータ)であるということです。
つまりどんなプログラムだろうと、
どんなデータだろうと元はバイナリであって、
アセンブラやバイナリエディタを使えば、作れるということです。

そして1日目ではこの謎のバイナリデータが動きます。

 
 


2日目

1日目でつくったバイナリデータはこんな意味でしたっという
アセンブラのコードが出てきます。
実装する上ではFAT12フォーマットという仕組みを理解するのに苦労しました。
この時は気づいていませんでしたが、
あとあとこのフォーマットについての説明が出てきます。

 
 


3日目

書籍と異なる環境で実装する場合、最難関だと思います。
実際多くの挑戦者がここで脱落されているようです。
ブートローダーがメモリ上にOSの実行ファイルをマップして、
画面に表示するだけの簡単なOSを実行します。

私は64bit環境でやっていますが、
32bit用のコードを生成したり、
フォーマットを生成する部分に手こずりました。

このおかげでELFファイルの構造などの知識が格段にあがったような気がします。

 
 


4日目

画面の任意の点に表示する方法についてです。
結局はメモリ上のある空間に何を書き込むかということです。
そしてカラーパレットという仕組みも登場します。
ある色番号が何色に対応しているかを設定する表みたいなイメージでしょうか。

 
 


5日目

内容はGDT(グローバルディスクリプタテーブル)とです。
プロテクトモードではGDTを使って、メモリの管理を行います。
メモリをセグメントという単位に区分して管理します。
このおかげでアクセス管理が可能になります。

 
 


6日目

この章では割り込みを扱います。
マウスやキーボードなど、人間があるインターフェースを通して、
入力行為を行うと、「割り込み」が発生し、
割り込みハンドラに処理が移る仕組みです。

その割り込みのための設定を行っています。

ハードとソフトがリンクする、
個人的にはこの書籍のなかでも最大に面白かったと思います。

 
 


7日目

キーボード部分をより細かく実装していきます。
キーボードコントローラーと呼ばれるICに
設定を書き込んだり、読みだしたりします。

 
 


8日目

キーボードと同じくマウスについての設定を行います。
マウスからのコードがそれぞれどういう意味なのかを解読していきます。

 
 


9日目

個人的に面白かった章の第二位です。
ここではメモリ管理を実装します。
c言語で動的にメモリを確保する時に使う
malloc、freeなど直感的に分かりづらい関数が
実際どういう役割なのかがイメージできるようになりました。

 
 


10日目

シートという概念を取り入れ、画面を書く処理を実装していきます。
概念自体はそれほど難しくないのですが、
いろんな座標系が出てきて、頭がこんがってしまいます。
絵に書いてみることをオススメします。

 
 


11日目

画面描画処理のためにシートに加えてMAPという概念を導入します。
画面の一つのドットがどのシートによるものかを記録しておくものです。

 
 


12日目

タイマーによる割り込み処理を試しています。
PIT(Programmable Interval Timer)というものに
ある時間を設定することで
その都度割り込みが発生してくれます。

 
 


13日目

前章で作った処理を改良していきます。
タイマーは「時間を測る」ということだけでなく、
定期に実行すべき処理のために必須です。
後にマルチスレッドで活躍することになります。

 
 


14日目

VBEを使ってより解像度の高い画面が作れます。
この章によって見た目が格段に格好良くなりました。

 
 


15日目

TSS(タスクステートセグメント)を扱います。
タスクスイッチ、マルチスレッドのためには必要なのですが、
残念ながら理解が得られませんでした。

 
 


16日目

タスクスイッチを実装します。
タスクに優先度を持たせて、その優先度を加味したタスクスイッチです。

 
 


17日目

コンソールを作ります。
Windowsで言うところのコマンドプロンプト、
Linuxで言うところのターミナルを実装します。
私はこれに加えて、せっかくの解像度を活かして、
コマンドプロンプトを大きくする改造をしてみました。

 
 


18日目

メモリの状況を表示するmemコマンド
コンソールの内容を削除するclsコマンド
ファイルの内容を表示するdirコマンド
を実装します。

 
 


19日目

ここで書籍ではようやくFAT12フォーマットが出てきますが、
私のブログでは2日目にすべて説明しきってしまったので、
FAT12フォーマットについて知りたい方は2日目の記事をご覧ください。

 
 


20日目

アプリケーションから呼び出すAPIを実装しています。
処理が移り変わる順序が複雑で、理解し辛い部分でした。
それに加えて書籍と環境の違いによる躓きがありました。
おそらく、OS自作に取り組む前だったら解決できなかったと思います。
この章は一通り読み終えたのでもう少し深堀してみたいところです。

 
 


21日目

前章ではアセンブラで書いたアプリケーションを実行していました。
この章ではC言語で書いたアプリケーションを
HariboteOS上で動かします。
拡張子がhrbの独自フォーマットにコンパイルする必要がありますが、
リンカスクリプトで対応しています。

 
 


22日目

前章の続きでアプリケーションについて更に深く実装していきます。
hrb形式のフォーマットについての解説が出てきます。

 
 


23日目

GUIアプリケーションのようなメッセージループを持ったアプリを実装します。
直線的な処理ではなく、ユーザーと対話するようなアプリが作れるようになります。
Windowsアプリ開発に近い雰囲気です。

 
 


24日目

ウインドウに対してより細かく実装していきます。
ウインドウが押されたかどうかの判定、
ウインドウの切り替え、
「×」ボタンでウインドウが閉じる、
などができるように改造します。

 
 


25日目

この章ではいろいろと細かな部分の作り込みを行うのですが、
まずはカラーパレット。
最初のほうで16色まで使えるように設定していましたが、
256色まで使えるようにします。
そして、今まで一つだったコンソールを
複数個立ち上げるために改造します。

 
 


26日目

ウインドウの動きがモタついているので、
解消するために画面描画の処理部分を見なおしています。
難しいというよりは、描画処理を作ったのが前過ぎて、
思い出すのに苦労する感じです。

 
 


27日目

LDT(Local Descriptor Table)を実装します。
久しぶりに難解な代物が出てきました。
やはり図を書いてみるのが一番の理解への近道なように思います。
そして、ライブラリファイルを作ります。
Linux環境ではarコマンドを使います。

 
 


28日目

allocaを実装する章なのですが、
私の環境ではこれなしで動きました。
未だに原因不明です。
もうひとつはファイルAPIです。
ファイルをメモリ上に読み込み、操作するための処理を実装します。

 
 


29日目

tekという形式で圧縮されたファイルを取り扱えるようにします。
ここにきて、自分だけでは解決できない壁にぶち当たってしまったのですが、
twitterでアドバイスをもらってなんとか解決できました。

 
 


30日目

はっきりいって、作ってきたOS上にアプリをのせるだけなので、
記事にしていません。

 
 
 


最後に

これを30日で理解するのはまず不可能でしょう。
私の場合、11月から初めて4月までかかっていますので、
半年かけてゆっくり読み進めまたことになります。
しかし、それだけの期間を投資する価値は十二分にあると感じています。

繰り返しますが、
コンピューターがなぜ動くのか、
OSってどうやって動いているのか、
こういうところに疑問を感じている方には、
この本を強くオススメいたします。

オススメついでに

この本と合わせて読めば、
コンピューターがどうやって動いているのかを
理解できると思います。
私はこの本を読んでからOS自作にとりかかりましたが、
良い順序だったように思います。
 
 
 
 

OS自作入門29日目 【Linux】 | ファイル圧縮と簡単なアプリケーション

残すところあと僅か、OS自作29日目です。
結論から書いてしまうと残念ながらファイル圧縮については
実装することができませんでした。
詳細は下に書きますが、非常に悔しいです。

追記:アドバイスを頂き、実装できました。
詳細は一番下をご覧ください。

 
 
 


使用環境

$ uname -a
Linux ***-E200HA 4.10.0-37-generic #41-Ubuntu SMP Fri Oct 6 20:20:37 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
$ nasm -version
NASM version 2.13.01
$ qemu-system-i386 --version
QEMU emulator version 2.10.1(Debian 1:2.10+dfsg-0ubuntu13)
$ gcc --version
gcc (Ubuntu 5.3.1-14ubuntu2) 5.3.1 20160413

 
 
 


ファイル圧縮

著者さん作のコンパイラを使っていない関係上、
今までもsprintfやstrcmp関数を自作してしのいできたのですが、
今回ばかりは無理でした。

まず、何も考えずにmake runしてみると、
以下のようなエラーがでます。

tek.o: 関数 `tek_getsize' 内:
tek.c:(.text+0x6a): `memcmp' に対する定義されていない参照です
tek.o: 関数 `tek_rdget1' 内:
tek.c:(.text+0xb4b): `longjmp' に対する定義されていない参照です
tek.o: 関数 `tek_decmain5' 内:
tek.c:(.text+0x10c5): `_setjmp' に対する定義されていない参照です
Makefile:22: ターゲット 'bootpack.bin' のレシピで失敗しました

 
 
memcmp、longjmp、setjmpがないよと怒られます。
memcmpくらいなら今までのように自作可能なのですが、
longjmpやsetjmpは今の私の技量では到底自作できません。
おそらく自作しようとするとアセンブリで書く必要がありそうです。

しばらく悩んでいたのですが、
ばっさり諦めました。

ファイル圧縮はなくてもOSの機能自体に問題はないし、
なんとなくですが、圧縮の仕組みは想像できる。
、、、という言い訳の元、さっさと先に進むことにしました。

まあ、一週間くらい悩んだんですが。
 
 
 


簡単なアプリケーション

ここまでバッチリ動いてくれると感慨深いですね。
残すところ、後1日です。
OS自作が終わったら、次は何に手を出そうか悩みますねー。

今のところの候補は
・Linuxのソースを読んで見る
・バイナリ解析
・ネットワーク系(http通信など)
くらいですかね。

 
 
 


追記:__builtin_memcmp、__builtin_longjmp、__builtin_setjmp

twitterで@sksat_ttyさんにアドバイスを頂き、
__builtin_memcmp
__builtin_longjmp
__builtin_setjmp
なんてものを知りました。

結局、tek.cのmemcmp、longjmp、setjmpを
上記の関数に置き換えることで、
longjmpとsetjmpについては問題なくリンクできました。
memcmpは自分で実装して対応しました。

#include <stddef.h>

int memcmp(const void *p1, const void *p2, size_t n) {
	const unsigned char *pp1 = (const unsigned char *)p1;
	const unsigned char *pp2 = (const unsigned char *)p2;

	for (int i = 0; i > n; i++) {
		if (*pp1 != *pp2) return *pp1 - *pp2;
		if (i == n) return 0;
		pp1++; pp2++;
	}
}

あらためて@sksat_ttyさん、ありがとうございました。

【関連記事】
OS自作入門 1日目
OS自作入門 3日目-2
OS自作入門28日目
30日でできる!OS自作入門 まとめ

OS自作入門28日目 【Linux】 | alloca、ファイルAPI

何故かWifiの調子が悪く、なかなか更新できませんでした。
OS自作28日目の内容はalloca、ファイルAPI、日本語表示と
内容盛りだくさんです。
個人的に興味のある内容なのでじっくりと進めたいと思います。
 
 
 


使用環境

$ uname -a
Linux ***-E200HA 4.10.0-37-generic #41-Ubuntu SMP Fri Oct 6 20:20:37 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
$ nasm -version
NASM version 2.13.01
$ qemu-system-i386 --version
QEMU emulator version 2.10.1(Debian 1:2.10+dfsg-0ubuntu13)
$ gcc --version
gcc (Ubuntu 5.3.1-14ubuntu2) 5.3.1 20160413

 
 
 


alloca

おそらく、WindowsやLinuxではページングというメモリの管理方法を使っていて、
その管理方法ではメモリ空間を4KB単位で管理しているため、
スタックを4KB以上使う場合にallocaという関数が呼ばれるのだと思います。

HariboteOSでは、セグメントでのメモリ管理なので、
本来のallocaの持っている役割(4KBをページを追加で確保する)を
実装する必要はなく、単純にEAX分のメモリ空間を追加で確保すれば良いようです。

ただ、、、
結論から書くと、私の環境ではallocaがなくても動いてしまいます。
原因は定かではないのですが、
私の環境でallocaは呼ばれていないようです。

ただし、普通に実行すると、
セグメントのアクセス違反で落ちますので、
スタックの確保領域のみリンカスクリプトを変更しています。

allocaがどのようなタイミングで呼ばれるのかは
理解できていないのですが、
リンカスクリプトを以下のように変更することで、
動きました。

 
 
変更前のリンカスクリプト(api.ls)

OUTPUT_FORMAT("binary");

SECTIONS
{
	.head 0x0 : {
		LONG(128 * 1024)
		LONG(0x69726148)
		LONG(0)
		LONG(0x400)
		LONG(SIZEOF(.data))
		LONG(LOADADDR(.data))
		LONG(0xE9000000)
		LONG(HariMain - 0x20)
		LONG(24 * 1024)
	}

	.text : { *(.text) }

	.data 0x0400 : AT ( ADDR(.text) + SIZEOF(.text) ) {
		*(.data)
		*(.rodata)
		*(.bss*)
	}

	/DISCARD/ : { *(.eh_frame) }
}


 
 
変更後のリンカスクリプト(api.ls)

OUTPUT_FORMAT("binary");

SECTIONS
{
	.head 0x0 : {
		LONG(128 * 1024)
		LONG(0x69726148)
		LONG(0)
		LONG(0x2c00)
		LONG(SIZEOF(.data))
		LONG(LOADADDR(.data))
		LONG(0xE9000000)
		LONG(HariMain - 0x20)
		LONG(24 * 1024)
	}

	.text : { *(.text) }

	.data 0x2c00 : AT ( ADDR(.text) + SIZEOF(.text) ) {
		*(.data)
		*(.rodata)
		*(.bss*)
	}

	/DISCARD/ : { *(.eh_frame) }
}

書籍ではスタック用の領域をmakeするときに指定するような方法で、
何も指定しない場合は1KBを確保するようになっています。

私が使ったリンカスクリプトでは元々もっと大きなサイズ
(128 * 1024) * 4KBを確保するようにはなっていたのですが、
スタックの初期値(底と表現したほうがよいかもしれません)に問題があります。

元々はスタックの初期値が0x0400でしたが、
sosu2において同じ状態で実行するとセグメントアクセス違反が発生します。

> sosu2
2
INT OD :
 General Protected Exception.
EIP : 79

>

こんな状態になります。

何が問題かというと、
データ領域(スタック領域、.dataなどを含めた領域)自体は
大きくとっていても、スタックの初期値が浅すぎる(小さすぎる)ため
このような状態になってしまったようです。

そこで0x2c00まで開始位置を大きく撮ってあげることで、
無事に動きました。

ちなみに.dataセクションの開始位置もその分後ろにずらしています。

 
 
 


ファイルAPI

fopen、freadなどファイルを扱うAPIを作ります。
fwriteがないのが物足りないのですね。

こんなファイルをイメージファイルにくっつくてtypeiplしてみました。
text.txt

abcde

イメージファイルには以下のようにして連結します。


mformat -f 1440 -C -B ipl.bin -i os.img ::
mcopy -i os.img os.sys ::
mcopy -i os.img api/typeipl.hrb ::
mcopy -i os.img text.txt ::

 
 
これで実行すると、

なんなく成功です。
typeiplという名前がいまいち合っていないですが、
あまり大した意味はないので良しということで。

追加した新しいファイルAPIはファイルハンドル構造体を使って
様々な処理を行っています。

struct FILEHANDLE {
	char *buf; //マップした位置
	int size; //サイズ
	int pos; //現在参照しているの位置
};

そして各々のファイルAPIは以下のような処理を行っています。
・api_fopen
ファイルをメモリ空間にマッピングして、
buf:先頭アドレス
size:ファイルのサイズ
pos:現在の参照位置として”0″
をそれぞれセットしています。

・api_fclose
メモリを解放して、
buf:0
をセットしています。

・api_fseek
posに任意のオフセット(ファイル先頭との相対位置)をセットしています。

・api_fsize
与えられたファイルハンドルが指し示しているファイルのサイズを返します。

・api_fread
与えられたファイルハンドルが指し示しているファイルの内容を
引数として与える変数に格納します。

fwriteの処理を作っていないのが物足りないですね。

 
 
 


日本語対応

文字コードについては勉強しなくちゃなーと思っていたので、
良い機会だと思ったのですが、
触れてみるとかなり奥が深そうなので、
また別の記事に書きたいと思います。

HariboteOSはこんな感じでうまく行っています。

【関連記事】
OS自作入門 1日目
OS自作入門 3日目-2
OS自作入門27日目
OS自作入門29日目
30日でできる!OS自作入門 まとめ

OS自作入門27日目 【Linux】 | LDT(Local Descriptor Table)

27日目の主な内容はLDT(Local Descriptor Table)と
APIのライブラリ化です。
環境の違いによる躓きがありそうな予感です。

 
 
 


使用環境

$ uname -a
Linux ***-E200HA 4.10.0-37-generic #41-Ubuntu SMP Fri Oct 6 20:20:37 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
$ nasm -version
NASM version 2.13.01
$ qemu-system-i386 --version
QEMU emulator version 2.10.1(Debian 1:2.10+dfsg-0ubuntu13)
$ gcc --version
gcc (Ubuntu 5.3.1-14ubuntu2) 5.3.1 20160413

 
 
 


LDT(Local Descriptor Table)

この部分の解読にはかなりの時間を要してしまいました。
あらたにLDTという仕組みを使うのですが、
なかなか難解です。

結局のところ今までと何が違うのかというと、
今まではGDT(Global Descriptor Table)のエントリが指し示していたのは、
アプリケーションのコードセグメント、データセグメントそのものでした。

もう少し具体的に言うと、GDTの
1005番目のエントリは1個目のコンソールに属するアプリケーションのコードセグメントを、
2005番目のエントリは1個目のコンソールに属するアプリケーションのデータセグメントを
指し示し、
1006番目のエントリは2個目のコンソールに属するアプリケーションのコードセグメントを、
2006番目のエントリは2個目のコンソールに属するアプリケーションのデータセグメントを
指し示していました。
 
 
ここにLDTを導入してどのように変わったのか。
1005番目のエントリは1個目のLDTを指していて、
LDTの1番目のエントリがアプリケーションのコードセグメントを、
LDTの2番目のエントリがアプリケーションのデータセグメントを、
指し示しています。

さらに1006番目のエントリは2個目のLDTを指していて、
LDTの1番目のエントリがアプリケーションのコードセグメントを、
LDTの2番目のエントリがアプリケーションのデータセグメントを、
って感じで続いていきます。

つまりタスク毎(コンソール1個ずつに)にLDTを持っていて
その中でコードの領域とデータの領域にわかれているような構造をしています。

言葉で書いても分かりづらいので、図にしてみました。
まずLDTを導入していない状態の図です。

 
次にLDTを導入した時の図です。

 
 
 


ライブラリファイルの作成

api001、api002・・・をライブラリにまとめる手順なのですが、
書籍とは全く異なるやり方なので順番に説明していきます。

Linuxの場合はStatic(静的)ライブラリとでもいうのでしょうか。
arというコマンドで作ることができます。

まずapi001.nas、api002.nas・・・の
拡張子をnas→asmに変更しますが、一つづつ変更するのはめんどうなので、

$ for i in *.nas ; do mv $i ${i/nas/asm} ; done

これで一発変換できます。

次にソースコードをnasm用のコードに変換します。
私はテキストエディターにVimを使っているので、マクロ機能を使ってチートしました。

【変更前】

;api001.nas
[FORMAT "WCOFF"]
[INSTRSET "i486p"]
[BITS 32]
[FILE "api001.nas"]

		GLOBAL	_api_putchar

[SECTION .text]

_api_putchar:	; void api_putchar(int c);
		MOV		EDX,1
		MOV		AL,[ESP+4]		; c
		INT		0x40
		RET

【変更後】

;api001.asm
GLOBAL	api_putchar

SECTION .text

api_putchar:	; void api_putchar(int c);
		MOV		EDX,1
		MOV		AL,[ESP+4]		; c
		INT		0x40
		RET


同様ににすべてのファイルを変換します。

 
 
次にnasmを作って32bit用ELFフォーマットのオブジェクトファイルを作ります。

$ nasm -f elf32 -o api001.o api001.asm

すべてのファイルについて行い、ディレクトリ内にapi001.oからapi019.oが
存在する状態にします。
 
 
次にこれらをまとめたライブラリファイルを作ります。

$ ar r apilib.a *.o

 
 
ちなみにこのコマンドでライブラリの中身が見れます。

$ ar t apilib.a
api001.o
api002.o
api003.o
api004.o
api005.o
api006.o
api007.o
api008.o
api009.o
api010.o
api011.o
api012.o
api013.o
api014.o
api015.o
api016.o
api017.o
api018.o
api019.o

思い通りのライブラリファイルが作成できていることが分かります。
 
 
最後にこのライブラリファイルを使って、他のオブジェクトファイルとリンクして、
HRB形式のファイルを作ってみます。

$ ld -m elf_i386 -e HariMain -o color.hrb -Tapi.ls color.o apilib.a

 
 
ライブラリファイルは思ったより簡単でしたが、
LDTの方に思いのほか時間を費やしてしまいました。
残り少ないOS自作を最後まで楽しみたいと思います。

【関連記事】
OS自作入門 1日目
OS自作入門 3日目-2
OS自作入門26日目
OS自作入門28日目
30日でできる!OS自作入門 まとめ

OS自作入門26日目 【Linux】 | ウインドウの高速化

30日でできる!OS自作入門、26日目の記事です。
分厚い本も残すところ5日です。
ページが開いたままにできないことで、
進捗を感じています。

ここまでOSを作ってきて、
LinuxやWindowsはどうなっているんだろうという
好奇心が湧いて来たので、
横道にそれてしまいそうなのですが、
そんな気持ちは一旦しまっておいて、
今はHariboteOSを完成させることに一旦専念します。



 
 
 


使用環境

$ uname -a
Linux ***-E200HA 4.10.0-37-generic #41-Ubuntu SMP Fri Oct 6 20:20:37 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
$ nasm -version
NASM version 2.13.01
$ qemu-system-i386 --version
QEMU emulator version 2.10.1(Debian 1:2.10+dfsg-0ubuntu13)
$ gcc --version
gcc (Ubuntu 5.3.1-14ubuntu2) 5.3.1 20160413

 
 
 


ウインドウ移動の高速化

実際、これらの改善を行うことで、
劇的に操作間が変わりました。
アルゴリズムって大事ですね。

やっている方策は大きく分けて3つです。
・2重ループ内でのifによる条件分岐を避ける。
・メモリへのアクセス回数を減らす。
・画面を描く処理のタイミングを変更する。
 
 
まず、改善前は2重ループ内で、1ピクセルが透明であるかないか
の条件分岐が存在します。
sheet構造体のメンバであるcol_invが-1であるかを見ているわけですが、
実際、col_invが-1でないのはマウスのシートだけです。
なので、マウス用とその他と処理を分けたイメージです。

ちなみに、col_invって何だっけ?と思ったのは私だけかもしれませんが、
透明色の色番号を決める変数です。
マウス用のシートではこの値が99になっていて、
透明のピクセルには99が入っています。
 
 
次に、メモリへのアクセス回数を減らすために、
1byteずつの書き込みから4byteずつの書き込みに変更しています。
CPUの処理に比べて、メモリへの読み書きは遅いので、
こういう部分を改善することに意味があるということですね。
 
 
最後に画面を描く処理のタイミングですが、
fifoのデータをすべて処理してから画面を描くように変更しています。

new_wx, new_wyという新しい変数を準備して、
・fifoが空になったとき
・移動が終わった直後(左クリックを離したとき)
に画面を書き換えています。

 
 
 


コンソールの動的確保

コンソールは2つと決め打ちなコーディングでしたが、
ここで、任意の数に増やせるように改造しています。

以下の関数を新たに作って、
増やしたり、減らしたりを可能にしています。
私なりの言葉でコメントを書いておきます。

struct TASK *open_constask(struct SHEET *sht, unsigned int memtotal)
{
	struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR; //MEMMANの開始アドレスを再定義しているだけ
	struct TASK *task = task_alloc(); //タスク確保
	int *cons_fifo = (int *) memman_alloc_4k(memman, 128 * 4); //FIFOバッファの領域を確保
	task->cons_stack = memman_alloc_4k(memman, 64 * 1024); //stack領域を確保
	task->tss.esp = task->cons_stack + 64 * 1024 - 12; //以下TSS構造体にセグメント等をセット
	task->tss.eip = (int) &console_task;
	task->tss.es = 1 * 8;
	task->tss.cs = 2 * 8;
	task->tss.ss = 1 * 8;
	task->tss.ds = 1 * 8;
	task->tss.fs = 1 * 8;
	task->tss.gs = 1 * 8;
	*((int *) (task->tss.esp + 4)) = (int) sht;
	*((int *) (task->tss.esp + 8)) = memtotal;
	task_run(task, 2, 2); /* level=2, priority=2 */ //タスク開始
	fifo32_init(&task->fifo, 128, cons_fifo, task); //FIFOバッファとタスクを関連付ける
	return task;
}

struct SHEET *open_console(struct SHTCTL *shtctl, unsigned int memtotal)
{
	struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR; //MEMMANの開始アドレスを再定義しているだけ
	struct SHEET *sht = sheet_alloc(shtctl); //シートの確保
	unsigned char *buf = (unsigned char *) memman_alloc_4k(memman, 256 * 165); //シート用のバッファを確保
	sheet_setbuf(sht, buf, 256, 165, -1); //バッファとシートを関連付ける
	make_window8(buf, 256, 165, "console", 0); //windowを描く
	make_textbox8(sht, 8, 28, 240, 128, COL8_000000); //windowを描く
	sht->task = open_constask(sht, memtotal);
	sht->flags |= 0x20;
	return sht;
}

void close_constask(struct TASK *task)
{
	struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR; //MEMMANの開始アドレスを再定義しているだけ
	task_sleep(task);
	memman_free_4k(memman, task->cons_stack, 64 * 1024); //タスクのスタック領域を解放
	memman_free_4k(memman, (int) task->fifo.buf, 128 * 4); //タスクのFIFOバッファ領域を解放
	task->flags = 0; //タスク解放
	return;
}

void close_console(struct SHEET *sht)
{
	struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR; //MEMMANの開始アドレスを再定義しているだけ
	struct TASK *task = sht->task;
	memman_free_4k(memman, (int) sht->buf, 256 * 165); //シート用のメモリ解放
	sheet_free(sht);
	close_constask(task);
	return;
}

もう少し簡単にまとめると以下のようになります。

open_constask()
…タスク、FIFOバッファを確保してタスクを開始する

open_console()
…シートを確保してからopen_constask()

close_constask()
…タスク、FIFOバッファを解放

close_console()
…シートを解放してclose_constask()

 

 
【関連記事】
OS自作入門 1日目
OS自作入門 3日目-2
OS自作入門25日目
OS自作入門27日目
30日でできる!OS自作入門 まとめ

イマドキの手に職って、ITエンジニアだと思う【IT派遣テクノウェイブ】

OS自作入門25日目 【Linux】 | カラーパレットその2

OS自作入門25日目の記事です。



 
 
 


使用環境

$ uname -a
Linux ***-E200HA 4.10.0-37-generic #41-Ubuntu SMP Fri Oct 6 20:20:37 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
$ nasm -version
NASM version 2.13.01
$ qemu-system-i386 --version
QEMU emulator version 2.10.1(Debian 1:2.10+dfsg-0ubuntu13)
$ gcc --version
gcc (Ubuntu 5.3.1-14ubuntu2) 5.3.1 20160413

 
 
 


カラーパレットへの色の追加

これまで、16色だけでグラフィックを描いてきたのですが、
ここにきて、色を追加するようです。

カラーパレット、、、さっぱり忘れました。
ブログにまとめていてよかったです。
おそらく、このブログを読んでくださっている読者の方も、
忘れているはず。

ということで4日目の記事にさらっと目を通してから、
読んでいただければ、さくっと理解できると思います。

4日目までで、カラーパレットがどうなっているかというと、

色番号 0 = RGB(#FFFF00)
色番号 1 = RGB(#0000FF)
色番号 2 = RGB(#00FFFF)
・・・
色番号 15 = RGB(#848484)

こんな感じで16番まで設定が完了しています。

今回はこれを16色から232色にまで増やそうということです。

色番号 0 = RGB(#FFFF00)
色番号 1 = RGB(#0000FF)
色番号 2 = RGB(#00FFFF)
・・・
色番号 15 = RGB(#848484)
色番号 16 = RGB(#000000)
色番号 17 = RGB(#330000)
色番号 18 = RGB(#660000)
色番号 19 = RGB(#990000)
色番号 20 = RGB(#CC0000)
・・・
色番号231 = RGB(#FFFFFF)

作成したパレットはこんな感じになります。

 
 

set_palette(0, 15, table_rgb); //元々の16色をカラーパレットに定義
for (b = 0; b < 6; b++) {
	for (g = 0; g < 6; g++) {
		for (r = 0; r < 6; r++) {
			table2[(r + g * 6 + b * 36) * 3 + 0] = r * 51;
			table2[(r + g * 6 + b * 36) * 3 + 1] = g * 51;
			table2[(r + g * 6 + b * 36) * 3 + 2] = b * 51;
		}
	}
}
set_palette(16, 231, table2); //新しく追加する色を定義


ソースコードはこんな感じになっています。
51という数字がとこから出てきたのかというと、
#00FFFFのようなフルカラー(rgbがそれぞれ256段階)ではなく、
今回はその1/5の各6段階なので、
255 / 5 = 51

ここから出てきた51です。

 
 
 


コンソールを増やす

次にコンソールを増やす実装に移っていくのですが、
タスク、シートの関係がごちゃごちゃしてきたので、
私の頭の整理を兼ねて、メモしておきます。

まず、タスクは以下のようになります。
・task_a
・task_cons[0]
・task_cons[1]

これら3つのタスクはそれぞれfifoバッファを持っていて、
task_aは静的に確保したfifoバッファを
task_cons[0]、[1]は動的に確保したfifoバッファをそれぞれ持っています。

次はシートです。
シートにはタスクが割り当てられていて
・sht_back→0
・sht_cons[0]→task_cons[0]
・sht_cons[1]→task_cons[1]
・sht_mouse→0

タスクが0の場合はtask_aが処理をすることになります。

まとまりのない記事になってしまいましたが、
OS自作ももう少しで完成です。
ここまで読んでくださるみなさんに感謝です。
 
 

 
【関連記事】
OS自作入門 1日目
OS自作入門 3日目-2
OS自作入門24日目
OS自作入門26日目
30日でできる!OS自作入門 まとめ

OS自作入門24日目 【Linux】 | ウインドウの制御

いよいよ終盤に差し掛かってきました。
OS自作入門24日目の記事です。



 
 
 


使用環境

$ uname -a
Linux ***-E200HA 4.10.0-37-generic #41-Ubuntu SMP Fri Oct 6 20:20:37 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
$ nasm -version
NASM version 2.13.01
$ qemu-system-i386 --version
QEMU emulator version 2.10.1(Debian 1:2.10+dfsg-0ubuntu13)
$ gcc --version
gcc (Ubuntu 5.3.1-14ubuntu2) 5.3.1 20160413

 
 
 


ウインドウがクリックされたかどうかの判定

24日目の”b”では、ウインドウの一部をクリックすると、
そのウインドウ最上面に移動するような処理を作ります。

シートの上下は関数一つなので簡単なのですが、
座標の判定が少しややこしいです。

//23日目, "b", bootpack.cより
for (j = shtctl->top - 1; j > 0; j--) {
	sht = shtctl->sheets[j];
	x = mx - sht->vx0; //画面座標系からシート座標系へ
	y = my - sht->vy0; //画面座標系からシート座標系へ
	if (0 <= x && x < sht->bxsize && 0 <= y && y < sht->bysize) { //マウスの位置がウインドウの範囲なら
		if (sht->buf[y * sht->bxsize + x] != sht->col_inv) { //透明じゃなければ
			sheet_updown(sht, shtctl->top - 1);
			break;
		}
	}
}

mx, my : 画面座標系でのマウスの位置
sht->vx0, sht->vy0 : シート左上の画面座標系での位置
sht->bxsize, sht->bysize : シートのサイズ

まずはmx, myは画面座標系なので、シート座標系に変換します。
そしてその座標が0以上、シートのサイズ未満なら、
かつ、その部分が透明でなければ、
そのシートを最上面の一つ下(一番上はマウスのシート)にする。

日本語で書くならこんな感じでしょうか。
余計にわかりづらくなったような気もしますが、、、

 
 
 


メッセージをウインドウ宛にする

23日目に書いたとおり、
これまではket_toという変数を見て、
コンソールとtask_aにキーボードやマウスのメッセージの
送り先を変えていましたが、
24日目では、フォーカスのあたっているウインドウに対して、
メッセージを送るように変更しています。

key_winという変数が、フォーカスのあたっているウインドウを
示しています。
正確に言うと、ウインドウが描かれているシート構造体へのポインタです。

そして”tab”キーを押すたびに、
フォーカスのあたっているウインドウを変更しつつ、このkey_winの値を
変えています。

そして、シート構造体にはtaskというメンバを作って、
そのシートに関連するタスクがわかるようにしています。

キーボードが押されたら、
・どのウインドウ(シート)にフォーカスがあたっているか。
・そのシートのタスクはどれか。
を確認して、そのタスクが持つfifoバッファに、
メッセージを届けているようなイメージです。

 
 

【関連記事】
OS自作入門 1日目
OS自作入門 3日目-2
OS自作入門23日目
OS自作入門25日目
30日でできる!OS自作入門 まとめ

OS自作入門23日目 【Linux】 | メッセージループ

OS自作入門23日目の内容です。
23日目の内容自体に難しいところは特にないのですが、
今までやってきたことがちょくちょく出てきて、
その内容を忘れているので、思い出すのに苦労しました。

特にキー入力部分が、いろんな関数を経由していて
分かりづらかったので、メモしておきます。



 
 
 


使用環境

$ uname -a
Linux ***-E200HA 4.10.0-37-generic #41-Ubuntu SMP Fri Oct 6 20:20:37 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
$ nasm -version
NASM version 2.13.01
$ qemu-system-i386 --version
QEMU emulator version 2.10.1(Debian 1:2.10+dfsg-0ubuntu13)
$ gcc --version
gcc (Ubuntu 5.3.1-14ubuntu2) 5.3.1 20160413

 
 
 


キー入力を捉える

中でも難しく感じたのが、23日目の”f”から出てくる、
キー入力の部分です。
Windowsでいうところのメッセージループを作って、
入力されたキーによって処理を変えています。

今までにやったFIFO、マルチタスク、シートコントロールが
フル活用されていて、
忘れた部分を思い出す必要があります。

アプリケーションに対してどのように割り込みのデータが
送られるのかを追いかけてみます。

 
 
 


キー入力がアプリケーションに送られるまで

まずキーボードが押されると、
割り込みが起こり制御は以下のように移っていきます。
 

asm_inthandler21→inthandler21

inthandlerの処理でfifoバッファにスキャンコード+256の値が入れられます。
なぜ256を足しているのかというと、
このfifoバッファはマウスやタイマー割り込みも入ってくるので、
これらを識別するためです。

次にbootpack.cのHariMain関数内にある
メッセージループ内で、fifoからデータが取得されます。
このデータの行き先は、変数key_toの値によって
条件分岐します。

変数key_toとはデータの行き先を変えるためのもので、
Tabキーを押すことで、コンソール行きとtask_a行きに変更できます。

今回はアプリケーションへの転送を追いたいので、
このコンソール行きになっていたものとして考えます。
つまりkey_toの値は1だったとします。

ソースコードでいうと以下の部分です。

if (key_to == 0) {
	if (cursor_x < 128) {
		s[1] = 0;
		putfonts8_asc_sht(sht_win, cursor_x, 28, COL8_000000, COL8_FFFFFF, s, 1);
		cursor_x += 8;
	}
} else {	
	fifo32_put(&task_cons->fifo, s[0] + 256); //←ここでコンソールタスクのfifoバッファに送信
}


 
 

次にアプリ側のソースを見てみます。

for (;;) {
	if (api_getkey(1) == 0x0a) {
		break;
	}
}


永久ループの中でapi_getkey()を呼び出して、
0x0a = Enterキー であれば終了するようなコードです。

api_getkey()はアセンブラのコードを介して、
console.cの中の以下の部分を呼び出しています。

} else if (edx == 15) {
	for (;;) {
		io_cli();
		if (fifo32_status(&task->fifo) == 0) {
			if (eax != 0) {
				task_sleep(task);
			} else {
				io_sti();
				reg[7] = -1;
				return 0;
			}
		}
		i = fifo32_get(&task->fifo); //fifoからもらってくる。
		io_sti();
		if (i <= 1) { 
			timer_init(cons->timer, &task->fifo, 1);
			timer_settime(cons->timer, 50);
		}
		if (i == 2) {
			cons->cur_c = COL8_FFFFFF;
		}
		if (i == 3) {
			cons->cur_c = -1;
		}
		if (256 <= i && i <= 511) {
			reg[7] = i - 256; //←もともとキーコードに256を足しているので、元に戻している。
			return 0;
		}
	}
}


コメントを入れた場所、fifo32_get関数で、コンソールタスクのfifoバッファから、
データをもらってきています。
256以上511以下なら、キーボードのデータということになるので、
reg[7]つまりeaxレジスタにi-256の値を戻り値として書き込んでいます。

【関連記事】
OS自作入門 1日目
OS自作入門 3日目-2
OS自作入門22日目
OS自作入門24日目
30日でできる!OS自作入門 まとめ

OS自作入門22日目 【Linux】 | RETF命令でアプリケーションにジャンプ

最近このブログにちょくちょくコメントを頂くことがあって、
大変嬉しく思っています。

私はプロのプログラマーでもありませんし、
文章を書くプロでもありません。

記事に誤りがあったり、文章がおかしい部分があったり、
分かりづらいところがあると思いますので、
気軽にコメントいただけると嬉しく思います。

ここおかしいんじゃない?
こうやったほうがいいんじゃない?
こんなこと試してほしい!
とかなんでも良いのでコメントをお待ちしております。




 
 
 

さて、今回はOS自作22日目の記事です。
といいながら21日目、22日目の内容で
私が抱いた細かい疑問を一つ一つメモしておきます。
 
 
 


使用環境

$ uname -a
Linux ***-E200HA 4.10.0-37-generic #41-Ubuntu SMP Fri Oct 6 20:20:37 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
$ nasm -version
NASM version 2.13.01
$ qemu-system-i386 --version
QEMU emulator version 2.10.1(Debian 1:2.10+dfsg-0ubuntu13)
$ gcc --version
gcc (Ubuntu 5.3.1-14ubuntu2) 5.3.1 20160413

 
 
 


RETF命令でアプリにジャンプする

21日目の”g”でnaskfunc.nasのstart_appに以下のような部分があります。

start_app:		; void start_app(int eip, int cs, int esp, int ds, int *tss_esp0);
		PUSHAD	
		MOV		EAX,[ESP+36]	
		MOV		ECX,[ESP+40]	
		MOV		EDX,[ESP+44]	
		MOV		EBX,[ESP+48]	
		MOV		EBP,[ESP+52]	
		MOV		[EBP  ],ESP		
		MOV		[EBP+4],SS		
		MOV		ES,BX
		MOV		DS,BX
		MOV		FS,BX
		MOV		GS,BX
		OR		ECX,3	;わからないポイント1
		OR		EBX,3			
		PUSH	EBX				
		PUSH	EDX			
		PUSH	ECX				
		PUSH	EAX				
		RETF ;わからないポイント2

OSからアプリケーションを呼び出す部分のコードなのですが、
わからない箇所が2つあります。

・わからないポイント1
ひとつ目はECXとEBに3をorしているのがどういう意味なのかというところです。
0bit目と1bit目のフラグを立てているのでしょうか。

調べてみると、おそらくRPLという特権レベルと思われます。
この2bitで0から3までの特権レベルを指定しています。
0が最高で3が最低です。

最低の特権レベルにしておくことでアプリケーション側からOSのセグメントに
アクセスしようとした時に、例外が発生するんではないかというのが、
私の見解です。
 
 

・わからないポイント2
そもそもRETF命令とはfar RETのことで、far CALL命令で呼ばれたときに、
RETFで元の場所に戻る命令ですが、ここでは違う使い方をしています。

ここでRETF命令が呼ばれる時のスタック内部は以下のようになっています。


ESP + 0x00 : アプリのEIP
ESP + 0x04 : アプリのCS
ESP + 0x08 : アプリのESP
ESP + 0x0C : アプリのSS

ここでRETFは
POP EIP
POP CS
を行ってから
JMP [CS:EIP]
を行う命令なので、その通りのスタック構造になっていることが
わかりました。

さらに特権レベル間のジャンプではスタックからESP、SSも
ロードされるようです。
すなわち異なるスタック領域を使うための
切り替え操作を行っているようです。
 
 
 


esp0とss0

もう一点理解できないのが、
TSS構造体のesp0, ss0です。
アプリケーションの終了処理で
task->tss.esp0のように参照しているのですが、
esp0とは何のことなんでしょう。

なんとなくスタックの切り替えのために、
タスクスイッチ時に書き込まれているような気がするのですが、
調べてみても良くわかりませんでした。

この部分は分かり次第、書き足そうと思います。
ということで勘弁して下さい。

 
 

【関連記事】
OS自作入門 1日目
OS自作入門 3日目-2
OS自作入門21日目
OS自作入門23日目
30日でできる!OS自作入門 まとめ

OS自作入門21日目 【Linux】 | C言語アプリを実行

書籍ではOSを守ろうと題して、
セグメントの話しが繰り広げられるわけですが、
隠れたハードルであるC言語で作ったアプリを
自作OS上で実行するという部分についてまとめます。



 
 
 


使用環境

$ uname -a
Linux ***-E200HA 4.10.0-37-generic #41-Ubuntu SMP Fri Oct 6 20:20:37 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
$ nasm -version
NASM version 2.13.01
$ qemu-system-i386 --version
QEMU emulator version 2.10.1(Debian 1:2.10+dfsg-0ubuntu13)
$ gcc --version
gcc (Ubuntu 5.3.1-14ubuntu2) 5.3.1 20160413

 
 
 


HariboteOS上で動く実行ファイルの形式

20日目まではアセンブラで書かれたアプリケーションを実行するだけでしたが、
C言語で書いたアプリケーションを実行するにはどうするのか。

目標は書籍にある実行ファイル形式である
.hrbファイルの構造は次のようになっています。

ヘッダー
テキスト
データ
の3つにわかれていて、
更にヘッダーは以下のような構造をしています。
※書籍でもあとから出てきます。

・0x0000 : アプリ用データセグメントのサイズ
・0x0004 : 0x69726148 つまり”Hari”
・0x0008 : データセグメント内の予備領域の大きさ
・0x000c : ESP初期値
・0x0010 : データ部分(.dataセクション)のサイズ
・0x0014 : hrbファイル内にあるデータ部分の開始位置
・0x0018 : 0xe9000000
・0x001c : HariMainのアドレス-0x20
・0x0020 : malloc領域開始アドレス

 
 
 


hrb形式にコンパイル

この構造になるように
NASM、GCCを使ってコンパイルしていきます。
使うのは21日目の”b”の
・a.c
・a_nask.nas→a_nasm.asm(私はアセンブラにnasmを使っている)
の2つです。

ソースコードは以下の通り。(書籍通りです)

// a.c
void api_putchar(int c);

void HariMain(void) {
	api_putchar('A');
	return;
}

;a_nasm.asm
global api_putchar

section .text

api_putchar:
	mov edx,1
	mov al, [esp+4]
	int 0x40
	ret

各々、オブジェクトファイルに変換します。

$ gcc -c -m32 -o a.o a.c
$ nasm -f elf32 -o a_nasm.o a_nasm.asm

そしてこれらをリンクするときにリンカスクリプトを使います。
リンカスクリプトというのは、
このセクションはここに配置してねーとか
リンカに対して指示を与えるファイルです。
ここのサイトを参考にさせていただきました。

で、こんな感じでリンクします。

$ ld -m elf_i386 -e HariMain -o a.hrb -Tapi.ls a_nasm.o a.o

-Tapi.lsの部分がリンカスクリプトを指定している部分です。
-e HariMainはたぶん無くても関係ないです。

肝心のリンカスクリプトが以下です。

OUTPUT_FORMAT("binary");

SECTIONS
{
	.head 0x0 : {
		LONG(128 * 1024)
		LONG(0x69726148)
		LONG(0)
		LONG(0x0400)
		LONG(SIZEOF(.data))
		LONG(LOADADDR(.data))
		LONG(0xE9000000)
		LONG(HariMain - 0x20)
		LONG(24 * 1024)
	}

	.text : { *(.text) }

	.data 0x0400 : AT ( ADDR(.text) + SIZEOF(.text) ) {
		*(.data)
		*(.rodata)
		*(.bss*)
	}

	/DISCARD/ : { *(.eh_frame) }
}

あとはできあがったa.hrbをmcopyコマンドでimgファイルに書き込みます。

 mformat -f 1440 -C -B ipl.bin -i os.img ::
mcopy -i os.img os.sys ::
mcopy -i os.img a.hrb ::

 
 


【関連記事】
OS自作入門 1日目
OS自作入門 3日目-2
OS自作入門20日目
OS自作入門22日目
30日でできる!OS自作入門 まとめ