JD(情報系大学生)雑記

JD-情報系大学生-

OS学習メモ9(30日OS自作入門9日目)

(完成直前の記事が誤操作で一度全部消えたので沈みムード)
1週間ぶりのOS本の進捗です。ここ1週間は中間試験の勉強やseccampの選択課題の勉強であまりこちらに手を回せていませんでしたが、意欲は全く下がっていません。張り切ってやっていきましょうー。

前回は「ようやく動いたマウスカーソル君がせっかく作った画面をどんどん破壊してしまう」という状態で終了したので、今回はそれを直していく...のかなと思いきやちょっと休憩。メモリ管理のお話です。

さてメモリ管理とは言っても、そもそもメモリがどのくらいの容量なのかが分からなければ管理もへったくれもないので、まずはそこから調べていきます。どうやら起動時にBIOSがチェックしてくれているらしいのですが、それを見るのはややこしいこといっぱいあって大変なのでBIOSを使わないでメモリチェックしていきます。

...って本には書いてあったんですが、そう言われちゃうとBIOSを使ってメモリチェックしてみたくなり、自分にはまだ早いかなー出来ないだろうなぁ..ーとは思いつつ、
http://oswiki.osask.jp/?(AT)BIOS
http://softwaretechnique.jp/OS_Development/Tips/Bios_Services/video_services.html
あたりを見ながら一応自分でも色々試してみました。3時間くらいは粘ったんですが案の定うまく行かなくて「やっぱり一旦本に沿って終わらせよう...」と改めて思うのでした...(悔しい)


まぁそんなわけで今回も本に沿って進めていきました。
f:id:unyamahiro:20180513222406p:plain
32MB。

更に進めて...
f:id:unyamahiro:20180513222511p:plain
空き容量も表示できましたよ

(正直書くのがちょっと面倒くさくなっちゃっているんですが...笑)説明これだけというのはさすがに雑なので、簡単に何をやったのかだけ書いて今日は終わろうと思います。

まずはコードをコピぺして置いておきます

void memman_init(struct MEMMAN *man){
        man->frees = 0;                 /* 空き情報の個数 */
        man->maxfrees = 0;              /* 状況観察用:freesの最大値 */
        man->lostsize = 0;              /* 解放に失敗した合計サイズ */
        man->losts = 0;                 /* 解放に失敗した回数 */
        return;
}

 /*確保*/
unsigned int memman_alloc(struct MEMMAN *man, unsigned int size){
        unsigned int i, a; 
        for (i = 0; i < man->frees; i++) {
                if (man->free[i].size >= size) {  /* 十分な広さの空きを発見した時 */                       
                        a = man->free[i].addr;
                        man->free[i].addr += size;
                        man->free[i].size -= size;
                        if (man->free[i].size == 0) {
                                man->frees--;
                                for (; i < man->frees; i++) {
                                        man->free[i] = man->free[i + 1]; 
                                }
                        }
                        return a;
                }
        }
        return 0; /* 空きが無かった場合 */
}

/* 解放 */
int memman_free(struct MEMMAN *man, unsigned int addr, unsigned int size){
        int i, j;
        for (i = 0; i < man->frees; i++) {
                if (man->free[i].addr > addr) {
                        break;
                }
        }
        /* free[i - 1].addr < addr < free[i].addr */
        if (i > 0) {
                if (man->free[i - 1].addr + man->free[i - 1].size == addr) {
                        man->free[i - 1].size += size;
                        if (i < man->frees) {
                                if (addr + size == man->free[i].addr) {
                                        man->free[i - 1].size += man->free[i].size;
                                        man->frees--;
                                        for (; i < man->frees; i++) {
                                                man->free[i] = man->free[i + 1]; 
                                        }
                                }
                        }
                        return 0; 
                }
        }
        if (i < man->frees) {
                if (addr + size == man->free[i].addr) {
                        man->free[i].addr = addr;
                        man->free[i].size += size;
                        return 0; 
                }
        }
        if (man->frees < MEMMAN_FREES) {
                for (j = man->frees; j > i; j--) {
                        man->free[j] = man->free[j - 1];
                }
                man->frees++;
                if (man->maxfrees < man->frees) {
                        man->maxfrees = man->frees;
                }
                man->free[i].addr = addr;
                man->free[i].size = size;
                return 0; 
        }
        man->losts++;
        man->lostsize += size;
        return -1; 
}

