第1章
開発環境構築と最初のプログラム

この章では、使用するスクリプト群とエミュレータを紹介しながら、本書でのゲームボーイ(以下、GB)プログラミングの流れを紹介します。

1.1 事前準備

Bashが動作する環境を用意しておいてください。LinuxやmacOSなどの場合は問題ないかと思いますが、Windowsの場合はWSLなどでBashが動作する環境を用意しておいてください。

1.2 本書のサンプルプログラムをダウンロード

シェルスクリプトでのGBプログラミングを補助するスクリプト群は、本書のサンプルに含まれています。

本書のサンプルは以下のGitHubリポジトリで公開しておりますので、こちらをcloneあるいはダウンロードしてください。

zipでダウンロードした場合はお好きな場所へ展開してください。

1.3 簡単なプログラムを作ってみる

プログラミングのための準備はこれで完了です*1。さっそく、簡単なプログラムを作ってみましょう。

[*1] テキストエディタは適宜お好きなものをご用意ください。

ここでは、最も簡単なプログラムとして「起動後、無限ループするだけ」というものを作ってみることにします。

本書におけるGBプログラミングの方針

まず、本書でのGBプログラミングの方針を説明します。

GBプログラミングのアウトプットは「GBのROMファイル」です。本書ではこのROMファイルをシェルスクリプトで生成します。

ROMファイルはGBのメモリ空間のROMの領域そのものです。GBのアドレス幅は16ビットで0x0000から0xFFFFまでのアドレス空間があります。その内、ROMの領域は0x0000から0x7FFFまでの32KBで、この部分のメモリマップは図1.1の通りです。

GBのROM領域のメモリマップ

図1.1: GBのROM領域のメモリマップ

本書ではこの32KBをシェルスクリプトで生成します。

割り込みベクタを作る

前節で紹介したROM領域のメモリマップを先頭から埋めていきましょう。

まずは、割り込みベクタです。ここは割り込みやリセットが発生した際にジャンプしてくる256バイトの領域です。アドレスごとに「どの割り込み/リセットのときにそこへジャンプしてくるか」が決まっています。

ここでは割り込みは使わないので、この領域は全てnopという命令で埋めることにします。nopは「何もしない(No OPeration)」というCPU命令です。

後述するカートリッジヘッダの先頭にはプログラム本体へのジャンプ命令を配置するため、割り込みベクタをnopで埋めておけば、どのような割り込み/リセットが発生したとしても、何もせずにカートリッジヘッダの先頭までCPUの命令実行が進み、プログラム本体へジャンプさせることができます。

では、割り込みベクタを作ってみましょう。

まず、作業場所として、サンプルリポジトリをcloneあるいはダウンロードして展開したディレクトリへ移動します。

$ cd gb_programming_with_shell-script_samples

nopは機械語で0x00です。ここではddコマンドを使って全て0の256バイトのファイルを生成します。

$ dd if=/dev/zero of=vec.dat bs=1 count=256
256+0 レコード入力
256+0 レコード出力
256 bytes copied, 0.00247894 s, 103 kB/s
$

これで割り込みベクタを作成できました。

カートリッジヘッダを作る

次にカートリッジヘッダを作ります。

カートリッジヘッダは、先述の通り「GBの起動時にどこへジャンプするか」というプログラムのエントリポイントや、ゲームタイトル文字列、カートリッジタイプ(ゲームボーイかゲームボーイカラーか・バッテリ搭載の有無など)などと末尾にチェックサムが書かれている領域です。

本書では、ゲームボーイ用でバッテリ無しのROM(ROM only)でゲームタイトル文字列も空で作成することにします。チェックサムはエントリポイントアドレス以降の領域で計算するため、タイトル文字列やカートリッジタイプなどを固定にしておくとチェックサム含めて固定のバイト列で使いまわすことができます。

そのようにしてカートリッジヘッダの生成をシェル関数化したものを、サンプルのinclude/gb.shgb_cart_header_no_titleという名前の関数で用意しています。

