『30日でできる!OS自作入門』のメモ

はじめに

本と同じ環境で開発している方へ

『C言語で直接フォントを表現』だけでも見ていってやってください。 その他の内容は、『はりぼてOSを大づかみに把握する』以外、本の開発環境には当てはまらないことが多いです。

開発環境はUbuntu上でtolsetを使わない

書籍の中ではWindowsでのtolsetを使った開発を想定して説明されてましたが、Ubuntuでtolsetを使わない方法に挑戦してみました。GNU/Linuxはあまり詳しくないので、つまづきまくりです。しかし、同じくGNU/Linux上で開発した先人たちがネット上に残してくれた有益な情報により、なんとか乗り越えることができました。私が参考にしたのは特に「Cyber Bird x86 OS自作入門」です。

このメモには、一般的な内容のものと、開発環境に特有のものがあります。

はりぼてOSを大づかみに把握する

はりぼてOS起動までの流れ

  1. 電源ON
  2. BIOS起動
  3. BIOSがフロッピーディスク(以下FD)の先頭1セクタ(IPL)をメモリ(0x7C00)に読み込む
  4. IPLがFDから10シリンダ分をメモリ(0x8200)へ読み込む
  5. OS本体の起動準備(画面モード設定・1MB以上のメモリにアクセスできるようにする・32ビットモードに移行など)
  6. bootpackを用意してあるセグメントにコピーして実行
  7. はりぼてOSの処理
対応するソースコード
番号 対応するソースコード
4 ipl10.nas
5,6 asmhead.nas
7 上記以外のbootpack.cやsheet.cなど

asmhead.nasについて

asmhead.nasの処理については最初から理解する必要はないです。 本の中で徐々に解説されていきます。 asmhead.nasですでにメモリにロードされているFDの内容をまた別のところにコピーしているのは、 用意してあるセグメントに移すためと、P171のメモリマップに合わせるためという意図があるようです。

フロッピーイメージ

以下を結合したものがフロッピーディスクのイメージとなります。

メモリマップ(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を使いました。

インラインアセンブラ

最初は、なんでかわからないけどインラインアセンブラを使うことにこだわってました。 でも、間違った書き方が原因でバグをよく起こしてしまいました。 インラインアセンブラに詳しくない人は普通のアセンブラを最初は使うほうがいいと思います。

OUT、IN、LIDTなどをインラインアセンブラで書いた例

アセンブラの割り込みハンドラをマクロで書く

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

STEP実行はあります!

『10シリンダ分を読み込んでみる』は、ブラウザでSTEP実行できます。

読み込める最大シリンダ数

安全に読むなら最大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とKzlog: OS自作入門3日目

位置 内容
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言語に変換したファイル(著作権は平木敬太郎さんと聖人(Kiyoto)さんのお二人にあります)。 これは以下の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()

自作OS 『OnSen』

私が作ったOSです。

ソースコード(GitHub)