メモリ管理用に作った構造体MEMMANには「frees,maxfrees,lostsize,losts,free」の5つが入っていて、その中のfree[ ]は「空いているメモリの番地と大きさの情報が(○○から--だけ空いていると言った感じに)入っている構造体FREEINFO」の配列です。
f:id:unyamahiro:20180513233113p:plain
メモリが必要になったら、MEMMANの中のfree[ ]から(free[i].sizeを見て)必要な大きさを確保できるところを探し、見つかったらその部分を確保。その部分は空き情報リストから削除します。反対に解放する時は、空き情報を追加して隣の空き地とひとまとめに出来ないかを調べ可能であればまとめるといった作業をしています。


後半は結構本の説明を借りちゃってますが..まぁ9日目はこんなところで。MINIX本の方のメモリ管理の章は手を付けていなかったと思うので、なるべく早めに読んでみようと思います。

おやすみなさい〜

OS学習メモ8(30日OS自作入門8日目)

8日目、今度こそマウスを動かす。
ここまでやってきてマウスからのデータを受け取れるようになったので、あとはそれを解読し、それにあわせてカーソルを動かせばよいだけ。ここからは本に沿ってやっていく。

i = fifo8_get(&mousefifo);
io_sti();

if (mdec->phase == 0) {
	if (i== 0xfa) {
		mdec->phase = 1;
	}
	return 0;
}
if (mdec->phase == 1) {
	mdec->buf[0] = i;
	mdec->phase = 2;	
	return 0;
}	
if (mdec->phase == 2) {
	mdec->buf[1] = i;
	mdec->phase = 3;
	return 0;
}
if (mdec->phase == 3) {
	mdec->buf[2] = i;	
	mouse->phase=1
	sprintf(s, "%02X %02X %02X", mouse_dbuf[0], mouse_dbuf[1], mouse_dbuf[2]);
	boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 32, 16, 32 + 8 * 8 - 1, 31);
	putfonts8_asc(binfo->vram, binfo->scrnx, 32, 16, COL8_FFFFFF, s);
}

マウスからデータが3バイト送られてくるので3バイトたまった時点で画面に表示。

途中経過
f:id:unyamahiro:20180505120954p:plain

こんな感じで解読はできた。
あとは表示部分を変えていく。

i = fifo8_get(&mousefifo);
io_sti();
if (mouse_decode(&mdec, i) != 0) {
	
	//マウスを消す
	boxfill8(binfo->vram, binfo->scrnx, COL8_00FF00, mx, my, mx + 15, my + 15);

	mx += mdec.x;
	my += mdec.y;
	if (mx < 0) {mx = 0;}
	if (my < 0) {my = 0;}
	if (mx > binfo->scrnx - 16) {mx = binfo->scrnx - 16;}
	if (my > binfo->scrny - 16) {my = binfo->scrny - 16;}
	putblock8_8(binfo->vram, binfo->scrnx, 16, 16, mx, my, mcursor, 16); //マウスを描く

}

カーソルがあった位置に背景色を書き込んでマウスカーソルを消して再度カーソルを描くという内容(↓の画像ではなんとなく色を変えてみたのでお絵描きみたいになっている)f:id:unyamahiro:20180505121905p:plain
見て分かる通り、下側の部分もboxfill関数で作ったただの四角形なので、カーソルを持っていくと上書きされてしまう。この問題は10日目から直していく。

簡単だけどこの辺で。

OS学習メモ7(30日OS自作入門7日目)

さてさて続き。
前回はマウスが反応しなかったところで終わりました。
今日3本目の記事でちょっと疲れてきましたがやっていきましょう。

int.cを見た。

void inthandler21(int *esp)
{
	struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;
	unsigned char data, s[1];
	io_out8(PIC0_OCW2, 0x61);	
	data = io_in8(PORT_KEYDAT);

	sprintf(s,"%02x",data);
	boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31);
	putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s);

	return;
}

キーボードを入力した時に、受け取ったキーコードを出力して割り込み処理を終了するプログラム。sprintfについてすっかり知識が抜けていたのでちょっと復習した。あとはそんなに難しいところはない。

順調に先に進めて…
FIFOバッファの説明とかを読みまして…

だいぶ飛びますがマウスの説明に入ります
(疲れてきたから説明端折ってラクしようとかじゃないですよ…多分)