その他にもGBのLR35902*2というCPUのCPU命令や、いくつかの処理をシェル関数化しており、gb.shをsourceコマンドで読み込むことで使えるようにしています。

[*2] 8ビットCPUで、Z80のカスタムCPU(シャープ製)

gb_cart_header_no_titleはエントリポイントアドレスを引数にとります。ROMで自由にプログラムを配置できる領域は0x0150以降なので、エントリポイントは0x0150にします。

それでは、このシェル関数を使ってカートリッジヘッダを生成してみましょう。

$ . include/gb.sh
$ gb_cart_header_no_title 0150 >head.dat
$

これでhead.datというファイル名でカートリッジヘッダのバイナリを生成できました。

■注意:includeディレクトリと同じ階層で作業すること

なお、includeディレクトリ内のスクリプトをsourceコマンド(あるいは簡略表現の".")で読み込む際は、includeが存在するディレクトリと同じディレクトリで行ってください。

include内のスクリプトはその他のスクリプトに定義された処理を使用する際はそのスクリプトを. include/スクリプト名のように読み込んでいます。実行時のカレントディレクトリがincludeディレクトリが存在するディレクトリであることを想定して作られているので、サブディレクトリを作ってその中で作業する際は、その中にincludeディレクトリのシンボリックリンクを作成するかincludeディレクトリをコピーして配置してください。

無限ループのプログラムを作成する

続いて、0x0150以降の領域を作成します。ここからがプログラム本体です。好きなようにプログラムやデータを配置することができます。

カートリッジヘッダに、起動後のエントリアドレスとしてこの領域先頭である0x0150を指定しましたので、ゲームボーイの電源を入れてNintendoロゴが表示された後、0x0150へジャンプしてきます。

今回は「無限ループするだけのプログラム」ということで、ここに無限ループのプログラムを配置することにします。

プログラムはCPUが直接解釈する機械語で配置します。ただ、手で機械語の16進数を書いていくのは少しつらいので、CPUの機械語命令のバイナリを生成するシェル関数を用意しています*3

[*3] まだ全部ではないと思いますが、GB上で動くOSもどきを作るのに困らないくらいはあります。

ここでは、CPUの相対ジャンプ命令を使って無限ループを実現します。各CPU命令はlr35902_から始まる名前のシェル関数で実装しています。相対ジャンプ命令はlr35902_rel_jumpという名前のシェル関数です。引数にジャンプする相対的なバイト数を2桁の16進数(1バイト)で指定します。

試しに、引数に0x00を指定して実行してみます。標準出力へ機械語バイナリを出力するので、適当なファイルへリダイレクトします。

$ lr35902_rel_jump 00 >rel.dat
$

lr35902から始まるCPU命令をシェル関数化したものを実行すると、カレントディレクトリにasm.lstというファイルが生成されます。ここには生成した機械語命令のアセンブラのリストが追記されていきます。特に本書で使ったりはしませんが、デバッグ時などに参考にしてみてください。(コロン区切りの右側の数字はその命令にかかるサイクル数です。)

$ cat asm.lst
jr $00  ;12
$

リダイレクトしたバイナリをhexdumpで見てみると0x18 00という2バイトが生成されていることがわかります。

$ hexdump -C rel.dat
00000000  18 00                                             |..|
00000002
$

これが相対ジャンプ命令の機械語バイナリです。0x18が「相対ジャンプ命令」であることを示し、続く0x00がジャンプする相対的なバイト数を示しています(引数で与えた0x00がそのまま入っています)。

相対ジャンプ命令で指定する「相対的なバイト数」は、「自身の次の命令」が基準です。そのため、今回のように0を指定した場合、「自身の次の命令」へジャンプすることになります*4

[*4] 単に次の命令へ進むだけで、nopと同じ挙動

無限ループにするためには自身の命令のバイト数マイナスの位置へジャンプする必要があります。マイナスの表現には2の補数を使います。相対ジャンプ命令のバイト数は2バイトなので、-2を表す0xfeを指定すると無限ループになります。

