Simple OSが動き出す!『いちばんやさしい!OS自作超入門』第4章のトラブル解決ガイド | UNIX Cafe

* 当サイトでは、コンテンツの一部に広告を掲載しています。

System Note $ cat /proc/ai-disclosure

本記事の構成および論理分析にはAI(人工知能)を使用しています。情報の正確性は、システム管理者(UNIXユーザー)による手動検証済みです。

「いちばんやさしい!OS自作超入門」第2回 第4章でつまずいたこと | UNIX Cafe
目次

第4章で気になったことを整理する

前回は、『いちばんやさしい!OS自作超入門』の環境構築やアセンブルの流れなど、本格的な開発に入る前の準備段階を整理しました。 

いよいよ今回は「第4章:シングルタスクOSの開発」に突入します。これまではバイナリ化した単体プログラムをCPUエミュレータへ直接放り込んでいましたが、ここからはOS本体(os.bin)を起動し、その上で動くシェルコマンドを1から実装していく、最高にエキサイティングなステージの始まりです。 

しかし、エミュレータの役割がガラリと変わることで「今までのやり方でプログラムが動かない!」という最初の壁にぶつかります。さらに、書籍の図をなぞるだけでは解決できない未定義ラベルの罠や、タイポした時に表示される難解なPythonのエラーログなど、アセンブリ開発ならではの「手荒い洗礼」も待ち受けていました。 

そこで今回は、エラーメッセージを劇的に追いやすくするために自作した補助ツール「asmx.py」の紹介を交えながら、割り込みベクタテーブルの配置やdateコマンド実装の欠落など、第4章を安全に完走するためのトラブル解決記録をお届けします。 

使用環境

4-2 CPUエミュレータを変更

emu.pyがos.bin起動用になる

テキスト P.76

    # プログラムをロード
    try:
        with open("os.bin", "rb") as file:
            prog = file.read()
            memory[0:len(prog)] = prog
    except:
        print("OSを読み込めませんでした。")
        sys.exit()

第4章では、CPUエミュレータ側でos.binを読み込む形に変更し、SPやPCの初期値もOS起動用に変わります。

この変更後は、第1章から第3章で使っていたような、次の手順ではhello.binを直接実行できません。

python emu.py hello.bin

この時点のemu.pyは、引数のhello.binではなく、カレントディレクトリのos.binを読み込む前提になります。

  • Simple OSを起動する場合は、python emu.pyを使います
  • hello.binのような単体プログラムを直接確認したい場合は、序盤用のearly.pyを使います
  • Simple OSから外部プログラムを実行する場合は、execコマンドが使えるようになってからdir/配下に.binを置いて実行します

第4章の途中では一時的に「前回のhello.bin直接実行手順」と「これから作るOS上でのexec実行手順」の間にいる、と理解すると混乱しにくいです。

作業前にgit statusを確認する

第4章の脚注には、次のようなコマンドが出てきます。

git fetch && git checkout v0.2
git fetch && git checkout v0.3

これらは指定されたタグやブランチの状態へ作業ツリーを切り替える操作です。手元で編集しているos.asmやasm.pyがある場合、内容が上書きされたり、切り替え前にコミットや退避を求められたりします。

実行前に、次のコマンドで作業ツリーの状態を確認しておきます。

git status はファイルの変更状況を確認するコマンドです。画面に working tree clean と表示されれば、安全に次の切り替え操作に進めます。

git status

エラーメッセージを追いやすくする

アセンブリのタイポが「Pythonのエラー」になる原因

python asm.py os.asm

os.asm をアセンブルするとき、ラベル名をタイプミスすると、次のようなPythonのtracebackが表示されました。

NameError: name '_get__nth_token_end' is not defined

一見するとasm.pyのバグに見えますが、原因は os.asm 側の記述ミスです。

JPZI    _get__nth_token_end

定義されているラベルは_get_nth_token_endなので、正しくは次のように書きます。

JPZI    _get_nth_token_end

asm.py は、ラベルの評価にPythonの eval() を利用しているため、未定義のラベルを読み込むとPythonの NameError がそのまま表示されてしまいます。

補助ツールasmx.pyを用意する

元のasm.pyでは、即値やラベルを評価するときにPythonのeval()を使っています。

return eval(exp)

ラベル名がsymbol_tableに存在しない場合、その名前がPythonの未定義変数としてeval()に渡されます。その結果、アセンブラとしてのエラーではなく、PythonのNameErrorがそのまま表示されていました。

そこで、asm.pyのエラーメッセージを少し改造した補助ツールとして、asmx.pyを用意しました。

asmx.pyのxは、asm.pyのextended版という意味です。短く入力でき、元のasm.pyをもとにした補助版だと分かりやすい名前にしています。