初期の頃のPCにはマウスが標準でついておらず、当然当時のほぼ全てのOSはマウスに対応していなかった。後になって対応したけれど、それを使おうと思おうと思ったら有効化命令を実行しなければならない仕組みになっていた(だから6日目の時はマウスを動かしてもクリックしても反応しなかった)

というわけで、「制御回路」と「マウスそのもの」の2つに有効化命令を出す。実はマウスの制御回路はキーボード制御回路の中に入っているらしいので、そっちをうまく設定すればいい感じにできるとのこと(説明が雑になってきた)

void wait_KBC_sendready(void)
{
	for (;;) {
	if ((io_in8(PORT_KEYSTA) & KEYSTA_SEND_NOTREADY) == 0) {
		break;
	}
	}
	return;
}

CPUとキーボード制御回路の回路の速さを比べた時、CPUの方が早いので、制御回路側で命令を受け取る準備ができていないのにCPUが命令を出してしまうと誤作動を起こしかねない。CPUはキーボード制御回路側が命令を受けられるようになるまで待つ必要があり、このプログラムがやっているのはまさにそれ。命令を受けられるようになると0x0064番の装置から読み取ったデータの中のあるビットが0になるので、それが確認できるまではループする(これをポーリングって言ったっけ?)

そして

void init_keyboard(void)
{
	wait_KBC_sendready();
	io_out8(PORT_KEYCMD, KEYCMD_WRITE_MODE);
	wait_KBC_sendready();
	io_out8(PORT_KEYDAT, KBC_MODE);
	return;
}

キーボード制御回路にデータを送れるようになるのを待ち、マウスを利用するモードにすることを伝える。

...ここまでが制御回路有効化
ここからがマウスの有効化

#define MOUSECMD_ENABLE			0xf4
void enable_mouse(void)
{
	wait_KBC_sendready();
	io_out8(PORT_KEYCMD,0xd4);
	wait_KBC_sendready();
	io_out8(PORT_KEYDAT, MOUSECMD_ENABLE);
	return; 
}

マウス有効化もキーボード制御回路を使う。制御回路に0xd4と送ると次のデータをマウスに送信してくれるのを利用し、マウスに有効化命令を送っている。

さて実行し、ちゃんとマウスから割り込みがきていることを確認した。
あとは本にあるとおりにプログラムを書いていけば…
f:id:unyamahiro:20180504230808p:plain
f:id:unyamahiro:20180504230921p:plain

ちゃんとマウス反応してくれました。
キーボードも今までと同じようにちゃんと動いてくれてます。

さて、今日はいっぱいやったので、これでおしまいです。明日はこのマウスデータを解読して、ついにマウスカーソルを動かしましょう!楽しみですねえ。……あ、そこのきみ、だめだよ先を読んじゃ。今日は寝るの。続きはあした。分かった?

分かりましたーーー!!
んーーっ!疲れた!

OS学習メモ6(30日OS自作入門6日目)

書いてて思ったんですけどやっぱりこの記事ただの進捗報告でしかなさそう。人に読ませる気を自分でもあまり感じられない笑。

やり残したGDTRの説明から。

_load_gdtr:		; void load_gdtr(int limit, int addr);
		MOV	AX,[ESP+4]		; limit
		MOV	[ESP+6],AX
		LGDT	[ESP+6]
		RET

出鼻をくじくようだけどもうこのコードに関してはこれ以上説明することがなさそう…と思ったけど、そういえば初見のときはリミットってなんだろうって思ってたような気がする。GDTRは48ビットのレジスタだがそのうち下位16ビットはこのリミットというのを表していて、これはセグメントディスクリプタテーブル(GDT)の大きさのこと。上位32ビットはGDTのある番地を表している。

では次に、GDTにセグメントの情報を設定するset_segdmesc関数を見ていく。

void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar)
{
	if (limit > 0xfffff) {
		ar |= 0x8000; /* G_bit = 1 */
		limit /= 0x1000;
	}
	sd->limit_low    = limit & 0xffff;
	sd->base_low     = base & 0xffff;
	sd->base_mid     = (base >> 16) & 0xff;
	sd->access_right = ar & 0xff;
	sd->limit_high   = ((limit >> 16) & 0x0f) | ((ar >> 8) & 0xf0);
	sd->base_high    = (base >> 24) & 0xff;
	return;
}

5日目の記事にも書いたが、ここで入れるべき情報はセグメントの「①大きさ②番地③属性」の3つ。これらを1つのセグメントにつき8バイトで表現する。