$ lr35902_rel_jump fe >inf.dat
$

これで、無限ループのプログラムを作ることができました。

なお、2の補数も都度計算するのは面倒なので、シェル関数を用意しています。

two_compというシェル関数は、引数で与えた16進数2桁の数値を2の補数によるマイナス表現へ変換します。例えば、-2の2の補数表現を得る際は以下のように使用します。

$ two_comp 02
FE
$

これは、そのまま相対ジャンプ命令の引数に与えることができます。

$ lr35902_rel_jump $(two_comp 02) | hexdump -C
00000000  18 fe                                             |..|
00000002
$

lr35902_rel_jumpの出力をそのままhexdumpに与えて確認すると、0x18 feという2バイトが生成できています。0x18という相対ジャンプ命令のオペランドとして0xfe(-2の2の補数表現)が与えられており、意図通りです。

また、10進数で指定した数を2の補数によるマイナス表現へ変換する関数two_comp_dも用意しています。(適宜使い分けます)

余白を埋める

ここまでで、割り込みベクタ(256バイト)・カートリッジヘッダ(80バイト)・機械語プログラム(2バイト)を作成しました。ROMファイルを32KBとするために、残るROMの領域を適当なデータで埋めます。

残るROMの領域は、(32*1024)-256-80-2=32430より、32430バイトです。このサイズ分を全て0で埋めたデータをddコマンドで作成します。

$ dd if=/dev/zero of=pad.dat bs=1 count=32430
32430+0 レコード入力
32430+0 レコード出力
32430 bytes (32 kB, 32 KiB) copied, 0.0553519 s, 586 kB/s
$

結合してROMファイル完成

ここまでで作成した全てを結合するとROMファイルになります。

$ cat cat vec.dat head.dat inf.dat pad.dat >inf.gb
$

無限ループするだけのROMということで、inf.gbというROMファイル名で作成してみました。

1.4 エミュレータで実行してみる

エミュレータは何でも構いませんが、本書では「BGB」というエミュレータで説明します。

BGBは以下のウェブサイトからダウンロードできます。

「downloads」というリンクをクリックするとジャンプするページ下部の以下の場所で実行バイナリをダウンロードできます。

筆者は64ビット版のアーカイブ「bgbw64.zip」をダウンロードして使用しています。

公開されている実行バイナリはWindows向けのみですが、Linux上でもWine*5を使用することで問題なく動作します*6

[*5] Linux上でWindowsアプリケーションを動作させるツール。

[*6] 筆者自身、Linux(Debian)上でWineを使用してBGBを動作させています。BGBのためにWineをインストールするくらい、BGBは色々なデバッグができる強力なエミュレータです。

Linuxの場合について、Wineのセットアップ方法を詳しく説明はしませんが、Debian/UbuntuなどのAPTが使える環境の場合、以下のあたりをインストールすれば問題ないかと思います。

  • wine64
  • winetricks

winetricksは、Wine用のツールやデータをダウンロードしたり設定したりするパッケージマネージャのようなものです。WineでBGBを起動した際にアルファベットが文字化けしたため、その対処を行うためにインストールしました。このような問題に陥った場合の対処方法は、筆者が参考にさせていただいたブログ記事を本書末尾の「参考にさせてもらった情報」に記載していますので、参照してみてください。

Wineを使用する場合、以下のようにBGBを起動できます。なお、BGBのzipアーカイブ(bgbw64.zip)はホームディレクトリ直下に展開されてるとします。

$ wine64 ~/bgbw64/bgb64.exe -nowarn ROMファイル名

なお、-nowarnのオプションは、カートリッジヘッダの生成時に設定を省いている一部のチェックサムのために付けています。実機では参照されないチェックサムなのですが*7、BGBではそこも確認し、起動時にワーニングのメッセージを表示するため、それをスキップするようにしています。

