『30日でできる!OS自作入門』のメモ
はじめに
本と同じ環境で開発している方へ
『C言語で直接フォントを表現』だけでも見ていってやってください。 その他の内容は、『はりぼてOSを大づかみに把握する』以外、本の開発環境には当てはまらないことが多いです。
開発環境はUbuntu上でtolsetを使わない
書籍の中ではWindowsでのtolsetを使った開発を想定して説明されてましたが、Ubuntuでtolsetを使わない方法に挑戦してみました。
GNU/Linuxはあまり詳しくないので、つまづきまくりです。しかし、同じくGNU/Linux上で開発した先人たちがネット上に残してくれた有益な情報により、なんとか乗り越えることができました。
私が参考にしたのは特に「Cyber Bird x86 OS自作入門」です。
このメモには、一般的な内容のものと、開発環境に特有のものがあります。
はりぼてOSを大づかみに把握する
はりぼてOS起動までの流れ
- 電源ON
- BIOS起動
- BIOSがフロッピーディスク(以下FD)の先頭1セクタ(IPL)をメモリ(0x7C00)に読み込む
- IPLがFDから10シリンダ分をメモリ(0x8200)へ読み込む
- OS本体の起動準備(画面モード設定・1MB以上のメモリにアクセスできるようにする・32ビットモードに移行など)
- bootpackを用意してあるセグメントにコピーして実行
- はりぼてOSの処理
番号 | 対応するソースコード |
---|---|
4 | ipl10.nas |
5,6 | asmhead.nas |
7 | 上記以外のbootpack.cやsheet.cなど |
asmhead.nasの処理については最初から理解する必要はないです。 本の中で徐々に解説されていきます。 asmhead.nasですでにメモリにロードされているFDの内容をまた別のところにコピーしているのは、 用意してあるセグメントに移すためと、P171のメモリマップに合わせるためという意図があるようです。
フロッピーイメージ
以下を結合したものがフロッピーディスクのイメージとなります。
- ipl10.nasをアセンブルしたバイナリ(512バイト)
- asmhead.nasをアセンブルしたバイナリ
- bootpack.cなどをコンパイルしたバイナリ
メモリマップ(P171)
ipl10.nas終了時
アドレス | 内容 |
---|---|
0x07C00ー0x07DFF | IPL。フロッピーの先頭1セクタ(ブートセクタ) |
0x08200ー0x34FFF | フロッピーの内容(10シリンダ分。IPLを除く) |
OS実行時
アドレス | 内容 |
---|---|
0x00000000ー0x000FFFFF | 起動中にいろいろ使うけど、その後は空き(1MB) |
0x00100000ー0x00267FFF | FDの内容記憶用(1440KB) |
0x00268000ー0x0026F7FF | 空き(30KB) |
0x0026F800ー0x0026FFFF | IDT(2KB) |
0x00270000ー0x0027FFFF | GDT(64KB) |
0x00280000ー0x002FFFFF | bootpack.hrb(512KB) |
0x00300000ー0x003FFFFF | スタックなど(1MB) |
0x00400000ー | 空き |
GAS
アセンブラはnasmじゃなくてgasを使いました。
インラインアセンブラ
最初は、なんでかわからないけどインラインアセンブラを使うことにこだわってました。 でも、間違った書き方が原因でバグをよく起こしてしまいました。 インラインアセンブラに詳しくない人は普通のアセンブラを最初は使うほうがいいと思います。
アセンブラの割り込みハンドラをマクロで書く
asm_inthandler21やasm_inthandler27などはほぼ処理が同じなのでマクロを使って共通化しました。
.macro asm_inthandler c_inthandler
pushw %es
pushw %ds
pushal
movl %esp, %eax
pushl %eax
movw %ss, %ax
movw %ax, %ds
movw %ax, %es
call \c_inthandler
popl %eax
popal
popw %ds
popw %es
iret
.endm
asm_inthandler21:
asm_inthandler inthandler21
asm_inthandler27:
asm_inthandler inthandler27
マクロがどのように展開されるのか確認するには以下のコマンドを実行します。
$ as -alm asmfunc.s
GCC
アセンブリに対応する機械語が書かれたリストを得る
-Wa,-aオプションでアセンブラ時のリスト出力を得る。リストでは、アセンブリ言語のとなりに機械語が表示される。
IPL
読み込める最大シリンダ数
安全に読むなら最大33シリンダまでです。理由も知りたい方は以下を読んでください。
ipl10.nasではフロッピーのデータをアドレス0x08200へ10シリンダ分読み込んでいます。フロッピーディスクは80シリンダまであります。しかし、80シリンダ読み込むようにしてみましたができませんでした。asmheadで設定するA20GATEがONになっていないから、アクセスできるのは0xFFFFFまでだからです。80シリンダ読み込もうとすると、
読み込み先(0x08200) + 80シリンダ(0x168000) - IPL(0x200) = 0x170000
までのメモリにアクセスしようとするからです。
そこで、0xFFFFFまでに収まる55シリンダ(0xFFA00までアクセスする)を読み込んでみましたがそれでもできませんでした。0xF0000から0xFFFFFのメモリはBIOSが使うROM領域だからです。
そこで、0xEFFFFまでに収まる51シリンダ(0xED800までアクセスする)を読み込んでみたらうまく動きました。
ただし、これは私の環境での場合です。 (AT)memorymapを見ると、安全に使えそうなのは0x9FFFFまでなので、33シリンダ(0x9C800までアクセスする)ぐらいまでなら安全に読み込めそうです。
複数セクタがいっきに読み込めない
P56の『10シリンダ分を読み込んでみる』でiplをつくるとき、調子に乗って18セクタずついっきに読もうと改造したけど、うまくいかなくてハマりました。原因は、メモリの64KB境界(0x010000や0x020000など)をまたいでディスク読み込みをしようとしたことでした。ちゃんと本にも書いてありました。しかも、同じページに。
P671の『IPLの改良』では、64KB境界をまたがないようにしながら、複数セクタを一度に読み込んでいます。本ではアセンブリで何セクタを読むか計算していましたが、他の言語や電卓で事前に計算しといて定数として持っていてもいいのかなと思います。でも、複数セクタを読むようにIPLを改良しても、実機でUSBをつないだFDDを使う場合じゃないと、たいして速くならないみたいなので、1セクタずつ読む前のプログラムのままにします。
.hrb実行形式
gccで.hrb形式に対応
tolsetを使わないで開発する場合、バイナリの実行形式に注意する必要があります。『OS自作入門』では、はりぼてOSやそのアプリのバイナリ形式は、tolsetでつくられた形式であると想定されているからです。具体的には以下のようなヘッダがバイナリの先頭につきます。(参考:P460)
位置 | 内容 |
---|---|
0 | stack+.data+heap の大きさ(4KBの倍数) |
4 | シグネチャ “Hari” |
8 | mmarea の大きさ(4KBの倍数) |
12 | スタック初期値&.data転送先 |
16 | .dataのサイズ |
20 | .dataの初期値列がファイルのどこにあるか |
24 | 0xE9000000 |
28 | エントリアドレス-0x20 |
32 | heap領域(malloc領域)開始アドレス |
ここでは、リンカスクリプトを使って、バイナリの先頭にヘッダをつける方法を書きます。リンカスクリプトはgccの-Tオプションで指定できます。
$ gcc -T os.lds os.c
OS用リンカスクリプト
OUTPUT_FORMAT("binary");
SECTIONS
{
.head 0x0 : {
LONG(64 * 1024) /* 0 : stack+.data+heap の大きさ(4KBの倍数) */
LONG(0x69726148) /* 4 : シグネチャ "Hari" */
LONG(0) /* 8 : mmarea の大きさ(4KBの倍数) */
LONG(0x310000) /* 12 : スタック初期値&.data転送先 */
LONG(SIZEOF(.data)) /* 16 : .dataサイズ */
LONG(LOADADDR(.data)) /* 20 : .dataの初期値列のファイル位置 */
LONG(0xE9000000) /* 24 : 0xE9000000 */
LONG(HariMain - 0x20) /* 28 : エントリアドレス - 0x20 */
LONG(0) /* 32 : heap領域(malloc領域)開始アドレス */
}
.text : { *(.text) }
.data 0x310000 : AT ( ADDR(.text) + SIZEOF(.text) ) {
*(.data)
*(.rodata*)
*(.bss)
}
/DISCARD/ : { *(.eh_frame) }
}
アプリケーション用リンカスクリプト
UTPUT_FORMAT("binary");
SECTIONS
{
.head 0x0 : {
LONG(128 * 1024) /* 0 : stack+.data+heap の大きさ(4KBの倍数) */
LONG(0x69726148) /* 4 : シグネチャ "Hari" */
LONG(0) /* 8 : mmarea の大きさ(4KBの倍数) */
LONG(0x0400) /* 12 : スタック初期値&.data転送先 */
LONG(SIZEOF(.data)) /* 16 : .dataサイズ */
LONG(LOADADDR(.data)) /* 20 : .dataの初期値列のファイル位置 */
LONG(0xE9000000) /* 24 : 0xE9000000 */
LONG(HariMain - 0x20) /* 28 : エントリアドレス - 0x20 */
LONG(24 * 1024) /* 32 : heap領域(malloc領域)開始アドレス */
}
.text : { *(.text) }
.data 0x0400 : AT ( ADDR(.text) + SIZEOF(.text) ) {
*(.data)
*(.rodata*)
*(.bss)
}
/DISCARD/ : { *(.eh_frame) }
}
解説できるほど、リンカスクリプトを理解しているわけではないので、自分で調べてください。環境によっては修正しないといけないと思います。それから、.eh_frameの行は、これがなかったらエラーが出るけど、こう書いたら動きました。よくわかりません。
C言語で直接フォントを表現
『OS自作入門』ではフォントを表すのにhankaku.txtという「.」と「*」で書かれたフォントを使っています。hankaku.txtはC言語から直接使えないのでmakefont.exeで変換しなければなりません。でも、以下のやり方だとC言語で直接フォントを表すことができます。たとえばAだとこんな風に。
s _ _ _ _ _ _ _ _ ,
s _ _ _ X X _ _ _ ,
s _ _ _ X X _ _ _ ,
s _ _ _ X X _ _ _ ,
s _ _ _ X X _ _ _ ,
s _ _ X _ _ X _ _ ,
s _ _ X _ _ X _ _ ,
s _ _ X _ _ X _ _ ,
s _ _ X _ _ X _ _ ,
s _ X X X X X X _ ,
s _ X _ _ _ _ X _ ,
s _ X _ _ _ _ X _ ,
s _ X _ _ _ _ X _ ,
s X X X _ _ X X X ,
s _ _ _ _ _ _ _ _ ,
s _ _ _ _ _ _ _ _ ,
ここで使っている方法は『エキスパートCプログラミング』に書いてあります。やり方は、以下のマクロを定義するというものです。
#define X )*2+1
#define _ )*2
#define s ((((((((0
たとえば、上に書いたAのパターンの下から3行目
s X X X _ _ X X X ,
を考えてみるとマクロによって以下のように置き換えられます。
((((((((0)*2+1)*2+1)*2+1)*2)*2)*2+1)*2+1)*2+1
足し算以外を計算していくと以下のようになります。
128 + 64 + 32 + 4 + 2 + 1
2進数で表すと「11100111」です。うまく変換されています。
フォントを書き終わったら#undefを忘れずしておく必要があります。
#undef X
#undef _
#undef s
hankaku.txtをC言語に変換したファイル。 これは以下のpythonプログラムでつくりました。 これを自分のお気に入りの言語でつくってみるのも楽しいと思います。
#!/usr/bin/env python
f = open('hankaku.c', 'w')
f.write('#define X )*2+1\n')
f.write('#define _ )*2\n')
f.write('#define s ((((((((0\n')
f.write('\n')
f.write('char hankaku[4096] = {\n')
i = 1
for line in open('hankaku.txt', 'r'):
line = line.rstrip()
if line.startswith('char'):
f.write('\n/* ' + line + ' */\n')
elif len(line) == 8:
line = line.replace('.', '_').replace('*', 'X')
f.write('s ' + ' '.join(line))
if i != 4096:
f.write(' ,\n')
else:
f.write('\n')
i += 1
f.write('};\n')
f.write('\n')
f.write('#undef X\n')
f.write('#undef _\n')
f.write('#undef s\n')
f.close()