①はセグメントが何バイトであるかを表す”リミット”だが、セグメントの最大サイズが4GBであることを考えると、大きさを表現するだけで32ビットの数値になってしまってこれだけで用意された8バイトの半分を使ってしまう。ということで、リミットを20ビットで表現する別の方法がある。後述するセグメント属性にはGビットというフラグがあって、これを1にすると、リミットをバイト単位ではなくページ単位であると解釈するようになる。PCのCPUでは1ページ4KBなので,これでちゃんと4GBを表現できるようになった。
②はセグメントのある位置を表すが、メモリは4GBなのでこれを表現しようとするとどうしても32ビットになる。
③セグメント属性は、セグメントのアクセス権を表す。12ビット。
これでちゃんと8バイト。

これで、長かったGDTとIDTの説明が全部片付いたので早速マウスを動かしていこう...と思ったらまだPICの初期化が終わってない。PICはprogrammable interrupt controller(設定可能な割り込みコントローラ)の略で、8個の割り込み信号を1つの割り込みにまとめる装置。CPUは単独では1つしか割り込みを扱えないのでPICを使って制御しなくてはならない。

そんなPICの初期化のプログラム↓

void init_pic(void)

{
	io_out8(PIC0_IMR,  0xff  );  //全ての割り込みを受け付けない
	io_out8(PIC1_IMR,  0xff  ); //全ての割り込みを受け付けない

	io_out8(PIC0_ICW1, 0x11  ); //エッジトリガモード
	io_out8(PIC0_ICW2, 0x20  );//IRQ0-7をINT20-27で受ける
	io_out8(PIC0_ICW3, 1 << 2); //PIC1とIRQ2で接続
	io_out8(PIC0_ICW4, 0x01  ); //ノンバッファモード

	io_out8(PIC1_ICW1, 0x11  ); //エッジトリガモード
	io_out8(PIC1_ICW2, 0x28  ); //IRQ8-15をINT28-2fで受ける
	io_out8(PIC1_ICW3, 2     ); //PIC1とIRQ2で接続
	io_out8(PIC1_ICW4, 0x01  );  //ノンバッファモード

	io_out8(PIC0_IMR,  0xfb  ); //PIC1以外の割り込みを受け付けない
	io_out8(PIC1_IMR,  0xff  ); //全ての割り込みを受け付けない

	return;
}

IMRはinterrupt mask register(割り込み目隠しレジスタ)の略。全8ビットで、それぞれIRQ信号(interrupt request つまり割り込み要求信号)の8個に対応しており、このビットが1になると対応しているIRQ信号の割り込みを無視する。ICWはinitial control word(初期化制御データ)の略。ICWは1~4まであって合計で4バイトのデータになる。ICW1,4に関しては割り込み信号の電気的特性に関することなのでいじらない。ICW3は何個のIRQにスレーブが繋がれているかを8ビットで設定するもの。一方でスレーブの方では、それがマスタの何番に付いているかを3ビットで設定するがこれも変にはいじれないのでこの値のまま変更しない。この辺り詳しいことは「はじめてよむ486」が参考になる。

まぁまぁそんなわけでICW2のみを設定していく。これは、IRQをどの割り込み番号としてCPUに通知するかを決めるもので、今回の設定ではIRQ0-15の通知をINT 0x20-0x2fで受けるようにしている(0x00-0x0fは別の処理で使うため割り込みを受けるためには使えない)

これ以降はもう本通りに進めて…
f:id:unyamahiro:20180504203150p:plain
キーボードを押すと割り込み処理で文字列表示
f:id:unyamahiro:20180504203153p:plain

マウスはまだ動きません。7日目に。
それではまた〜

OS学習メモ5(30日OS自作入門5日目)

張り切って5日目。
前回まででそれっぽい画面が出来たので、今回は文字を表示させてみたい。
f:id:unyamahiro:20180504140000p:plain
…まぁ本の通りやったら表示できますよそりゃぁね(笑)。でもそれだけじゃぁつまらないので、自分でも何かかいてみようと思った。考えた結果フレーミーを描くことにした。

それでっ \バン/(気力の音)
f:id:unyamahiro:20180504140131p:plain

とりあえずこんな感じになったのはいいが、本に書いてあるやり方では35x26マスのフォントなんて書けそうにないのでここからさらに…
f:id:unyamahiro:20180504140236p:plain
8x26に分割する。

