* OSセレクタ?を作ろう!
-(by [[K]], 2006.09.24)
-発展課題のページ(全部読む前にここを読むのは混乱するのでおすすめじゃないです)
----
-[[「はりぼて友の会」:http://haribote.org/]]とかを覗いていると、「はりぼてOS」の兄弟OS(姉妹OS?)といえそうなものがたくさん生まれています。これらを別々のディスクにインストールして、好きなときに好きなOSを起動して遊ぶのは楽しいですが、これらのOSはみんなすごく小さいことですし、1枚のディスクにまとめられたら便利かなと思いました。
-そんなわけで、てきとーにOSセレクタ(の元?)を作ってみることにしました。
-というか、実は「はりぼてOS友の会」が[[10月のオープンソースカンファレンス:http://haribote.org/?OSC2006TokyoFall]]に出るので(おめでとう!)、その記念作品だったりします。・・・ということで、このプログラムは「はりぼて友の会」に奉げます(ライセンスは毎度のKL-01なので誰でも使えます)。
*** さてどうやって作ろうか?
-景気よく「作るぞー」と意気込んではみたものの、さてどうしましょうか。OSセレクタというとまずはIPLやasmheadの部分を書き換える方法が考えられますが、これはどうやら[[Akkie]]さんがチャレンジする予定らしいので、[[K]]はやらないことにしました。
-そうなるとbootpack.hrb部分をいじるしかないわけで、コンソールにOS切り替えコマンドでもつけようかなと考えました。つまり一度は普通に「はりぼてOS」(かもしくはその兄弟OS)が起動して、そこからコマンドで他のOSに移るわけです。OS切り替えプログラムが超巨大化して、それ自身がOSになってしまった、と考えてもいいでしょう。
--「はりぼてOS」は小さいので、これでも下手なブートセレクタよりも小さかったりするのですが。
-でもこれだと切り替えコマンドがかっこ悪いとか言われそうです。それもくやしいので、切り替えAPIをつけることにしました。APIならOS切り替えアプリをかっこよくすればいいですし、それなら[[K]]がバカにされる心配もありません(笑)。
~
~
-仕組みはこうしました。まず、新たに.shsというファイル形式を考えます。これは「Select-bootable Haribote.Sys」の略で、「選択起動可能なharibote.sys」のつもりです。「Sister of Haribote.Sys」の略でもいいんですが(笑)。たいそうな略語ですが中身は簡単で、これはharibote.sysの中のCOLOR(#0000ff){bootpack.hrbの部分だけを取り出して}ファイル名と拡張子を変えただけです。・・・これをディスクから読み込んで、メモリ上にそれっぽく配置してJMPしてやればうまくいきそうだと考えたわけです。・・・こんなてきとーな方法でほんとにうまくいくのかな。
-なんか.shsだとWindowsの挙動がおかしいので、.bhsにしました。「select-Bootable Haribote.Sys」もしくは「Brother of Haribote.Sys」の略。
*** とにかく作ってみるよ
-うまくいくかどうかを心配していても前進しないので、気の向くままに作ってみました。API番号はとりあえず30番です。
~
~
-console.c
int *hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)
{
(中略)
} else if (edx == 30) {
/* EAXに.bhsへのハンドル */
fh = (struct FILEHANDLE *) reg[7];
hrb_api_osselect(fh->buf, (int *) memman_alloc_4k(memman, 512 * 1024));
}
(中略)
}
void hrb_api_osselect(char *bhs, int *tmp)
/* 主な作業内容:
bootpackのコード部分をtmpのメモリにコピーする
割り込み禁止にする
0xfff8のセレクタを設定する
bhsの内容を0x280000にメモしておく
far-jmp 0xfff8:hrb_api_osselect_second
*/
{
struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) ADR_GDT;
int i, *bootpack = (int *) ADR_BOTPAK;
for (i = 0; i < (LIMIT_BOTPAK + 1) / 4; i++) {
tmp[i] = bootpack[i];
}
/* PICはまた初期化されるのでそのための準備 */
io_out8(PIC0_IMR, 0xff); /* すべて禁止 */
io_out8(PIC1_IMR, 0xff); /* すべて禁止 */
io_cli();
set_segmdesc(gdt + 8191, LIMIT_BOTPAK, (int) tmp, AR_CODE32_ER);
*((int *) ADR_BOTPAK) = (int) bhs;
farjmp((int) hrb_api_osselect_second, 8191 * 8);
}
void hrb_api_osselect_second(void)
/* 主な作業内容:
bhsのコードを0x280000に転送
bhsのデータもヘッダを解析して転送
asm_osselect_third(esp);
→ ESP=esp; farjmp(0x1b, 2 * 8);
*/
{
char *bhs = (char *) *((int *) ADR_BOTPAK);
int i, *bootpack = (int *) ADR_BOTPAK, *bhs_i = (int *) bhs;
int *p, *q, datsiz;
for (i = 0; i < (LIMIT_BOTPAK + 1) / 4; i++) {
bootpack[i] = bhs_i[i];
}
datsiz = (bhs_i[16 / 4] + 3) / 4;
p = (int *) (bhs_i[20 / 4] + bhs);
q = (int *) bhs_i[12 / 4];
for (i = 0; i < datsiz; i++) {
q[i] = p[i];
}
asm_osselect_third(bhs_i[12 / 4]);
}
----
-naskfunc.nas
(中略)
GLOBAL _asm_osselect_third
(中略)
_asm_osselect_third: ; void asm_osselect_third(int esp);
MOV AX,SS ; SSには8が入っているのでそれを使う
MOV FS,AX
MOV GS,AX
MOV ESP,[ESP+4]
JMP 2*8:0x0000001b
----
-bootpack.h : 以下の3行を適当な場所に書き足し
void asm_osselect_third(int esp);
void hrb_api_osselect(char *bhs, int *tmp);
void hrb_api_osselect_second(void);
*** 動作チェック用アプリ
-osselect.c
#include "apilib.h"
void api_osselect(int i); /* これはapilib.hに書いておくべきだな */
void HariMain(void)
{
char s[32], *p;
int i;
/* コマンドライン解析 */
api_cmdline(s, 30);
for (p = s; *p > ' '; p++) { } /* スペースが来るまで読み飛ばす */
for (; *p == ' '; p++) { } /* スペースを読み飛ばす */
i = api_fopen(p);
if (i == 0) {
api_putstr0("file open error!\n");
api_end();
}
api_osselect(i);
}
-とりあえず >osselect hiyos.bhs のように使う。
-このサンプルをそのまま使う場合は、 STACK = 1k MALLOC = 0k で十分。
----
-api030.nas
[FORMAT "WCOFF"]
[INSTRSET "i486p"]
[BITS 32]
[FILE "api030.nas"]
GLOBAL _api_osselect
[SECTION .text]
_api_osselect: ; void api_osselect(int i);
MOV EDX,30
MOV EAX,[ESP+4] ; i
INT 0x40
RET
*** 作ったときの感想など(というか苦労話?)ついでに動作原理など
-方針1. (本当はすごく使いたいけど)アセンブラは極力使わずにがんばる。アセンブラを使うとすぐに意味わからんと投げ出す人が増えそうなので。
-方針2. できるだけ短く。長いと読むほうが疲れちゃう。
~
~
-APIではファイル名指定ではなく、ファイルハンドル指定にした。こうすれば指定されたファイルが存在するかどうかをチェックする必要はなくなるし、とりあえずメモリに読み込まれた状態になっているから。
-APIではファイルが.hrb形式になっているかという最低限のチェックすらしてない。そういう改造はきっと読者がやってくれるだろうと信じているのである(笑)。このページでは基本原理さえ示せばいいだろう。
-関数hrb_apiでは、適当にメモリ512KBを確保してhrb_api_osselectを呼び出しておしまい。簡単ですね。
~
~
-hrb_api_osselectは3段ロケット方式になっている。CSを変更しなくてもできることをだいたいやってしまうhrb_api_osselect(以後first)、CSを変更後にCで記述できる範囲の仕事を全部やるhrb_api_osselect_second(以後second)、どうやってもアセンブラでしか記述できない処理をやるhrb_api_osselect_third(以後third)である。
-まずなぜCSを変更しなければいけないかだが、ちょっと考えてみてほしい。bootpackの内容を入れ替えるためには、入れ替え処理プログラムが必要である。しかしその入れ替えプログラムをメモリのどこに置くのか。CS=2*8のところに旧bootpackをおいたまま入れ替えプログラムを実行してしまうと、自分自身を上書きしてしまう事故がおきて、暴走してしまうだろう。
-それでとりあえず現在動作中のbootpack(というかharibote.sys全体)をメモリのあいたところに適当に転送し(これがtmp番地)、そこを指し示すコードセグメントを設定した後に、そこへfar-jmpする。これでCSは書き換わり、tmp上でOSは実行されるようになるのである。
~
~
-secondへ移行してしまえば、あとは.bhsファイルの内容をしかるべき番地へ転送する。つまりsecondがやっていることは本文p.170〜171のアセンブラがやっていたことをC言語でやっているだけである。高速化しようと思って32bitのコピーにするために式がややこしくなっているが、8bitのコピーでよければ、もう少しすっきりする。以下の書き方でも処理速度が落ちる以上の問題はないので、理解できなければ以下を使うのもいいかもしれない。
void hrb_api_osselect_second(void)
{
char *bhs = (char *) *((int *) ADR_BOTPAK);
int i, datsiz;
char *p, *q, *bootpack = (char *) ADR_BOTPAK;
for (i = 0; i < LIMIT_BOTPAK + 1; i++) {
bootpack[i] = bhs[i];
}
datsiz = *((int *) (bhs + 16)); /* MOV datsiz, DWORD [bhs+16] */
p = bhs + *((int *) (bhs + 20)); /* 以下類推してね */
q = (char *) *((int *) (bhs + 12));
for (i = 0; i < datsiz; i++) {
q[i] = p[i];
}
asm_osselect_third(*((int *) (bhs + 12)));
}
-thirdは何をしているのかというと、C言語だけでESPに代入する方法がどうしても思いつかなかったので、アセンブラでやっているというだけのことである。ついでにFSとGSに8を入れていなかったのでそれもやっている。そしてESPに代入した後はfar-jmpしか処理が残っていなかったので、それもアセンブラでやってしまっている(というか、そもそもESPを変更した後はCのプログラムに帰るのがややこしいので、呼び出しもとへ帰らずにfar-jmpするほうが処理が簡単で好ましい)。
----
-作り始めてしばらくして思ったのだが、普通のブートセレクタよりもこの方式のブートセレクタのほうが2点ほど利点がある。それは何かというと、ブートセレクタ自身がOS上で動くアプリなので、GUIでかっこいいブートセレクタが作りやすい。ブートセレクタに不要なAPIは削ってしまえばいいだろう。もちろん最初に起動するOSをブートセレクタ以外にも使うつもりなら、APIはすべて残しておいて普通に使えるようにしておいてもいいだろう。
-もう一点は、.bhsファイルは圧縮していてもかまわないということである。これでOSをコンパクトにまとめておけるので、よりたくさんのOSをディスクに入れておけるだろう。
* こめんと欄
-おぉおぉ〜感動です!!はりぼてファミリーをCD-Rに焼いて起動時に選択できるとすごぉ〜く嬉しいです。もうバリバリ焼いちゃいますよ♪ -- ''ひよひよ'' SIZE(10){2006-09-24 (日) 13:32:34}
-とりあえずページ完成。試してみてくださいね。 -- [[K]] SIZE(10){2006-09-24 (日) 18:21:44}
-原理は全然理解していないのですが・・・VirtualPC上で動作しました。QEMUではエラーになっちゃいましたけど。OSCでは、起動OSを選択できるCD-Rを配布したいところです。豪華なランチャーを作成する必要がありますなぁ〜。 -- ''ひよひよ'' SIZE(10){2006-09-24 (日) 22:01:02}
-あれえ、うちのQEMUではうまくいったんだけどなあ。まあそういうものなのかなあ。IPLのCYLSが足りないということはありませんでしたか?まあとにかく楽しんで活用してください。 -- [[K]] SIZE(10){2006-09-24 (日) 22:36:36}
-で、実機で試してみたのですが、切り替えた瞬間にリセット。切り替え先は、ノーマルharib27fとバージョン6適用harib27fとHiyOS 0.0.7.20060914 です。どこでリセットがかかるのかを追っていませんが、初期化周りかなぁ。 -- ''ひよひよ'' SIZE(10){2006-09-24 (日) 22:48:43}
-ノーマルharib27fはバージョン6が適用されていないから、落ちるのは無理ないかなあと思います。でも他の切り替え先でも落ちるのは、なんかちょっとおかしいですね。そのうち当方でも試してみます。 -- [[K]] SIZE(10){2006-09-24 (日) 22:55:48}
-はりぼてOSを参考にして大幅に書き換えられたBayOSは、QEMUでもVirtualPCでも切り替え直後にスタック例外が発生しました。 -- ''ひよひよ'' SIZE(10){2006-09-24 (日) 22:56:25}
-技術的に可能であることを提示していただけたので、私なりに「はりぼて友の会ランチャー」について検討してみます。 -- ''ひよひよ'' SIZE(10){2006-09-24 (日) 22:58:40}
-BayOSの中身はよく知らないのですが、asmhead.nasを大幅に書き換えているのではないでしょうか?「はりぼてOS」のように、アプリと同じフォーマットのbootpack.hrbをくっつけているタイプじゃないと、今回の方法では切り替えられません。・・・参考になったようなので、その点では幸いです。とりあえずうまくいかない例があったのは僕個人としてくやしいので、時間を見つけてがんばります。 -- [[K]] SIZE(10){2006-09-24 (日) 23:18:37}
-こちらで hiyos.bhs に切り替える実験をしたところ(QEMU上)、HiyOSに切り替わった後でキー入力したらおかしくなりました(つまり何もしなければ全く正常)。ちょっと思い当たるところがあるので、しばらく研究しますね。 -- ''K'' SIZE(10){2006-09-24 (日) 23:57:12}
-BayOSもasmhead.nasは変えていません。また、はりぼてOSと同じようにアプリと同じフォーマットのbootpack.hrbをくっつけています。その後は全然違いますが、そこまでは一緒です。 -- ''bayside'' SIZE(10){2006-09-25 (月) 11:25:14}
-baysideさんコメントありがとうございます。それなら当方のバグさえ直せば、BayOSも対応できそうです。今のところ、harib12aまではどのOSでも無事に切り替えられて、harib12bにしても最初の10秒は順調に動きそうなことが確認できました(QEMU上でのテスト)。 -- [[K]] SIZE(10){2006-09-25 (月) 22:03:23}
-大変お騒がせしましたが、バグは取れました。thirdに3行ほど書き足して、FSとGSに8が確実に入るようにしました。asmheadではこれをやっているのに、osselectでやり忘れたのがバグの原因だったようです。うちではHiyOSへの切り替えも問題なくできています(QEMU上でしかテストしていませんが・・・手抜きですみません)。 -- ''K'' SIZE(10){2006-09-26 (火) 00:48:27}
-バグが取れて落ち着いてみると、COLOR(#7f0000){「OSやアプリは友の会の自作なのにOSセレクタは自作じゃないのかよー」}ってつっこまれたら(僕としては)悲しいので、「これでもう僕のせいじゃないぞ」と自分を納得させるためのコーナーだったかもしれません(苦笑)。 -- ''K'' SIZE(10){2006-09-26 (火) 00:55:24}
-実機でもバッチリ動くようになりましたぁ〜。倍率変更もバッチリです!!でも、アプリは各OSで共通になるんですよね・・・。改変した場合オリジナルとファイル名を変えないと整合が取れなくなっちゃいますね・・・。 -- ひよひよ SIZE(10){2006-09-27 (水) 00:40:42}
-これからはみんな自作OSを.bhsファイルでも公開だ!・・・そしたら簡単に試せていいよー、と思った。そのためにはまずは僕がこのosselect入りのharibote.sysを入手しやすくしないとダメだな。 -- [[K]] SIZE(10){2006-09-30 (土) 13:46:07}
-とりあえず GUI 版を作ってみました→ http://hiyos.info/HariboteOS/20061007.htm GUI なのにマウスで選択できないのがミソ。 -- ''ひよひよ'' SIZE(10){2006-10-07 (土) 11:23:35}
-C言語だけでESPに代入する方法も、無くは無いです。でもCコンパイラが適当に最適化してくれちゃうと上手く動かない。理論としては、代入する値をxとして、関数A(int x)を呼び出す。関数Aでは関数B(int x)を呼び出し、関数Bはまず内部でPUSH EBP,MOV EBP,ESPが実行される。*((int*)&x-2)=x+4;としてreturn;し、内部でPOP EBPでEBPがx+4書き換わった所でRET。関数Aに戻ってreturn;するときにもし内部でLEAVEしてからRETするなら、LEAVEでESPにEBPがコピーされてPOP EBPが実行され、結果としてESPにxが入る。(この説明じゃ多分よく分からないと思う・・・。) -- [[Source]] SIZE(10){2008-08-25 (月) 13:25:31}
-よくこんなロジック思いつきますね…せっかくなので検証してみました。下のソースをGO/gcc->gas2nask(tolset_h/z_new_wを使用)でnasにして紙の上でトレースしてみました。
void A(int x) {
(&x)[-2] = x + 4;
}
void tester(void) {
A(0x040000);
}
ちなみにnasはこんな感じ(抜粋)になりました。
GLOBAL _A
_A:
PUSH EBP
MOV EBP,ESP
MOV EAX,DWORD [8+EBP]
ADD EAX,4
MOV DWORD [0+EBP],EAX
POP EBP
RET
GLOBAL _tester
_tester:
PUSH EBP
MOV EBP,ESP
PUSH 262144
CALL _A
LEAVE
RET
その結果、espに入る値はどうやら0x040008になりそうです。おそらく*((int*)&x-2)=x+4;は*((int*)&x-2)=x-4;の書き間違いではないかと思います。そこのところどうでしょうか?>>Sourceさん いきなりの長文失礼しました。 -- [[Sero]] SIZE(10){2008-08-27 (水) 16:59頃 手書きで挿入}
-私も頭の中で割と適当に考えただけのものなので、「PUSHとPOPでスタックがどっちの方向に進むか」という基本的なところでミスしていたようです。指摘してくれて、ありがとう。 -- [[Source]] SIZE(10){2008-08-27 (水) 21:27:13}
#comment