python asmx.py os.asm

元のasm.pyを直接置き換えず、エラー原因を詳しく見たいときだけasmx.pyを使う形にしておくと、書籍の元コードとの差分も追いやすくなります。

asmx.pyでは、gccやclangのように「どのファイルの何行目で、どの部分が原因か」を見やすくしました。

os.asm:185: error: undefined symbol '_get__nth_token_end'
        JPZI    _get__nth_token_end
                ^^^^^^^^^^^^^^^^^^^
hint: did you mean '_get_nth_token_end'?

未定義ラベル、オペランド不足、オペランド過多、不正レジスタ、.BYTEなどの引数不足、20ビット即値の範囲外も、Pythonのtracebackではなくアセンブラ風のエラーとして表示するようにしました。

asmx.pyはこのブログ記事のGitHubで公開していますので、気になる方は試してみてください。

4-4 プログラムを実行するコマンドを追加

execコマンドで外部プログラムを実行する

テキスト P.93

4-4では、Simple OSにexecコマンドを追加します。ここからは、hello.binやpi.binのようなユーザープログラムを、エミュレータに直接渡すのではなく、Simple OSのプロンプトから起動する流れになります。

実行する.binファイルは、dir/配下に置きます。

python asm.py pi.asm
mkdir -p dir
cp pi.bin dir/pi.bin
python emu.py

Simple OSが起動したら、プロンプトで次のように入力します。

> exec pi.bin

dir/pi.binが無い場合は、次のようなエラーになります。

File open error.

4-5 現在時刻を表示できるようにする

tick数カウント追加時のADDRエラー

テキスト P.101

os.asmにtick数をカウントするための割り込み処理を追加したところ、次のエラーが出ました。

os.asm:131: error: .ADDR が現在位置より小さい値を指定しています: 0xB0000 < 0xFF865
            .ADDR   0xB0000
                    ^^^^^^^

このエラーは、.ADDR 0xB0000そのものが間違っているという意味ではありません。

直前に追加した割り込みベクタテーブルを、0xFF800へ配置していたことが原因でした。

; Vector Table
        .ADDR   0xFF800
vector_table:
        .DWORD  int_timer
        .DWORD  int_other
        .DWORD  int_other
        .DWORD  int_other

asm.pyは、ソースコードを上から順番に読みながら、出力ファイル上の現在位置を進めていきます。そのため、一度.ADDR 0xFF800で現在位置を高い番地へ進めると、そのあとに.ADDR 0xB0000で低い番地へ戻ることはできません。

対応として、割り込みベクタテーブルをソースの末尾側へ移動しました。

0x80000  OS本体
0xB0000  システム関数
0xB1000  get_nth_token
0xC0000  keybuffer
0xC1000  tokenbuffer
0xFF800  割り込みベクタテーブル

テキストを見直すと、図22の下には「os.asmの末尾にこのコードを追加する」という注意書きがあります。ここを読み落として、図21の直後に続けて書くと、今回のように.ADDRの後戻りエラーになります。

修正後は次のように成功しました。

python3 asm.py os.asm
Wrote os.bin (1046544 bytes)

os.binのサイズが大きくなっているのは、.ADDR 0xFF800まで0でパディングされるためです。

tick数が増えていることを確認する

タイマー割り込み処理では、TPレジスタをインクリメントしています。

int_timer:
        INC     TP

regコマンドで確認すると、TPが増えていることが分かりました。

TP: 000000EA
TP: 000001F4

0xEAは10進数で234、0x1F4は10進数で500です。この値が増えているので、タイマー割り込みごとにint_timerが呼ばれ、INC TPが実行されていると見てよさそうです。

dateコマンド追加時のつまずき

テキスト P.104

do_dateラベルが未定義になる

続いて、書籍の図24から図26に従ってdateコマンドを追加したところ、do_dateが未定義というエラーになりました。

os.asm:84: error: undefined symbol 'do_date'
        JPZI    do_date
                ^^^^^^^
hint: did you mean 'cmd_date'?

書籍の図25には、dateコマンドを判定するコードが載っています。

; date
        MOVI    R9, cmd_date
        CALLI   cmp_str
        JPZI    do_date

また、図26にはcmd_dateとbasetimeのデータ定義が載っています。

cmd_date:
        .STRING "date"

basetime:
        .DWORD  0

ただ、図24、図25、図26で説明が終わり、その次が第5章に進んでいますので、ジャンプ先であるdo_date:本体の追加コードは見当たりません。

そのため、図25のコードだけを追加すると、JPZI do_dateのジャンプ先が存在せず、undefined symbol 'do_date'になります。

SYSCALL 11の説明から考えると、最低限必要なdo_date:本体は次の形になります。