OSASKのフォントファイルhankaku.txtを見たら、最後の方が使われていなかったので、ここに作ったやつを自分で手入力をして(8x26)、bootpack.cを(だいぶ無茶して)変えて…
f:id:unyamahiro:20180504140403p:plain
huremi関数を作って表示するところまで出来ました
(ここまで色々な格闘があった)

とまぁ3時間くらいかけてフォントを用意するところから自分でフレーミーが描けたわけです。こうして自分で作ってみて「英字のフォント、さらりと用意されているけどこれ用意するのとか大変だったんだろうなぁ」と思いました。思わされました(笑)。OSASKフォントを作った方に敬意を表して今後有り難〜く使わせてもらおうと思ったのでした(笑)。

さてさて。次はマウスを動かそうとのこと。
ここでGDTとIDTの初期化の話が出てくるが、その前にセグメンテーションの話。セグメンテーションは、ざっくりいうと「メモリを自由に切り分けて独立させること」で、これによって複数のプログラムをメモリ上でぶつからないように配置させたり、他のプログラムに不正にアクセスできないようにすることができる。こうして切り分けた一つひとつをセグメントと呼ぶが、このセグメントを表すには「セグメントの大きさ」「セグメントがどの番地から始まるか」「セグメントの管理用属性」の3つの情報が必要になり、CPUはこれらを64ビットのデータで表している。とは言っても、セグメントを指定するレジスタは16ビットしか無い。困った。

そこで別の方法を使う。
4日目でパレットを設定したときのように、セグメントセレクタと呼ばれる番号をそれぞれのセグメントとあらかじめ対応づけておいて、使う時は、このセグメントセレクタをセグメントレジスタで指定して使う。13ビット分(8192個)のセグメントを指定することができ、肝心のそれぞれのセグメント情報はセグメントディスクリプタテーブル(GDT)というやつに格納してメモリにおいておく。ごちゃごちゃしてきたので図で整理するとこんな感じ(セグメント番号って書いてあるのが上の説明でいうセグメントセレクタのこと)
f:id:unyamahiro:20180504163812p:plain
ちなみに、メモリの中にあるセグメントディスクリプタテーブルの先頭番地や有効設定個数などはGDTR(セグメントディスクリプタテーブルレジスタ)というレジスタに格納される。長くなったけど、ここまでがGDTの話。

IDTは割り込みディスクリプタテーブルのこと。(以前MINIX本で勉強して記事にもまとめた気がするけど)CPUでは、外部からの入力あるいはめったに起きない内部トラブルが起こると、その時点での処理を一旦中断して別の処理に切り替える機能があり、これを割り込みという。ここでの目的はマウスを動かすことで、マウスを使うには割り込みを使えるようにならなくてはならず、そのためにはIDTを設定する必要があるという流れ。IDTは、割り込み番号0-255に対して「割り込み番号123が発生したらOX関数を呼び出す」みたいな設定の表(GDTに似ている)。IDT設定のためにはセグメントの設定がきちんと完了していないといけないのでGDTを設定した。

コードの関係ありそうな所を見てみる(サンプルコピー)

struct SEGMENT_DESCRIPTOR {
	short limit_low, base_low;
	char base_mid, access_right;
	char limit_high, base_high;
};

struct GATE_DESCRIPTOR {
	short offset_low, selector;
	char dw_count, access_right;
	short offset_high;
};

void init_gdtidt(void);
void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar);
void set_gatedesc(struct GATE_DESCRIPTOR *gd, int offset, int selector, int ar);
void load_gdtr(int limit, int addr);
void load_idtr(int limit, int addr);

void init_gdtidt(void)
{
	struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) 0x00270000;
	struct GATE_DESCRIPTOR    *idt = (struct GATE_DESCRIPTOR    *) 0x0026f800;
	int i;

	for (i = 0; i < 8192; i++) {
		set_segmdesc(gdt + i, 0, 0, 0);
	}
	set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, 0x4092);
	set_segmdesc(gdt + 2, 0x0007ffff, 0x00280000, 0x409a);
	load_gdtr(0xffff, 0x00270000);

	for (i = 0; i < 256; i++) {
		set_gatedesc(idt + i, 0, 0, 0);
	}
	load_idtr(0x7ff, 0x0026f800);

	return;
}