[*7] カートリッジヘッダの「グローバルチェックサム」というものです。詳しくは、本書末尾の「参考にさせてもらった情報」の「Everything You Always Wanted To Know About GAMEBOY」の「The Cartridge Header」の章を参照してください。

以降、BGBでROMファイルを動作させることを示す際のコマンドは以下のように記載します*8

[*8] 適宜読み替えても良いですし、シェルへのエイリアス設定追加や、パスの通った場所に"bgb"というファイル名で起動用のスクリプトを配置しても構いません。

$ bgb ROMファイル名

なお、Windowsの場合は、上記の表記が出てきた場合も、よしなにBGBの実行ファイルをダブルクリックしてROMファイルを開くなりしてもらえれば構いません。

それでは、作成したROMファイルinf.gbを起動してみましょう。

$ bgb inf.gb

起動すると図1.2のような画面が表示されます。

inf.gbの実行画面

図1.2: inf.gbの実行画面

「Nintendo」というロゴが表示されている状態がゲームボーイの起動直後のデフォルト画面です。

無限ループしているだけなので、画面上は何も変化がありません。

ちゃんと意図通りに動いているのか確認するために、デバッグ画面を開いてみましょう。

右クリックで表示されるメニューから[Other]の中の[Debugger]をクリックしてください。

すると、図1.3の画面が表示されます。

inf.gbのデバッグ画面

図1.3: inf.gbのデバッグ画面

色々と表示されていますが、ここでは4分割された中の左上の領域を見てください。

ここには、「今ROMのどこを実行中であるか」が、機械語を逆アセンブルした結果とともに表示されています。

右向き矢印で示された行が今実行中の箇所で、この行のみ抜き出すとリスト1.1の通りです。

リスト1.1: 今実行中の行
ROM0: 0150 18 FE            jr 0150

この行は「ROMのバンク0内でアドレスが0x0150」、「0x18 feというバイナリが書かれていて逆アセンブルするとjr 0150」ということを表しています。

jrは相対ジャンプ命令のアセンブラ表現で、オペランドが0150になっているのはBGBが逆アセンブル時に絶対アドレスへ変換しているためです。(機械語を見ると18 feと、意図通りに書かれています。)

「0x0150へジャンプし続ける無限ループ」が実現できており、意図通りに動いていることを確認できました。

本書ではこのようにしてGBのROMファイルを作成していきます。

1.5 次章からのための準備

基本的には以上のやり方でGBのROMファイルを作っていくことになります。

ただ、都度コマンドを手打ちするのは面倒なので、今後はシェルスクリプト化したものをベースに説明します。

シェルスクリプト化

この章の内容をリスト1.2のシェルスクリプトにします。

リスト1.2: 01_inf/01_inf.sh
#!/bin/bash

set -uex

. include/gb.sh

PROG_SIZE=2

# 全てnopの割り込みベクタ生成
gb_all_nop_vector_table

# カートリッジヘッダ生成
gb_cart_header_no_title ${GB_ROM_FREE_BASE}

# 無限ループ
lr35902_rel_jump $(two_comp 02)

# 32KBに満たない分を0で埋める
dd if=/dev/zero bs=1 \
   count=$((GB_ROM_SIZE - GB_VECT_SIZE - GB_HEAD_SIZE - PROG_SIZE))

シェルスクリプト化にあたり、以下の変更を加えています。

  • 実行時のデバッグ出力や未定義変数チェックなどのために、冒頭にset -uexを追加しています
  • 割り込みベクタ生成処理はgb_all_nop_vector_tableというシェル関数化しているため、それを使っています
  • ROMのカートリッジヘッダ直後の自由に使える領域の先頭アドレスはGB_ROM_FREE_BASEという名前で変数を定義済みのため、それを使っています
  • 32KBに満たない分を埋める処理のサイズ計算にも、定義済みの変数を使っています

シェルスクリプトでは、標準出力へROMバイナリを生成するようにしています。

ROMファイルはシェルスクリプト実行時にファイルへリダイレクトすることで生成できます。

