1.NASK環境からの解脱

川合君のアセンブラNASKや結合ツールを使わずにまずは3日目までをGNUのツール群を使ってブートさせていこうとおもいます。
なぜそういう奇行に走るのかというと、"30日でできる! OS自作入門"を読み進めていくうちに、勝手に川合君お手製のソース非公開の不思議なツール(カーネル圧縮とかの都合上みたいです)がばしばし出てきてOS学習の理解が進まない感じがしてきたのと、作成の過程が不透明な感じになってきたからです。GNUのツール群の方がスタンダードでクリアな印象を受けたからです。
※3日目とかどうとかってのは"30日でできる! OS自作入門"を参照ください。
この章で使うGNUのツールを挙げておくと、
GCC(CC,G++,LD,ASを含む)
NASM
MAKE
QEMU(要X環境)
です。debian環境の方は
$ apt-get install gcc nasm make qemu
で瞬時に美しくインストール出来るかと思います。
1.1 ブートシーケンス
"30日でできる! OS自作入門"にそって話を進めると、OS本体をブートするためにはipl10.nas → asmhead.nas → bootpack.asm

Fig1:フロッピーディスクイメージとip10実行後のメモリマップ
※?について
ブートセクタの前の0x00から0x8000は
らしいです。
0xA800にはなにやらbootpackの名前が入る?
詳しくはここに書いてあるのであとで整理します。
という手順でOSをロードしていきます。
最初の"ipl10.nas"はデータをフロッピーからロードするプログラム、"asmhead.nas"はCPUがプロテクトモードに移行するまでのプログラム、最後のbootpack.asmからがOS本体のプログラムとなっていきます。
(この状態ではOSまでは一つのバイナリの中に納められています。そいつを分割だとかいうのはもっと先の話です(;´Д`))
このうち"ipl10.nas""asmhead.nas"はNASKにによってアセンブルされたあと、川合君のツールにより結合され圧縮がかけられ、FD用イメージファイルになっていきます。これを自分たちのやり方で変えてみようと言うのがこの章の目的です。
まず、NASKを全部NASM形式に変えて、イメージ作成までのMakefileを作っていこうとおもいます。
1.2 NASK文法
どうも以下のページを参考にしてみると■tools/nask
naskの独自の文をそのままnasmに変えられるようです。
以下が書き換えたipl10.nas(nask構文) → ipl10.asm(nasm構文)です。
(コメントは抜いてあります。)
ipl10.asm
; haribote-ipl
; TAB=4
CYLS EQU 10
ORG 0x7c00
JMP SHORT entry
DB 0x90
DB "HARIBOTE"
DW 512
..中略
DD 0xffffffff
DB "HARIBOTEOS "
DB "FAT12 "
TIMES 18 DB 0
entry:
MOV AX,0
MOV SS,AX
..中略
msg:
DB 0x0a, 0x0a
DB "load error"
DB 0x0a
DB 0
TIMES 0x1fe-($-$$) DB 0
DB 0x55, 0xaa
これでnaskを使わずにアセンブル出来るようになりました。
1.3 asmhead.nasの分析と.hrbヘッダ
教科書である、"30日でできる! OS自作入門"にもあまり深くかかれていなかったasmhead.nasについて調べてわかったこと等をまとめていこうとおもいます。1.3.1 解析
まず1.2項のNASKのWikiよりasmhead.nasをNASK構文からNASM構文に書き換えたasmhead.asmを適当に切ったものを以下に示し、解説をしていきます。asmhead.asm
; haribote-os boot asm
; TAB=4
BOTPAK EQU 0x00280000 ; bootpackのロード先
DSKCAC EQU 0x00100000 ; ディスクキャッシュの場所
DSKCAC0 EQU 0x00008000 ; ディスクキャッシュの場所(リアルモード)
; BOOT_INFO関係
CYLS EQU 0x0ff0 ; ブートセクタが設定する
LEDS EQU 0x0ff1
VMODE EQU 0x0ff2 ; 色数に関する情報。何ビットカラーか?
SCRNX EQU 0x0ff4 ; 解像度のX
SCRNY EQU 0x0ff6 ; 解像度のY
VRAM EQU 0x0ff8 ; グラフィックバッファの開始番地
このセクションは各定数の設定です。
BOTPAK DSKCAC DSKCACO はロード先のアドレスを表します。
CYLS以降は、各オペランドはメモリ番地を示し次のセクションで値が代入され、後にbootpackから構造体として値が読み込まれます。
ORG 0xc200 ; このプログラムがどこに読み込まれるのか
; 画面モードを設定
MOV AL,0x13 ; VGAグラフィックス、320x200x8bitカラー
MOV AH,0x00
INT 0x10
MOV BYTE [VMODE],8 ; 画面モードをメモする(C言語が参照する)
MOV WORD [SCRNX],320
MOV WORD [SCRNY],200
MOV DWORD [VRAM],0x000a0000
; キーボードのLED状態をBIOSに教えてもらう
MOV AH,0x02
INT 0x16 ; keyboard BIOS
MOV [LEDS],AL
このセクションはHWの設定とそれをメモリにメモする作業です。
ここではVGAを320x200x8bitカラーに設定し、それらを前セクションで指定したメモリにメモしていきます。メモリにメモされた値はあとで構造体としてC言語からもアクセス可能です。
; PICが一切の割り込みを受け付けないようにする
; AT互換機の仕様では、PICの初期化をするなら、
; こいつをCLI前にやっておかないと、たまにハングアップする
; PICの初期化はあとでやる
MOV AL,0xff
OUT 0x21,AL
NOP ; OUT命令を連続させるとうまくいかない機種があるらしいので
OUT 0xa1,AL
CLI ; さらにCPUレベルでも割り込み禁止
; CPUから1MB以上のメモリにアクセスできるように、A20GATEを設定
CALL waitkbdout
MOV AL,0xd1
OUT 0x64,AL
CALL waitkbdout
MOV AL,0xdf ; enable A20
OUT 0x60,AL
CALL waitkbdout
このセクションは割り込みの禁止を行った後、"A20GATE"の設定をして1MB以上のメモリのアクセスを可能にします。
このあと32BITモードに移行するのでその間は不要な割り込みをシャットアウトするために一端ここで割り込み禁止にします。(PICの初期化をするときはこれをはずします。多分。たしか。)
"A20GATE線"とは古いPCとの互換性の為に用意された仕様でキーボードのおまけポートに0xdfを出力することによって設定されます。これで1MB以上のメモリにアクセス出来るようになります。
このセクションで"waitkbdout"というサブルーチンが出てきました。"waitkbdout"とは7章P.152で出てくる"wait_KBC_Sendready"と同じような動作をします。以下wait_KBC_Sendreadyについて本文からの抜粋です。
これはキーボード制御回路(ボードコントローラ:てKBC)が、制御命令を受け付けられるようになるまで待つ(wait)だけのものです。なんでこんなことをするのかというと、つまりCPUという電気回路はとてつもなく早いわけですが、キーボード制御回路はCPUほどには早くないのです。だからCPUが一方的に命令を出すと、ついていけなくなって誤動作をしてしまいます。それで、コマンドが受け付けられなくなるようになったら、装置番号0x0064から読み取ったデータの下から2ビット目が0になることになっているので、それが確認出来るまで、for文でぐるぐるまわるわけです。
このセクションで3回行われる"waitkbdout"のうち、最後の3回目の命令はA20GATEの設定が確実におわるまで(コマンドの受付が始まるまで)待つということのようです。
; プロテクトモード移行
LGDT [GDTR0] ; 暫定GDTを設定
MOV EAX,CR0
AND EAX,0x7fffffff ; bit31を0にする(ページング禁止のため)
OR EAX,0x00000001 ; bit0を1にする(プロテクトモード移行のため)
MOV CR0,EAX
JMP pipelineflash
pipelineflash:
MOV AX,1*8 ; 読み書き可能セグメント32bit
MOV DS,AX
MOV ES,AX
MOV FS,AX
MOV GS,AX
MOV SS,AX
このセクションで32BITモードへと移行します。
まず先だって仮のGDTを設定します。GDTについてはGDT0のセクションで解説します。
次にCR0レジスタの設定を通して32BITモードへ移行します。
始めにCROレジスタの値をEAXに呼び出します。そしてCR0レジスタの一番上の位に0、一番下の位に1を代入します。やり方は以下の通りです。
0x7fffffff = 0b01111111111111111111111111111111
とCR0レジスタの値をAND演算することでBIT31を0にします。
また、
0x00000001 = 0b00000000000000000000000000000001
とCR0レジスタの値をOR演算することでBIT0に0にします。
(ブール代数の演算についてはこちらを参照)
このCR0の設定だとページングを用いない32BITのプロテクトモードになります。他にもせっていがあるようですが、とりあえずは必要なさそうなので今回説明は割愛します。
(ん、ということはページングが使えないから、もしもページングを有効にするときはまずはこっから設定を変えていく必要があるということですね)
それが出来たらEAXの値をCROに代入して設定完了です。
設定直後はCPUの仕様上すぐにJMP命令を実行する必要があるようです。どうやらCPUのパイプライン上のデータがプロテクトモードへの移行にともなる設定の変更で解釈が変るらしいんでJMP命令を実行することによってやり直してもらうらしいです。
(ん、なんかちょっとよくわからんが、まぁJMPすれば問題ない訳か。「そういう仕様」ってのは某社の御家芸みたいですが。)
CPUのパイプライン処理についてはここを参照してみると良いかとおもいます。
ラベル"piplineflash"以降ではすべてのセグメントレジスタに0x0008を代入します。これはGDTの2つめのセグメントをあらわします。ここで2つめのセレクタの示す番地がbootpackです。
(全部のセグメントレジスタを2つめのセレクタに設定するのはこのあとbootpackに移行したときにちゃんとメモリを使えるようにする下準備としてかんがえていいのかな?まぁそう考えるのが普通でしょうね。C言語では直接セグメントレジスタを弄るのはめんどいですからね。)
; 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 ; シリンダ数からバイト数/4に変換
SUB ECX,512/4 ; IPLの分だけ差し引く
CALL memcpy
このセクションでipl10によってメモリ上に転送されたFDイメージを再度メモリ上に配置します。なぜ再配置するのかというと、32ビットモードに移行したことによりメモリ上にGDTやIDTが作られる関係上、メモリマップを用途別に整理する方が運用上の利点、また一般的にも正しいと思われるからです。多分。
(メモリがリークしてGDTやIDTを上書きするような事があったらマズイので領域を分けているのではないかと)
この直後のメモリマップは他のサイトで掲載されていたのでページにリンクを張っておきます。
■メモリマップ
上記ページ最後の項目で、なぜFDのイメージを残しておくのかという事がかかれていましすが、確か記憶によるとあとでFDにアクセスする時にディスクイメージ上のアドレスで0xA800辺りに記述されているイメージ内に結合されたファイルの情報を読み取ってFD内のファイルにアクセス(したような気になる。実際は先に読み込んでおいたものである)する為にあるのかとおもいます。
サブルーチンmemcpyについては最後のセクションで解説します。
; asmheadでしなければいけないことは全部し終わったので、
; あとはbootpackに任せる
; bootpackの起動
MOV EBX,BOTPAK
MOV ECX,[EBX+16]
ADD ECX,3 ; ECX += 3;
SHR ECX,2 ; ECX /= 4;
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
このセクションではbootpack.hrbのヘッダを読み込んでbootpackの起動に必要な設定をします。
まずEBXにbootpack.hrbの先頭アドレスが渡されます。次に[EBX+16]、つなり.dataのサイズをECXに代入し3を足して4で割ります。
ここでJZをつかってZF(ゼロフラグ)が立っていたら、".data"が必要ないので.dataの領域の確保がいらないので"skip"までジャンプします。
ここで、なぜ3を足して4で割るという操作をして.dataセクションのの有無を判別しているのかというと、ECXの値がこの後で場合によっては使われるmemcpyの転送データサイズを表す引数の意味も持っているからです。".data"のサイズをn(byte)としECXに代入し3を加算するとn+3と表せます。これを4で割るとn=1の場合以外は商を持ちます。これを使うと後述するmemcpyが2バイトずつデータを転送するので4で割った時のデータの個数を表せるうえに、演算結果のZFを利用して転送の必要性の判断も行っているのです。
".data"があるならば、サブルーチンmemcopyで".data"の領域を作成します。
メインルーチンに戻ってきたらESP(スタックポイントレジスタ)にスタック領域の先頭(これより前の領域にスタックが積まれます)を代入して最後に"2*8:0x0000001b"つまり2番目のセグメントの0x001bへと飛びます。
※boopack.hrbの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
; memcpyはアドレスサイズプリフィクスを入れ忘れなければ、ストリング命令でも書ける
ALIGNB 16, DB 0
GDT0:
TIMES 8 DB 0 ; ヌルセレクタ
DW 0xffff,0x0000,0x9200,0x00cf ; 読み書き可能セグメント32bit
DW 0xffff,0x0000,0x9a28,0x0047 ; 実行可能セグメント32bit(bootpack用)
DW 0
GDTR0:
DW 8*3-1
DD GDT0
ALIGNB 16, DB 0
bootpack:
最後のセクションはサブルーチンとGDT設定用のラベルです。
まず各サブルーチンですが、"waitkbdout"は3つめのセクションで解説したとおりです。
"memcpy"はメモリの特定の範囲を別の範囲にコピーします。
まずサブルーチン実行前に、ESIに転送元、EDIに転送先、ECXに転送データのサイズ(bitを4で割った整数)を入れておきます。これらを引数としてデータのコピーを行います。
GDT設定用のラベルですが、ここに仮のGDTが作られます。このGDTはasmheadからbootpackまでの間で使われる仮のもので後に正式なGDT(とIDT)を設定します。
GDTを作る前にALIGNB命令が使われています。これはGDTが16の整数倍のメモリ番地に設置された方が実行速度が速くなる関係上、16の整数倍のメモリ番地になるまで0x00で埋めていく命令です。
GDTは必ずヌルセレクタ(空のディスクリプタ)から始まりますので最初の一つ分8byteを0x00で埋めておきます。
2つめのディスクリプタはasmhead用の領域です。3つめはbootpack用の領域です。
※ここでは詳しいディスクリプタの説明は割愛します。必要があればまた後で説明します。
そして最後のラベルGDTR0はLGDT命令でレジスタに転送されるGDTの先頭アドレス、テーブルサイズです。
1.3.2 .hrbファイルの構成

Fig2:ソースファイルの変換行程概略
HariboteOSのファイルヘッダです。この章では.hrbファイルの作られ方を順を追って見ていきたいと思います。
まずFig2を見て頂くとわかると思いますが、このような行程でhrbファイルを作っていきます。
1.3.2.1 .bimファイルとヘッダ
obj2bimはリンカの役割に近い物を果たします。Fig2よりわかるように複数のオブジェクトファイルから一つのbimファイルを作り出し、ファイルヘッダに情報を書きます。
以下にヘッダの構成を示します。
[ .bimファイルの構造(haribote.rulを使った場合) ]Table1:bimファイルの構成(haribote.rul使用時)
+ 0 : .textサイズ
+ 4 : ファイル中の.textスタートアドレス(0x24 = 36)
+ 8 : メモリロード時の.textスタートアドレス(0x24 = 36)
+12 : .dataサイズ
+16 : ファイル中の.dataスタートアドレス
+20 : メモリロード時の.dataスタートアドレス
+24 : エントリポイント
+28 : bss領域のバイト数
+36 : コード
haribote.rul内のフォーマット指定部
format:
/* このセクションでリンクの方針を記述 */
code(align:1, logic:0x24, file:0x24);
data(align:4, logic:stack_end, file:code_end);
logicというのは、メモリに読み込まれたときの番地のこと。この番地の値を参考にリンクが行われる。
ORGみたいなものだと思うといいかもしれない。
fileというのは、.bimファイル内での位置のこと。
[ .bimファイルの構造(guigui00.rulを使った場合:つまりOSASKアプリ) ]Table2:bimファイルの構成(guigui00.rul使用時)
+ 0 : .textサイズ
+ 4 : ファイル中の.textスタートアドレス(0x48 = 72)
+ 8 : メモリロード時の.textスタートアドレス(0x48 = 72)
+12 : .dataサイズ
+16 : ファイル中の.dataスタートアドレス
+20 : メモリロード時の.dataスタートアドレス
+24 : エントリポイント
+28 : bss領域のバイト数
+36 : あき
+72 : コード
guigui00.rul内のフォーマット指定部
format:
/* このセクションでリンクの方針を記述 */
code(align:1, logic:0x48, file:0x48);
data(align:4, logic:stack_end, file:code_end);
Table1とTable2はOsaskWikiのtools/obj2bimからの抜粋です。
この章で必要なのはTable1です。これはHariMain関数用のバイナリファイルをはき出したときのヘッダです。
順を追って説明していくと、最初に"+0: .textサイズ"というのが出てきます。これはファイルヘッダの先頭、つまりファイルの先頭は.textサイズという4bitの値から始まると言うことです。では.textとはなんでしょうか?これはアセンブラのバイナリファイルのセグメント構成を表します。
アセンブラのセグメント構成は".text"".data"".bss"".stack"という構成になっています。セグメント構成とはメモリの利用方法別に領域を分けた物で".text"は実際に実行されるコードの置かれる領域、".data"は初期値を持つデータの置かれる領域、".bss"は初期値を持たないデータの置かれる領域、".stack"はスタック用の領域です。".bss"と".stack"は値を持っていないので実行ファイルにデータとして含まれる必要が無いので、これらのセクションを分けてアセンブルすることによって実行ファイルサイズが小さくなる利点があります。
これを踏まえると"+20:メモリ..."まではセグメント構成とメモリの利用法が書かれていることがわかります。
ここで、"+20:メモリ..."は単に.dataのメモリ上でのスタートアドレスを表すだけでなく、スタックの初期位置も表します。つまりこれより前の領域をスタックとして使うと言うことです。
次の"+20:エントリポイント"はエントリ、プログラムをどこから読み始めるかです。ここにはstartup関数のファイル内のアドレスが入ります。ではstartup関数とは何でしょうか?
startup関数は、"30日でできる! OS自作入門"の付属CDの"omake/tolsrc/hrblib0a/startup.c"の事を意味しています。
startup.cの内容をいかに示します。
startup.c
void HariMain(void);
void HariStartup(void)
{
/* 将来HariMainの実行に先立って何かしたくなったら、ここに書き足す */
HariMain();
/* 将来HariMainの終了後に何か処理をさせたくなったら、ここに書き足す */
return;
}
"HariStartup()"内ではメイン関数である"HariMain()"が実行されます。
つまり、
ヘッダ => HariStartup => HariMain
という順序でメインルーチンが進行していきます。Startup.cのコメントにあるようにもし、将来何かHariMainに先だって関数を実行する時はStartupの前に新しい関数を書き込むだけで新しいルーチンを追加できます。
話がずいぶんそれましたが、最後に">+28:bss領域のバイト数"です。これは初期化のされていないデータを格納するメモリサイズです。
これ以降はTable1を見てもらえばわかる通りです。
1.3.2.2 .hrbファイルとヘッダ
[ .hrbファイルの構造 ]Table3:".hrb"ファイル構成
+ 0 : stack+.data+heap の大きさ(4KBの倍数)
+ 4 : シグネチャ "Hari"
+ 8 : mmarea の大きさ(4KBの倍数)
+12 : スタック初期値&.data転送先
+16 : .dataのサイズ
+20 : .dataの初期値列がファイルのどこにあるか
+24 : 0xe9000000
+28 : エントリアドレス-0x20
+32 : heap領域(malloc領域)開始アドレス
+36 : コード
HariboteOSの実行ファイル形式である".hrb"ファイルについて解説します。
長くなりそうなのでファイルヘッダの役割と実行時の動きを分けて説明していきます。
1.3.2.1 .hrbファイルヘッダ
Table3に沿って解説していきます。まず"+ 0 : stack+.data+heap の大きさ(4KBの倍数)"ですが、これはスタックとデータとheapの合計値が入ります。では、その3つの値はどこからきたのでしょうか?
まず"stack"のですが、これは".bim"ファイルを作る時に"obj2bim"の第3引数から来ています。Makefileをよくみてみると
...Table4:Makefileの一部
bootpack.bim : bootpack.obj naskfunc.obj Makefile
$(OBJ2BIM) @$(RULEFILE) out:bootpack.bim stack:3136k map:bootpack.map \
bootpack.obj naskfunc.obj
# 3MB+64KB=3136KB
...
という風にstackの引数を指定しています。この引数はメモリのアドレスを示し、また、スタックのサイズも表しています。(アドレスより前はスタック領域になるから。) これらより、obj2bim実行時で".data"と".stack"と".bss"のメモリ上での位置とサイズが決まっていた訳です。(作者の川合君は非常にうまく、効率的にこれらを実装しています。すごい)
残るは"heap"です。heapとは"malloc"によって確保されるメモリ領域のことです。heapのサイズは"bim2hrb"の第3引数で指定されます。現段階では"malloc"は実装されていないのでサイズは0になります。
この3つの値の足し算ですが、多少複雑です。
まず、bimヘッダの"+12:.dataサイズ"と"+20:メモリロード時の.dataスタートアドレス"と"bss領域のバイト数"を足して16byteで切り上げを行います。その値にheapのサイズを足してさらに4KBで切り上げを行います。これで"+0:stack+..."のできあがりです。
次の"+ 4 : シグネチャ "Hari""は実行ファイルのシグネイチャ(識別子)です。まぁそのままです。
"+ 8 : mmarea の大きさ(4KBの倍数)"は現状では使わないので、とりあえず0にしておけば問題ないので説明を省きます。
"+12 : スタック初期値&.data転送先"は上記Table4で出てくるスタックの値を表しています。これより前の領域はスタック、この後は.data領域として割り当てられます。
"+16 : .dataのサイズ"は文字通り" .data"の領域の大きさを表します。(単位:byte)
"+20 : .dataの初期値列がファイルのどこにあるか"は.dataのファイル内の位置を入れておきます。
"24 : 0xe9000000"と"+28 : エントリアドレス-0x20"は一見すると何を表しているのか検討も付かないとおもいます。24〜31Byte辺りを見てみると

Fig3:hrbヘッダ
Fig3の様になります。リトルエンディアンなので0xe9000000という値は
0x00 0x00 0x00 0xe9
というようにメモリに格納されます。この27(0x1B)byte目のE9は逆アセンブルするとJMP命令です。次にエントリアドレスが来るのでこれはエントリアドレスへのジャンプ命令をあらわしています。ここで単にジャンプしないでアドレスから0x20減算しているのはJMP命令が相対ジャンプなのでエントリアドレスの次のバイト、0x20から数えたByte数がJMP先になります。つまりエントリアドレスから0x20減算することによって相対ジャンプのジャンプ先を指定しているのです。
さて、こんな所にJMP命令をつくって何の役に立つのでしょうか?それはasmheadに理由があります。asmheadの最後の命令は"JMP WORD 2*8:0x0000001b"で終わっています。0x1B、先ほどのE9なわけです。
ここまで整理すると
asmhead => hrbヘッダの0x1B => hrbファイル内のstartup関数 => HariMain関数
のようにブートシーケンスが進んでいくわけです。
"+32 : heap領域(malloc領域)開始アドレス"はmallocの開始アドレスを表します。とりあえず現状ではmallocを使わないので0になります。
1.3.2.2 bim2hrb
川合君のwikiにてbim2hrbのソースは公開されていたので、動作の詳細がソースより読み取れます。
オープンなモノは積極的に使いたいと思うので、(個々の要素の挙動を知るのが目的なので)今後の方針としてはLDを使ってbimファイルのヘッダを作っていき、それをbim2hrbでヘッダの付け替えをする感じで進めていきたいと思います。
1.4 bootpackのリンク
1.3ではobj2bimとbim2hrbを使うことを前提にそのヘッダについて説明をしていきましたが、まずは独自ツールを使わずにhrbファイルを作り、それを踏まえて独自の実行ファイルを作っていこうと思います。(後者は予定?)
1.4.1 リンカの役割と仕事
リンカ(リンケージエディタ)とは複数のオブジェクトファイルを結合して一つの出力ファイルを作ります。各オブジェクトファイルはセクション(".text"".data"".bss"等)を保持していますが、リンカは一つの出力ファイルにする際に各セクションをバラバラにしてセクション別に整理して出力ファイルにマッピングしていきます。
代表的なリンカとしてはGNUのGCC付属の"ld"、Microsoftの"LINK"などがあります。OSASKではobj2bimがリンカのような役目をしています。※
※参照 - tools/obj2bim
Project_Renaでは冒頭で示した目的、OSASK独自環境からの脱却を目指すので"obj2bim"を使わずにオブジェクトファイルの結合を試みてみようと思います。
1.4.2 GNUリンカ"LD"を用いたオブジェクトファイルの結合
GNUリンカ"LD"ついては以下の様な解説サイトがあります。
GNU リンカ LD の使い方
LDにはリンカスクリプトというモノがあります。
で、上記のサイトをとか参考にしてこんなリンカスクリプトを作ってみました。
bootpack.ld
/*入力オブジェクトファイル
入力ファイルを増やすときはここに追加*/
INPUT(bootpack.o nasmfunc.o)
/*出力オブジェクトファイル*/
OUTPUT(bootpack.bim)
/*エントリーポイントファイル
これを設定するとstartupルーチンが.textセクションの先頭にくる*/
STARTUP(startup.o)
/*出力フォーマット
"binary"でtext,data,bssだけのシンプルな構成になる。*/
OUTPUT_FORMAT(binary)
/*セクション*/
SECTIONS
{
/*textセクションのメモリ上での開始位置*/
. = 0x280000;
.text :
{
/*textセクションを全部*/
*(.text);
/*textのサイズをシンボルに代入*/
_b_tsize = SIZEOF(.text);
}
/*dataセクションのメモリ上での開始位置*/
.data 0x310000 :
/*dataセクションのファイル上の開始位置*/
AT ( ADDR(.text) + SIZEOF(.text))
{
/*.dataのスタートアドレスをシンボルに代入*/
_b_dstart = SIZEOF(.text) ;
/*.dataセクションを全部*/
*(.data);
/*.dataのサイズをシンボルに代入*/
_b_dsize = SIZEOF(.data);
}
/*bssセクションのメモリ上での開始位置
ディフォルトではdataの終端から*/
.bss ( 0x310000 + SIZEOF (.data) ) :
{
*(.bss);
_b_bsssize = SIZEOF(.bss);
}
}
/*
NOTE
リンカスクリプト内のシンボル
各シンボルはstartup.oに参照される。
_b_tsize .textセクションのサイズ
_b_dstart .dataセクションの開始位置
_b_dsize .dataセクションのサイズ
_b_bsssize .bssセクションのサイズ
*bimヘッダの定義ではbssセクションのサイズは64bitであるが、
quad命令がいつもエラーるので32bitにしてむりやり64bit化している。
もしもbssセクションのサイズが32bitを超える仕様のモノを作るとしたら
再考の余地がある(そんなデカい必要は無いと思うが)
*/
このスクリプトの意味するところは
- 以下のようにファイルとメモリ上にセクションがマップされる。
.text | .data | .bss | |
ファイル | 0x00 | .textの次 | .dataの次 |
メモリ | 0x280000 | 0x310000 | .dataの次 |
- エントリーポイントはtextセクションの一番始め(にあるHari_startupルーチン)である。
- 出力フォーマットはbinary形式。これはヘッダもなにもへったくれもつかない。組み込みなどのローダを使わないものに便利。
でもってこいつを
$ ld -H bootpack.ld -nostdlib
でリンクします。
1.5イメージ作成
1.4章までで、細かい点の解説は終りです。
ここからは実際にディスクイメージを作っていきます。
まず、ipl10.bin、asmhead.bin、bootpack.hrbを連結してディスクイメージを作る方法です。
以下に僕の作った起動ディスクイメージ作成プログラムを示します。
mkimg.cpp
https://yamaneko.or.tp/harib/mkimg.cpp
※長時間つながらないときはツッコミを入れて下さい。
使用方法はコンパイルして実行するだけです。
$ g++ -Wall mkimg.cpp -o mkimg
実行時の引数は第一引数がブートレコードのバイナリ、第二引数がasmheadのバイナリ、第三引数がbootpackのバイナリです。
これでイメージを作る準備は完了です。
ここまでをまとめてMAKEFILEまで作ったものが以下です。
https://yamaneko.or.tp/harib/hari++.zip
これを適当なディレクトリに展開して、
$ make
を実行すると"fdimage0.bin"というファイルが出来ます。これをqemuで実行してみるとちゃんと動きました。
また、ldの出力したマップファイルも意図した通りに配置が行われていることが読み取れます。
1.6 まとめ
ここまでをまとめると、GNUのツール群と自作のプログラムを使ってもHariboteOSを再現することが出来る事が検証された。(img2hrbはソースが公開されていたので、自分でスクラッチするのもめんどくさいので例外とさせてもらいます)ここまで読んでもらった方はお気づきかと思われますが、コンパイラやリンカの動作は普段意識しないでプログラムをしていますが、HariboteOSを題材に普段とは違うリンカやコンパイラの使い方を学ぶことによって、その挙動や原理が低水準ではあるかと思いますが、理解出来たかと思います。コンパイラという巨大なブラックボックスの動作を知ることによってコンピュータに対する理解や知識を得ることが出来たという点ではこの学習に意味はあったと総評したいです。
2006年11月20日(月) 00:41:29 Modified by yamaneko1144
添付ファイル一覧(全5件)
25072b93900bf189.jpg (18.20KB)
Uploaded by yamaneko1144 2006年11月02日(木) 01:36:09
Uploaded by yamaneko1144 2006年11月02日(木) 01:36:09
4bb2268fb19d1724.jpg (52.04KB)
Uploaded by yamaneko1144 2006年10月17日(火) 00:55:51
Uploaded by yamaneko1144 2006年10月17日(火) 00:55:51
655d9b0d7e2461fa.jpg (51.77KB)
Uploaded by yamaneko1144 2006年10月17日(火) 00:36:17
Uploaded by yamaneko1144 2006年10月17日(火) 00:36:17
dd93a038fc254182.jpg (39.98KB)
Uploaded by yamaneko1144 2006年10月10日(火) 14:30:50
Uploaded by yamaneko1144 2006年10月10日(火) 14:30:50
00ffa34f46698365.jpg (55.01KB)
Uploaded by yamaneko1144 2006年09月23日(土) 21:48:24
Uploaded by yamaneko1144 2006年09月23日(土) 21:48:24