void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar)
{
	if (limit > 0xfffff) {
		ar |= 0x8000;
		limit /= 0x1000;
	}

	sd->limit_low    = limit & 0xffff;
	sd->base_low     = base & 0xffff;
	sd->base_mid     = (base >> 16) & 0xff;
	sd->access_right = ar & 0xff;
	sd->limit_high   = ((limit >> 16) & 0x0f) | ((ar >> 8) & 0xf0);
	sd->base_high    = (base >> 24) & 0xff;
	return;
}

void set_gatedesc(struct GATE_DESCRIPTOR *gd, int offset, int selector, int ar)
{
	gd->offset_low   = offset & 0xffff;
	gd->selector     = selector;
	gd->dw_count     = (ar >> 8) & 0xff;
	gd->access_right = ar & 0xff;
	gd->offset_high  = (offset >> 16) & 0xffff;
	return;
}

まず気になるのが

for (i = 0; i < 8192; i++) {
	set_segmdesc(gdt + i, 0, 0, 0);
}

ここでの変数gdtにはセグメントディスクリプタの先頭アドレスが入っている。整数iを加えていくことで(ポインタなので番地は8ずつ増える)、全てのセグメントに対して「セグメントの大きさ0」と「番地0」と「アクセス権の属性0」を設定している。

次の行からは

set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, 0x4092);
set_segmdesc(gdt + 2, 0x0007ffff, 0x00280000, 0x409a);
load_gdtr(0xffff, 0x00270000);

セグメント番号1番とセグメント番号2番に対して設定。CではGDTRに代入することが出来ないのでload_gdtr関数を呼んでアセンブラの力を借りて設定する。IDTも同じようなことをしている。

これにて5日目終了です。
さて、6日目に入ります。

OS学習メモ4(30日OS自作入門4日目)

※OS本の前置きの方にある「これは教材OSだから自由に使って下さい」のお言葉に甘えてこの記事でもコードそのまま載せさせてもらっています。本当に有り難く思っています。この場を借りてお礼申し上げます。


前回は真っ黒画面を出力したので今回は何か書きたい。
というわけで4日目ではそんな関数を作っていく。

_write_mem8:	; void write_mem8(int addr, int data);
		MOV		ECX,[ESP+4]		
		MOV		AL,[ESP+8]		
		MOV		[ECX],AL
		RET

Cからアセンブラの関数を呼び出せるようにするには、ラベルのところに”_関数名”と書く。ここではwrite_mem8関数としてCから呼ぶことができる。Cでこの関数を呼び出した時に指定した引数はスタックに格納されているので、それらをECXとALレジスタに一旦読み込んでから、MOV命令を使ってメモリに値を書き込んでいる。

これを使ってC言語の方を書くと

void write_mem8(int addr, int data);
void HariMain(void)
{
	int i;
	for (i = 0xa0000; i <= 0xaffff; i++) {
        write_mem8(i, 15);
        }
    	for (;;) {
		io_hlt();
	}
}

これはVRAM全部に色番号15番(白)を書きこんでいて(背景同化してる)、
f:id:unyamahiro:20180503232602p:plain
当然だけど15という値を変更したら色んな色になる。
f:id:unyamahiro:20180503232727p:plain

これを利用して、書きこむ色を少し工夫することでこんな模様も書ける。
f:id:unyamahiro:20180503232755p:plain

…。まぁ、別にメモリに書きこむだけならばわざわざアセンブラを使わずともポインタでも良いわけで、それが↓

void HariMain(void)
{
	int i;
	char *p;

	for (i = 0xa0000; i <= 0xaffff; i++) {
		p = i; 
		*p = i & 0x0f;
	}
}

さて、今使っている8ビットカラーモードだと255通りの色指定までしか出来ない。普通のPCとかがRGBで0x000000から0xffffffまで指定とかしていることを考えても、これだとあまりにもバリエーションが少ないので、ちゃんと好きな色を使えるようにする(最大256色を設定できるが今回はそんなに使わないので16色)

#000000:黒,#ff0000:明るい赤,....,#008484:暗い水色,#848484暗い灰

void io_cli(void);
void io_out8(int port, int data);
int io_load_eflags(void);
void io_store_eflags(int eflags);

void init_palette(void);
void set_palette(int start, int end, unsigned char *rgb);

void HariMain(void)
{
	int i; 
	char *p; 
	init_palette();
	p = (char *) 0xa0000; 
	for (i = 0; i <= 0xffff; i++) {
		p[i] = i & 0x0f;
	}

}