do_date:
        LDDI    R8, [basetime]
        SYSCALL 11
        JPI     cmdloop

LDDI R8, [basetime]で、OS起動時に保存したUNIXタイムをbasetimeから読み出します。そのあとSYSCALL 11を呼ぶと、エミュレータ側でR8の基準時刻とTPのtick数から現在時刻を計算して表示します。最後にJPI cmdloopでSimple OSのコマンド入力待ちへ戻ります。

date判定を入れる場所

dateコマンドの判定コードは、未知コマンドのエラー表示より前に置く必要があります。例えば、exec判定の後、次のエラー表示へ進む前に入れます。

; exec
        MOVI    R9, cmd_exec
        CALLI   cmp_str
        JPZI    do_exec
; date
        MOVI    R9, cmd_date
        CALLI   cmp_str
        JPZI    do_date

        MOVI    R8, cmd_error1
        SYSCALL 1

未知コマンドのエラー表示より後ろにdate判定を置くと、dateと入力しても先にエラー処理へ流れてしまい、do_dateまで到達しません。

date追加時の完成形

冒頭部分は次のようにします。

        .ADDR   0x80000
        SYSCALL 10
        STDI    R8, [basetime]
        MOVI    TP, 0
        MOVI    VT, vector_table
        EI
        MOVI    R8, start_message
        SYSCALL 1

コマンド実行部分には、書籍のページ内では見当たらなかったdo_date:本体を追加します。

do_date:
        LDDI    R8, [basetime]
        SYSCALL 11
        JPI     cmdloop

データ部分には、書籍の図26どおりcmd_dateとbasetimeを追加します。

cmd_date:
        .STRING "date"
basetime:
        .DWORD  0

修正後は、アセンブルが成功しました。

python3 asm.py os.asm
Wrote os.bin (1046544 bytes)

OSを起動して、dateコマンドを実行すると、現在時刻が表示されます。

$ python emu.py
Welcome to Simple OS!
> date
2026-08-27 19:46:50
> 

今回のまとめ

第4章では、emu.pyの役割が単体プログラム実行用からos.bin起動用へ変わります。そのうえで、os.asmにコマンド処理、割り込み処理、ベクタテーブル、データ領域を少しずつ追加していくため、コードを入れる場所が重要になります。

  • emu.py変更後はpython emu.py hello.binではhello.binを直接実行できません
  • ラベル名のタイプミスは、元のasm.pyではPython tracebackとして出ます
  • 補助ツールasmx.pyを使うと、エラーの位置と候補を確認しやすくなります
  • .ADDRは現在位置を低い番地へ戻す用途には使えません
  • 図22の割り込みベクタテーブルは、os.asmの末尾に追加します
  • 図24から図26の範囲では、do_date:本体の追加コードは見当たりません

本格的にアセンブラを作り込むなら、式パーサをeval()ではなく専用の安全な実装に置き換える方法もあります。ただ、学習中の補助としては、今回の最低限の診断だけでも、ラベル名や命令の書き間違いをかなり見つけやすくなります。

次回は、シングルタスクから「マルチタスク&仮想メモリ」の世界へ

第4章で無事にオリジナルコマンドや割り込み処理が動き、シングルタスクOSとしての骨格が完成しました。

第7章では、いよいよ本作最大の山場である「仮想メモリ編」に突入します!複数のタスクが同じ論理アドレスを使っているのに、裏では別々の物理メモリで平然と同時に動くマジックの舞台裏と、そのつまずき解消ポイントを実験ノート風にまとめています。

いちばんやさしい!OS自作超入門 学習メモまとめ

第3章・第4章・第7章でつまずきやすいポイントを、章ごとの流れが見えるように整理しました。os.bin の起動、exec で読む dir/ 配下のファイル、asm.py のエラー表示などを、前後の章とのつながりで確認したい場合はこちらも参考にしてください。

おまけ:マルチタスクと仮想メモリをコントで読む

マルチタスクや仮想メモリの仕組みを、散髪屋コントにたとえて整理した読み物です。「CPUの切り替え」や「ページテーブル」のイメージを、もう少し感覚的につかみたい方におすすめです。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

のいのアバター のい UNIX Cafe マスター

Macintosh Color Classicから始まった旅は、長いWindows時代を経て、Windows10のサポート終了をきっかけにUNIXの世界へ戻ってきました。UNIX Cafeでは、UNIX・Linux・そしてMacな世界を、むずかしい言葉を使わず、物語のように書いています。プログラミングは、アイデアをコンピューターに伝えるための言葉です。簡単な単語と文法を覚えれば、誰でもコマンドを使えます。ぜひ一度、やさしいプログラミングの世界をのぞいてみてください。

目次