$ 01_inf/01_inf.sh >inf.gb
$

haltを使う

次章以降を始める前の準備として、無限ループの箇所を少し変更します。

このようなOSレスのプログラミングにおいては、実行終了後の戻り先が無いこともあり、プログラムの最後では無限ループで停止させることがよくあります。

本書今後作っていくいくつかのサンプルもその方針なのですが、CPUの実行を進めないようにする際に、単なるビジーループはCPUに余計な負荷をかけてしまい、優しくないです。

haltというCPU命令を使うと、割り込みなどのイベントがあるまでCPUを休ませることができるので、何もしないビジーループでも、haltを挟むと良いです。

先程シェルスクリプト化したものをhaltを使用するように変更するとリスト1.3の通りです。

リスト1.3: 01_inf/02_halt.sh
#!/bin/bash

set -uex

. include/gb.sh

PROG_SIZE=4     # 変更

# 全てnopの割り込みベクタ生成
gb_all_nop_vector_table

# カートリッジヘッダ生成
gb_cart_header_no_title ${GB_ROM_FREE_BASE}

# ・・・変更(ここから)・・・
# 無限halt
lr35902_halt
lr35902_rel_jump $(two_comp 04)
# ・・・変更(ここまで)・・・

# 32KBに満たない分を0で埋める
dd if=/dev/zero bs=1 \
   count=$((GB_ROM_SIZE - GB_VECT_SIZE - GB_HEAD_SIZE - PROG_SIZE))

halt命令はlr35902_haltというシェル関数で用意しています。

2バイトの命令にしている*9ので、相対ジャンプのジャンプの戻り先も2バイト分増やして$(two_comp 04)としています。

[*9] halt命令自体は0x76のみの1バイトの命令なのですが、色々調べてみるとhalt命令は実行時、次の命令を一つ実行してしまう挙動があるとの情報もあったので、halt命令には明示的にnop(0x00)を一つ追加するようにしています。(haltを呼ぶような時は急いでいることも無いので)

$ lr35902_halt | hexdump -C
00000000  76 00                                             |v.|
00000002

haltを使用して無限に待ち続ける、という処理は今後も良く書くと思われるので、infinite_haltというシェル関数化してあります。

これを使うように変更するとリスト1.4の通りです。

リスト1.4: 01_inf/03_halt_2.sh
#!/bin/bash

set -uex

. include/gb.sh

PROG_SIZE=4

# 全てnopの割り込みベクタ生成
gb_all_nop_vector_table

# カートリッジヘッダ生成
gb_cart_header_no_title ${GB_ROM_FREE_BASE}

# ・・・変更(ここから)・・・
# 無限halt
infinite_halt
# ・・・変更(ここまで)・・・

# 32KBに満たない分を0で埋める
dd if=/dev/zero bs=1 \
   count=$((GB_ROM_SIZE - GB_VECT_SIZE - GB_HEAD_SIZE - PROG_SIZE))

割り込みは一旦全て止める

割り込みは、使うまで一旦全て止めます。

割り込み機能自体を有効化/無効化する命令があり、ここで予め割り込みを無効化する命令を実行するようにしておきます。

処理を追加するとリスト1.5の通りです。

リスト1.5: 01_inf/04_di.sh
#!/bin/bash

set -uex

. include/gb.sh

PROG_SIZE=5     # 変更

# 全てnopの割り込みベクタ生成
gb_all_nop_vector_table

# カートリッジヘッダ生成
gb_cart_header_no_title ${GB_ROM_FREE_BASE}

# ・・・追加(ここから)・・・
# 割り込み無効化
lr35902_disable_interrupts
# ・・・追加(ここまで)・・・

# 無限halt
infinite_halt

# 32KBに満たない分を0で埋める
dd if=/dev/zero bs=1 \
   count=$((GB_ROM_SIZE - GB_VECT_SIZE - GB_HEAD_SIZE - PROG_SIZE))

次章からはこのコードをベースに説明します。