void init_palette(void)
{
	static unsigned char table_rgb[16 * 3] = {
		0x00, 0x00, 0x00,
		0xff, 0x00, 0x00,	
             // 略
		0x00, 0x84, 0x84,
		0x84, 0x84, 0x84	
	};
	set_palette(0, 15, table_rgb);
	return;
}

void set_palette(int start, int end, unsigned char *rgb)
{
	int i, eflags;
	eflags = io_load_eflags();	
	io_cli(); 					
	io_out8(0x03c8, start);
	for (i = start; i <= end; i++) {
		io_out8(0x03c9, rgb[0] / 4);
		io_out8(0x03c9, rgb[1] / 4);
		io_out8(0x03c9, rgb[2] / 4);
		rgb += 3;
	}
	io_store_eflags(eflags);	
	return;
}

やることはシンプル。まずCLI。次に0x03c8にパレット番号(0-15)を書き込んで、続いてR,G,Bをこの順に0x03c9に書きこむ。これをパレットの色の種類数だけ(ここでは16回)繰り返したら、最後に(必要に応じて)STIをする。

CLISTI
CLI命令は割り込みフラグを0に,STIは1にする命令。CPUに割り込み要求が入った時、このフラグが0ならば無視するし1ならば要求を処理する。割り込みの説明はまた今度ということなのでここでもこのくらいにしておく。

ここで登場するEFLAGSについて補足。これはキャリーフラグや割り込みフラグなどの色々なフラグが詰まったレジスタ。割り込みフラグを操作しようと思ったらEFLAGSを読み込むしかない。

こんな感じでサンプルプログラムをそのまんま実行したり四角を増やしたりした。
f:id:unyamahiro:20180503233932p:plain

こんな感じで4日目。
今日は寝よう...楽しかったけど疲れた。

OS学習メモ3(30日OS自作入門2-3日目)

OS学習の流れにのって30日本久々に読んでます。

この記事「OS本を持っていない人にもわかりやすく伝える」という感じではないです(微妙なところですが)。どちらかといえば進捗報告に近いのかなって思ってて、本読めばわかるようなところは説明まるごとすっ飛ばしたりします。時間があればガッツリ書きたかったんですがそうもいかないので…。

一日目 

すっ飛ばし

二日目

新出命令

ORG: 続く命令をどこから格納するか、つまり先頭アドレスを指示する命令。

MOV:Cでいう代入みたいなもの。データをコピーする。

ADD:スペルそのまま、足し算する命令。

CMP:2つのオペランドを比較し、結果をEFLAGSのキャリーフラグ(後述)にセットする。ここではジャンプ命令と組み合わせて条件分岐に使った。

JMP:ジャンプ~。簡単ですね(ここ好きだったのでパクりました)

HLT:CPUを待機状態にさせる命令。

INT:割り込み命令。3日目のところで説明する。

参考:http://softwaretechnique.jp/OS_Development/Tips/IA32_Instructions/A.html

レジスタ

汎用レジスタ(AX/BX/CX/DX)

何に使ってもいいレジスタ。頭文字がABCDになっていて覚えやすいけどこれは偶然で、それぞれAccmulator,Base,Counter,Dataの頭文字を取っている。何に使ってもいいとは言っても、AXは演算に,BXはメモリをの領域指定に,CXはカウンタ変数とかに,DXはI/O操作とかに使われることが一般的。

特殊レジスタ(SP/BP)

用途が限られているレジスタで、それぞれStackとBaseの略。SPはスタック領域の一番上、BPはスタック領域の底を指す。確か後になって使うレジスタだったと思うのでここでの説明は省略。ちなみにここには登場していないがIP(Instruction Pointer)とかもあって、次に実行するアドレスを入れる。

インデックスレジスタ(SI/DI)

SI (DI )は転送命令の転送元(先)を示すレジスタ。それぞれSourceとDestinationの略。

セグメントレジスタ

(後で出てくる)

 

メモリ指定の話

MOV命令で、転送先/元にメモリを指定する場合は[ ]のかっこを使う。この時、かっこの前にBYTEやWORDなどの単語を指定することで、読み書きするデータの大きさも指定できる。

3日目

新出命令

ジャンプ系命令がいくつか

説明を保留してたINT命令

INTは割り込み用の命令。PCにはBIOS(Basic Input Output System)というプログラムがある。BIOSはPC起動時に最初に起動され、OSを起動するよりも前に(マウス,ディスクなどの)入出力装置を使用できる状態にしてくれたり、またそれらを制御する関数を提供している。INT命令はその関数を呼び出すために使う命令で、”INT 0x--”のように数字とセットで使われる。参考:http://oswiki.osask.jp/?(AT)BIOS

ディスク読み込み

AL,CH,CL,CH,DLに値を代入してディスクのどの部分を読み込むかを決定する。順に、処理するセクタ数,シリンダ番号,セクタ番号,ヘッド番号,ドライブ番号となっている。ちょうどよいところに数日前の記事で使った画像があったので再掲します

f:id:unyamahiro:20180503143027j:plainf:id:unyamahiro:20180503143126j:plain

画像にない所を補足すると..".ヘッド番号"は上から磁気ヘッドを当てるか下から当てるかを0/1で指定するもので、"ドライブ番号"は…とりあえず0を指定しておけば良い。

バッファアドレス

メモリのどこに読み込むかを表す番地のこと。これをBXレジスタ(16bit)だけを使って表そうとすると、0-0xffff(およそ64KB)までの領域しか表せないので、もっと多くの番地を指定しようということでセグメントレジスタ(ES/CS/SS/DS/FS/GS)が使われた。ES:BXという表現がまさにそれで、この場合はES*16+BX番地を指定していることになる。

ここまでがIPLの説明。

ここからOS本体に入る

まずHLTをつかったプログラム(haribote.s)を作成して、ディスクイメージに保存する。それをバイナリエディタで観察すると「空の状態のディスクにファイルを保存するとファイル名は0x002600以降に入ること」と「ファイルの中身は0x004200以降に入ること」の2つがわかる.........らしい。

早速自分でも試してみようと思ったんですが、本とは環境が大分違うので結構調べることになりました。結論から言うと「それっぽいことはできたが確信が持てない」って感じです。..まぁとりあえず載せます。

まず準備としてharibote.nasを用意

 fin:
HLT
JMP fin

 アセンブルして実行形式のharihariというのを作った。

nasm -f bin haribote.nas -o harihari

準備ができたらまず「Finder→アプリケーション→ユーティリティ→ディスクユーティリティ」を開いて、空のイメージを作成して(この時「サイズ:1440KB」「パーティション:パーティションマップなし」「フォーマット:MS-DOS(FAT)」に変更する)、作成したディスクイメージの中にHLTのプログラムを普通に置く 

f:id:unyamahiro:20180503145419j:plain

f:id:unyamahiro:20180503153023p:plain

そしたらまた同じようなディスクイメージを、今度は別の場所に作る(画像ではデスクトップに作ってたのでそれ以外の場所に)。作り終えたらディスクユーティリティの画面に戻り、2回目に作ったディスクを選択して「復元→復元元で1回目のディスクイメージ選択」を実行する。そうして出来上がったものをバイナリエディタ(自分は0xED)で開いて中身を見てみた。

f:id:unyamahiro:20180503153446p:plain

f:id:unyamahiro:20180503153454p:plain

たしかにファイル名は0x002600以降にあるなという感じだが、F4 EB FDは0x007A00のところに来てる(っていうか目grepでこれ見つけるのほんと大変だった)。まぁ...理由はわかんないけどそんなに大きな違いとかはなさそう(?)

...ブートセクタの先頭がメモリの0x8000番地だったら...この0x7A00の部分はメモリの0x00FA00に読み込めているということなんだろうか?ならばharibote.nasに"ORG 0x00FA00"を付け加えて、ipl.nasの最後には"JMP 0x00FA00"を付け加えて...。

ってやって、そのちょっと先の方まで読み進めたが、なかなか実行までたどり着けずかれこれ4,5時間は格闘した。そして格闘した結果諦めることにした...ほんとに悔しい。自力では無理だったのでここからは先人が用意してくれた方法を使って先に進みます。

環境を整えて実行し...

f:id:unyamahiro:20180503170825p:plain

おぉ...すごい...ちゃんと動いてる...。

 

今回はここまで。正直めちゃくちゃ疲れました。目もしょぼしょぼしてるし、ブログを書いている今が17時なのに既に眠いです...笑。半年前は「とにかく動かしたい」って感じだったのであまり時間を掛けずに先に進んですっかり出来た気になってましたが、やっぱり今回みたいな感じになると力不足を感じます。

というかこのブログ、本読み進めて手を動かしてる傍らで書いてるんですけど、書き始めと今で明らかなテンションの違いを感じます笑。

まぁいいや、頑張りましょう。