投稿日 : 2020/09/30 3:21:06
PC-G850エミュにさらにグローバル変数表示機能を付けてみた
前回はg800エミュレータにMSVC(Microsoft Visual C++)のようなブレークポイントによるデバッグ機能を付けましたが、今回はさらにグローバル変数をリアルタイムに確認出来る機能を追加しました。
ただし、普通のコンパイラのようにC言語のコードを完璧に解析しているわけでは無いので、グローバル変数の定義にはちょっとした制限がありますが、それでも一般的な型や配列、構造体ならだいたいは解析出来るようになっているので、これだけあればゲームのデバッグには十分使えると思います。
ちなみに、ヘッダファイルに型や変数名などの情報を定義することで、アセンブラ上のグローバル変数(.dbや.dsなどのメモリエリア)も定義することが出来るので、アセンブラだけでゲームを作る場合にもかなり重宝すると思います。
なお、ローカル変数に関してはコンパイラが出力するコードの仕様が分からないので現時点では対応していませんが、裏技としてローカル変数を一時的にグローバル変数として作ったり、引数を確認用のグローバル変数に入れてやれば、結果的にグローバル変数として確認が可能なので、現状特に困ることは無いと思います。
それと、デバッグ機能についても少しパワーアップを行い、以前は常にアセンブラ単位でしかスキップ実行が出来ませんでしたが、今回はC言語単位でもスキップ出来るようになったり、さらにSDCCのマクロ機能を利用することで、アセンブラコードにも直接ブレークが置けるようになったため、アセンブラのデバッグがとてもしやすくなりました。ただし、アセンブラ上にブレークポイントを置く際は、C言語でのブレークとは異なり止めたい位置の前の行にブレークポイントを置く必要があり、この指定を間違えるとうまくアセンブル出来なくなるといった制限があります。
さらに、今までは自前のソースファイルを用意した場合、バッチファイルのリンク情報を書き換える必要がありましたが、今回からはバッチ内部で自動的にリンクするファイルを選択するようにしたので、毎回バッチファイルを書き換える必要が無くなりました。また、Cファイル以外にもアセンブラファイル(.as)があれば、これも自動的にアセンブルしてリンクするようになったので、Cとアセンブラ両方の開発がとてもしやすくなりました。
※コンパイラやアセンブラで生成されたrelファイルを全てリンク対象とします
ただし、普通のコンパイラのようにC言語のコードを完璧に解析しているわけでは無いので、グローバル変数の定義にはちょっとした制限がありますが、それでも一般的な型や配列、構造体ならだいたいは解析出来るようになっているので、これだけあればゲームのデバッグには十分使えると思います。
ちなみに、ヘッダファイルに型や変数名などの情報を定義することで、アセンブラ上のグローバル変数(.dbや.dsなどのメモリエリア)も定義することが出来るので、アセンブラだけでゲームを作る場合にもかなり重宝すると思います。
なお、ローカル変数に関してはコンパイラが出力するコードの仕様が分からないので現時点では対応していませんが、裏技としてローカル変数を一時的にグローバル変数として作ったり、引数を確認用のグローバル変数に入れてやれば、結果的にグローバル変数として確認が可能なので、現状特に困ることは無いと思います。
それと、デバッグ機能についても少しパワーアップを行い、以前は常にアセンブラ単位でしかスキップ実行が出来ませんでしたが、今回はC言語単位でもスキップ出来るようになったり、さらにSDCCのマクロ機能を利用することで、アセンブラコードにも直接ブレークが置けるようになったため、アセンブラのデバッグがとてもしやすくなりました。ただし、アセンブラ上にブレークポイントを置く際は、C言語でのブレークとは異なり止めたい位置の前の行にブレークポイントを置く必要があり、この指定を間違えるとうまくアセンブル出来なくなるといった制限があります。
さらに、今までは自前のソースファイルを用意した場合、バッチファイルのリンク情報を書き換える必要がありましたが、今回からはバッチ内部で自動的にリンクするファイルを選択するようにしたので、毎回バッチファイルを書き換える必要が無くなりました。また、Cファイル以外にもアセンブラファイル(.as)があれば、これも自動的にアセンブルしてリンクするようになったので、Cとアセンブラ両方の開発がとてもしやすくなりました。
※コンパイラやアセンブラで生成されたrelファイルを全てリンク対象とします
ビルドツールのダウンロード
ここではこのエミュレータを含めたポケコンの開発ツールをダウンロード出来ます。
※最新版のv3.00はこちら
ちなみにビルドツールとは、ゲームの開発に必要なC言語のソーステンプレートや、デバッガ対応のg800エミュレーターや転送ソフトなどをまとめた開発キット(SDK)です。
このビルドツールとSDCCがあれば、ポケコン本体が無くてもC言語やアセンブラでゲームの開発を行うことが出来ます。
※同梱のテンプレートの詳細や各種ツールの使い方、SDCCのインストール方法については以前の記事を参考にしてください
g800エミュレータはIOCSによる描画のエミュレートに対応していますが、PSG音源エミュレート時は割り込み処理の関係でIOCSのエミュレーションを切っているため、PSG音源を使ったゲームでIOCSによる描画も行いたいなら、実機からIOCSを吸い出してエミュレータに設定し、さらにビルドツールではその時の実機のRAMも必要となるので、同梱の説明書を参考にRAMの吸い出し作業を行う必要があります。
ポケコンビルドツール v2.02 | ダウンロード |
---|
ちなみにビルドツールとは、ゲームの開発に必要なC言語のソーステンプレートや、デバッガ対応のg800エミュレーターや転送ソフトなどをまとめた開発キット(SDK)です。
このビルドツールとSDCCがあれば、ポケコン本体が無くてもC言語やアセンブラでゲームの開発を行うことが出来ます。
※同梱のテンプレートの詳細や各種ツールの使い方、SDCCのインストール方法については以前の記事を参考にしてください
g800エミュレータはIOCSによる描画のエミュレートに対応していますが、PSG音源エミュレート時は割り込み処理の関係でIOCSのエミュレーションを切っているため、PSG音源を使ったゲームでIOCSによる描画も行いたいなら、実機からIOCSを吸い出してエミュレータに設定し、さらにビルドツールではその時の実機のRAMも必要となるので、同梱の説明書を参考にRAMの吸い出し作業を行う必要があります。
グローバル変数ビューワー
ここでは参考として、ビルドツールに含まれるPSG音源対応のテンプレートを使った確認方法を説明します。
まずはダウンロードしたビルドツールを任意のフォルダに解凍します。
次に、この中にある「開発用テンプレート(PSG音源対応)」フォルダの中に入ります。
この中にあるgame.cを開くと最初の方にグローバル変数が定義されていますが、今回はこの中のMMLPLAYER構造体のmMP変数を確認してみることにします。
MMPLAYER構造体とは、MMLプレイヤーライブラリがMMLの再生情報を管理するためのものです。
ちなみにMMLPLAYER構造体はMMLPlayer.hに定義されています。※MMLCHANNEL構造体の中身やMPMAXCHANNEL定数はMMLPlayer.hを参考にしてください
SDCCがインストールされている状態で、フォルダ内にある「!!!build.bat」を実行してみましょう。
実行するとビルドが開始され、ビルドが成功したら何かキーを押すことでエミュレーターが起動し、プログラムが即座に開始されます。
プログラムが起動するとエミュレータ画面の他に、GlobalVarsWindowとDebugWindowの2つウィンドウが表示されますが、グローバル変数はGlobalVarsWindowの方で確認出来ます。
GlobalVarsWindowの一番上の空白部分が、グローバル変数を選択するドロップダウンリストになっています。
ここをクリックすると現在デバッガが認識しているグローバル変数の一覧が表示されるので、ここからmMPを探して選択します。
※グローバル変数の数が多い場合はリストをスクロールして目的の変数名を探してください
選択すると下の部分に現在の変数の状態がリアルタイムに表示されます。
この状態で構造体の中にさらに構造体や配列が存在する場合、Class名の左に「+」マークが表示されるので、この行をクリックすることでさらにその中の値を表示させることも出来ます。
※もう一度押すと非表示に出来ます
ちなみにここではリアルタイムと言っていますが、実際には約100ms毎に現在の最新の値に置き換わるため、その間に何度も変化するような処理になっていると値は間引かれて表示されます。このため、もし細かな値の変化を確認したい場合は、値を書き換えているコード部分にブレークポイントを置くなどして、1回ずつ止めて確認するようにしてください。
※上記画面ではMML文字列の解析でpAddrのポインタがどんどん進んで行くのが確認出来ます
まずはダウンロードしたビルドツールを任意のフォルダに解凍します。
次に、この中にある「開発用テンプレート(PSG音源対応)」フォルダの中に入ります。
この中にあるgame.cを開くと最初の方にグローバル変数が定義されていますが、今回はこの中のMMLPLAYER構造体のmMP変数を確認してみることにします。
///////////////////////////////////////////////////////////////////////////////// // グローバル変数 ///////////////////////////////////////////////////////////////////////////////// KEYDATA mKeyData; // キー入力 SHORT sX; // X座標 SHORT sY; // Y座標 MMLPLAYER mMP; // MMLプレイヤー volatile float fNowTick; // PSG再生用 volatile WORD wOldTick;
ちなみにMMLPLAYER構造体はMMLPlayer.hに定義されています。
// MMLプレイヤー定義 typedef struct _MMLPLAYER { BOOL bPlay; // 再生中か BYTE cBpm; // 現在のテンポ(初期値120) BYTE cMixer; // 現在のミキサーレジスタの状態(def=0x3F[OFFの場合はビットが1となる]) MMLCHANNEL mChannel[MPMAXCHANNEL]; // 各チャンネルのMML情報 CHAR *pMacro[26]; // マクロのポインタ(マクロの開始文字列のポインタ/NULLならマクロ無し) } MMLPLAYER,*LPMMLPLAYER;
SDCCがインストールされている状態で、フォルダ内にある「!!!build.bat」を実行してみましょう。
実行するとビルドが開始され、ビルドが成功したら何かキーを押すことでエミュレーターが起動し、プログラムが即座に開始されます。
プログラムが起動するとエミュレータ画面の他に、GlobalVarsWindowとDebugWindowの2つウィンドウが表示されますが、グローバル変数はGlobalVarsWindowの方で確認出来ます。
GlobalVarsWindowの一番上の空白部分が、グローバル変数を選択するドロップダウンリストになっています。
ここをクリックすると現在デバッガが認識しているグローバル変数の一覧が表示されるので、ここからmMPを探して選択します。
※グローバル変数の数が多い場合はリストをスクロールして目的の変数名を探してください
選択すると下の部分に現在の変数の状態がリアルタイムに表示されます。
この状態で構造体の中にさらに構造体や配列が存在する場合、Class名の左に「+」マークが表示されるので、この行をクリックすることでさらにその中の値を表示させることも出来ます。
※もう一度押すと非表示に出来ます
ちなみにここではリアルタイムと言っていますが、実際には約100ms毎に現在の最新の値に置き換わるため、その間に何度も変化するような処理になっていると値は間引かれて表示されます。このため、もし細かな値の変化を確認したい場合は、値を書き換えているコード部分にブレークポイントを置くなどして、1回ずつ止めて確認するようにしてください。
※上記画面ではMML文字列の解析でpAddrのポインタがどんどん進んで行くのが確認出来ます
変数メモリ参照
このGlobalVarsWindow画面にはもう1つ機能があり、右側のAddress値の書かれた変数の行をダブルクリックすることで、DebugWindowの@RAMタブにそのアドレスのメモリの値を直接表示させることが出来ます。
例として以下のbPlayの行をダブルクリックすると・・・
DebugWindowの@RAMタブが自動的に選択され、さらに該当する変数のアドレス(この場合は0x5070)の値が赤字で表示されます。
ちなみにBOOLはplatform.hにてunsigned charと定義されているため、ここでは1byte分が赤字となっていますが、例えばこれがshort型の場合2byte分が赤字になります。グローバル変数というのはRAM上では常に固定のアドレスが割り当てられるので、bPlay以降の変数もその次のアドレスに順番に入っているのが確認出来ると思います。
なお、@RAMタブはリアルタイム表示は行っておらず、何らかの操作などで画面の再描画が発生したときにのみ更新されます。このため@RAMでメモリ状態を確認する場合は、ブレークをかけた状態や値があまり変化しないものを見たい時にのみ使用することをお勧めします。
例として以下のbPlayの行をダブルクリックすると・・・
DebugWindowの@RAMタブが自動的に選択され、さらに該当する変数のアドレス(この場合は0x5070)の値が赤字で表示されます。
ちなみにBOOLはplatform.hにてunsigned charと定義されているため、ここでは1byte分が赤字となっていますが、例えばこれがshort型の場合2byte分が赤字になります。グローバル変数というのはRAM上では常に固定のアドレスが割り当てられるので、bPlay以降の変数もその次のアドレスに順番に入っているのが確認出来ると思います。
なお、@RAMタブはリアルタイム表示は行っておらず、何らかの操作などで画面の再描画が発生したときにのみ更新されます。このため@RAMでメモリ状態を確認する場合は、ブレークをかけた状態や値があまり変化しないものを見たい時にのみ使用することをお勧めします。
グローバル変数の制限
ここではデバッガが認識出来るグローバル変数の定義の仕方について説明します。
まず前提として、今回のエミュレータは「.c」と「.h」ファイルを全て同列に解析します。
例えばあるcファイルの中のみで使用する構造体の名前が、他のhファイルに記述した構造体と同じ名前になっていると、どちらの型か判断できずに間違った型として表示されてしまう可能性があるため、デバッガを使用する場合はそのファイルでしか使用しなかったとしても、同名の定義を避ける必要があります。
念のため、コンパイラの挙動をあまり詳しく理解していない人のために説明すると、まずコンパイルと言うのは「.c」ファイル単位で行われます。勘違いしている人がうちの会社にもいましたが、「.h」はソースでは無いのでコンパイル対象ではなく、あくまでもcファイルからインクルードされることでそのcファイルの一部としてコンパイルされます。
コンパイルを行うとアセンブラファイルが生成され、さらにこれをアセンブルしたものがオブジェクトファイルとして出力されますが、このオブジェクトファイルはあくまでもそのcファイルの結果であり、他のcファイルとはまったく関係の無い状態となっています。そしてここで重要なのは、他のcファイルの関数を呼び出す場合、呼び出し先の最終アドレスはこの時点ではまだ決まってはおらず、空白状態となっているということです。
最後に、これらのオブジェクトファイルをリンクすることで最終的なプログラムコードを生成するわけですが、この時始めて各オブジェクトファイルが実際のメモリアドレス上に配置され、この時に各関数のアドレスが決まります。そして最後に空白だった呼び出し先のアドレスを埋めていくわけですが、そのアドレスを求めるためには関数名や引数と一致するかにて判断し、もしそのアドレスが見つからなかった場合はリンクエラーで失敗となります。
例えば引数の異なる同名の関数をそれぞれ別のhファイルに定義していた場合、上記の通りcファイルごとにコンパイルされるということは、各cファイルがインクルードしたhファイルの定義に合わせたコードが生成されてしまい、最後に正しくリンクできないといったエラーが発生します。これを避けるため、普通はhファイルには全cファイルが共通で使用するものを定義し、複数のhファイルに同名の定義をするような書き方はしてはならないわけですが、今回のエミュレータではC言語の仕様にのっとった解析はしておらず、ソースコードに含まれるすべてのhとcファイルを1回だけ解析し、全ての定義を全体で使用するといった仕様になっているため、もし別々の箇所に同名の定義を行ってしまうと、デバッガが間違った情報を表示してしまうことになります。
まず前提として、今回のエミュレータは「.c」と「.h」ファイルを全て同列に解析します。
例えばあるcファイルの中のみで使用する構造体の名前が、他のhファイルに記述した構造体と同じ名前になっていると、どちらの型か判断できずに間違った型として表示されてしまう可能性があるため、デバッガを使用する場合はそのファイルでしか使用しなかったとしても、同名の定義を避ける必要があります。
念のため、コンパイラの挙動をあまり詳しく理解していない人のために説明すると、まずコンパイルと言うのは「.c」ファイル単位で行われます。勘違いしている人がうちの会社にもいましたが、「.h」はソースでは無いのでコンパイル対象ではなく、あくまでもcファイルからインクルードされることでそのcファイルの一部としてコンパイルされます。
コンパイルを行うとアセンブラファイルが生成され、さらにこれをアセンブルしたものがオブジェクトファイルとして出力されますが、このオブジェクトファイルはあくまでもそのcファイルの結果であり、他のcファイルとはまったく関係の無い状態となっています。そしてここで重要なのは、他のcファイルの関数を呼び出す場合、呼び出し先の最終アドレスはこの時点ではまだ決まってはおらず、空白状態となっているということです。
最後に、これらのオブジェクトファイルをリンクすることで最終的なプログラムコードを生成するわけですが、この時始めて各オブジェクトファイルが実際のメモリアドレス上に配置され、この時に各関数のアドレスが決まります。そして最後に空白だった呼び出し先のアドレスを埋めていくわけですが、そのアドレスを求めるためには関数名や引数と一致するかにて判断し、もしそのアドレスが見つからなかった場合はリンクエラーで失敗となります。
例えば引数の異なる同名の関数をそれぞれ別のhファイルに定義していた場合、上記の通りcファイルごとにコンパイルされるということは、各cファイルがインクルードしたhファイルの定義に合わせたコードが生成されてしまい、最後に正しくリンクできないといったエラーが発生します。これを避けるため、普通はhファイルには全cファイルが共通で使用するものを定義し、複数のhファイルに同名の定義をするような書き方はしてはならないわけですが、今回のエミュレータではC言語の仕様にのっとった解析はしておらず、ソースコードに含まれるすべてのhとcファイルを1回だけ解析し、全ての定義を全体で使用するといった仕様になっているため、もし別々の箇所に同名の定義を行ってしまうと、デバッガが間違った情報を表示してしまうことになります。
型の制限(構造体含む)
グローバル変数として使用出来る変数の型には、C言語の組み込み型(charやint、shortなど)とポケコン用の独自型(BOOLやCHARなど)、さらに自作の構造体が使用出来ます。ただし、これらの型を使ったグローバル変数の定義には、解析の関係上いくつかの制限があります。
また、#if系による分岐は解析順の関係でいつ#defineされるか分からないため、基本的にはそのファイルのみで判定するようにしてください。
SDCCにてコンパイルを行うとグローバル変数の情報がMAPファイルに書き出されますが、最低でもこのファイルに変数名が出力されていないものはデバッガには表示されません。
※グローバル変数のリストは「_DATA」エリアに列挙されます
以下はデバッガが認識可能な定義の参考です。
- 型と変数名は1個ずつ定義する(同型の変数を「,」区切りで複数定義すると、2個目以降は未定義となる)
- staticは使用不可(そもそもstatic変数とはそのcファイルでのみ使用可能な制限付きの変数なのでグローバル変数ではない)
- constは使用不可
- 配列は1次元まで(2次元以上の配列は不正な参照となる)
- 構造体の中に構造体の定義は可能、さらにネストも可(配列も可能だがこれも1次元までの制限あり)
- ポインタは必ず型と変数名の間に「*」を明示的に付ける(型の後方か変数名の前のどちらでもOK)
- ポインタのポインタは不可(「*」を2つ付けた変数など)
- typedef単体での型の別名定義は不可(「typedef int MYINT,*LPMYINT;」とした場合MYINTやLPMYINTは型として認識されない)
- ポケコン用の独自型(BOOLやCHARなど)はplatform.hの解析からではなく組み込み型として認識(platform.hを使わず独自で再定義してしまうと型が一致しなくなる)
- 構造体は「struct NAME { *** };」または「typedef struct _name { *** } NAME,*LPNAME;」の形式のみで、この時は「NAME」が型となる(2つ目のLPNAMEは無視される)
- #defineで配列数を指定する場合は直値のみ可(「()」でくくった数値は可だが他のdefineや計算式などを入れるのは不可)
- enumは解析していないため配列数などには使用出来ない
- #defineの値は「#if defined(***)」や「#if !defined(***)」、また「#ifdef ***」や「#ifndef ***」による定義の有無チェックが可能
- #defineの値が数値の場合、#if判定で使用可能(0なら無効、それ以外なら有効)
また、#if系による分岐は解析順の関係でいつ#defineされるか分からないため、基本的にはそのファイルのみで判定するようにしてください。
#define INCLUDE_STDIO 1 // stdio.hをインクルードするか #if INCLUDE_STDIO #include <stdio.h> #endif
SDCCにてコンパイルを行うとグローバル変数の情報がMAPファイルに書き出されますが、最低でもこのファイルに変数名が出力されていないものはデバッガには表示されません。
※グローバル変数のリストは「_DATA」エリアに列挙されます
以下はデバッガが認識可能な定義の参考です。
/////////////////////////////////////////////////////// // 組み込み型 /////////////////////////////////////////////////////// int val; // 通常の変数は○ int val[8]; // 1次元配列は○ int val[8][8]; // 2次元配列は× int *val; // ポインタは○ int *val[8]; // ポインタの1次元配列は○ int *val[8][8]; // ポインタの2次元配列は× int val1,val2,val3; // 変数を一度に定義する場合、val1は○だがval2とval3は× const int val; // constがあると× /////////////////////////////////////////////////////// // ポケコン用組み込み型(platform.hに定義されているもの) /////////////////////////////////////////////////////// BOOL bVal; // unsigned charと同じなので○ CHAR cVal1; // signed charと同じなので○ BYTE cVal2; // unsigned charと同じなので○ SHORT sVal; // signed intと同じなので○ WORD wVal; // unsigned intと同じなので○ /////////////////////////////////////////////////////// // 構造体 /////////////////////////////////////////////////////// struct TEST1 { // TEST1構造体は○ int iVal; char cVal; CHAR *pVal; }; struct TEST1 mTest1; // TEST1構造体の実体は○ struct TEST1 *pTest1; // TEST1構造体のポインタは○ struct TEST1 **ppTest1; // TEST1構造体のポインタのポインタは× struct TEST2 { // TEST2構造体は○ int iVal; char cVal; CHAR *pVal; } mTest2; // 構造体定義と同時に変数定義は× struct TEST2 mTest2; // TEST2構造体の実体は○ struct TEST2 *pTest2; // TEST2構造体のポインタは○ struct TEST2 **ppTest2; // TEST2構造体のポインタのポインタは× typedef struct _TEST3 { // _TEST3は無視される int iVal; char cVal; CHAR *pVal; } TEST3,*LPTEST3; // 1つ目のTEST3構造体は○、2つ目以降のLPTEST3構造体のポインタは× TEST3 mTest3; // 別名定義のTEST3構造体の実体は○ TEST3 *pTest3; // 別名定義のTEST3構造体のポインタは○ struct _TEST3 mTest3; // _TEST3構造体は× typedef struct _TEST4 { TEST3 mTest3; // 別名定義のTEST3構造体の実体は○ TEST3 mTest3[8]; // 別名定義のTEST3構造体の配列は○ TEST3 *pTest3; // 別名定義のTEST3構造体のポインタは○ TEST3 *pTest3[8]; // 別名定義のTEST3構造体のポインタ配列は○ } TEST4; // 構造体内の全ての変数が○ならこの構造体も○ TEST4 mTest4; // 別名定義のTEST4構造体の実体は○ /////////////////////////////////////////////////////// // 配列定数 /////////////////////////////////////////////////////// #define WIDTH 4 #define HEIGHT 3 #define SIZE (WIDTH*HEIGHT) char buf[4]; // 直値は○ char buf[4 * 3]; // 計算値は× char buf[WIDTH]; // 直値の定数は○ char buf[SIZE]; // 直値ではない定数は× char buf[HEIGHT][WIDTH]; // 直値でも2次元配列は×
アセンブラ上のグローバル変数
アセンブラのソースコード上にあるメモリエリアは、それがグローバル変数なのか単にワークエリアとして使うものなのかを判断するのはとても困難のため、アセンブラファイルからの解析は行っていません。代わりにこのエミュレータでは、このメモリエリアの情報をC言語のヘッダなどでextern定義することで、デバッガ上であたかもC言語で用意されたグローバル変数として表示させることが出来ます。
まずは、自分で用意したアセンブラファイル(.as)をデバッガ上で認識させるために、最初のあたりに「.module」指定を行います。これを行わないとデバッグウィンドウのコードタブに表示されなくなるので、必ずアセンブラファイル名と同じ名前を指定しておいてください。
一般的にC言語からアセンブラ関数を呼び出すには、まずジャンプ先のラベル名が関数名となり、さらにその名前の先頭にアンダーバーを付けたもの、さらにグローバル関数であることを認識させるため、ラベル名のあとにコロンを2つ付けるといった規約がありますが、これと同じことをメモリエリアのラベル名に行うことで、C言語からこのメモリエリアの値を参照することが出来るようになります。
※逆に言えばC言語で「Test」という名前の関数や変数を定義すると、コンパイラによって「_Test::」ラベルのあるアセンブラコードが生成される
例)
例えばあるcファイルに定義したグローバル変数や関数を、他のcファイルから参照したいことがあると思いますが、この場合は実体として用意するのは必ず1個だけで、それ以外のcファイルでは変数や関数は外部にあるよという意味のextern指定を行います。もしexternをし忘れると、それぞれのcファイルに実体が作られてしまうため、リンク時に同じ変数が複数存在するといったエラーが表示されます。
なお、コンパイラはこのexternされた変数があれば同時に型も分かるので、あとは最終的な変数のアドレスを考慮したダミーコードを生成しておき、リンク時にその変数のアドレスを代入して最終的なコードを生成します。
ちなみに、あるアセンブラファイルにあるグローバル変数や関数を他のアセンブラファイルでも使用したい場合、SDCCには外部にあるということをアセンブラに教える「.globl」定義があります。例えばあるアセンブラファイルにグローバル関数として_SetData関数が定義されており、これを他のアセンブラファイルから呼び出したい場合、呼び出し元のアセンブラファイルのどこかに「.globl _SetData」と定義しておくことで、アセンブル時にこの関数は外部にあるものとしてダミーコードが生成されるようになります。
※グローバル変数も同じ方法で外部にあるものを参照出来ます
なお、この関数をC言語で使わないのであれば、特にアンダーバーを付ける必要はありません。
※C言語の規約に沿る必要は無し
まずは、自分で用意したアセンブラファイル(.as)をデバッガ上で認識させるために、最初のあたりに「.module」指定を行います。これを行わないとデバッグウィンドウのコードタブに表示されなくなるので、必ずアセンブラファイル名と同じ名前を指定しておいてください。
一般的にC言語からアセンブラ関数を呼び出すには、まずジャンプ先のラベル名が関数名となり、さらにその名前の先頭にアンダーバーを付けたもの、さらにグローバル関数であることを認識させるため、ラベル名のあとにコロンを2つ付けるといった規約がありますが、これと同じことをメモリエリアのラベル名に行うことで、C言語からこのメモリエリアの値を参照することが出来るようになります。
※逆に言えばC言語で「Test」という名前の関数や変数を定義すると、コンパイラによって「_Test::」ラベルのあるアセンブラコードが生成される
例)
アセンブラファイル(test.as)
ヘッダファイル(test.h)
※platform.hにてBOOLは「unsgined char(1byte)」、CHARは「signed char(1byte)」、SHORTは「signed
int(2byte)」と定義、デバッガ上ではポケコン用の独自型として解析される
※mCharaDataはCHARADATA構造体(5byte)でその配列が24個分
※pCharaTargetはCHARADATA構造体だがポインタなので2byte
.module test _bSetReg:: .ds 1 _mCharaData:: .ds 5 * 24 _pCharaTarget:: .ds 2
ヘッダファイル(test.h)
extern BOOL bSetReg; typedefstruct _CHARADATA { CHAR cIndex; SHORT sX; SHORT sY; } CHARADATA; extern CHARADATA mCharaData[24]; extern CHARADATA *pCharaTarget;
※mCharaDataはCHARADATA構造体(5byte)でその配列が24個分
※pCharaTargetはCHARADATA構造体だがポインタなので2byte
例えばあるcファイルに定義したグローバル変数や関数を、他のcファイルから参照したいことがあると思いますが、この場合は実体として用意するのは必ず1個だけで、それ以外のcファイルでは変数や関数は外部にあるよという意味のextern指定を行います。もしexternをし忘れると、それぞれのcファイルに実体が作られてしまうため、リンク時に同じ変数が複数存在するといったエラーが表示されます。
なお、コンパイラはこのexternされた変数があれば同時に型も分かるので、あとは最終的な変数のアドレスを考慮したダミーコードを生成しておき、リンク時にその変数のアドレスを代入して最終的なコードを生成します。
ちなみに、あるアセンブラファイルにあるグローバル変数や関数を他のアセンブラファイルでも使用したい場合、SDCCには外部にあるということをアセンブラに教える「.globl」定義があります。例えばあるアセンブラファイルにグローバル関数として_SetData関数が定義されており、これを他のアセンブラファイルから呼び出したい場合、呼び出し元のアセンブラファイルのどこかに「.globl _SetData」と定義しておくことで、アセンブル時にこの関数は外部にあるものとしてダミーコードが生成されるようになります。
※グローバル変数も同じ方法で外部にあるものを参照出来ます
なお、この関数をC言語で使わないのであれば、特にアンダーバーを付ける必要はありません。
※C言語の規約に沿る必要は無し
ブレークポイント(アセンブラ)
ここではアセンブラ上でブレークポイントを設定する方法について説明します。
※C言語のブレークポイントについては、以前の記事に詳しく載っているのでそちらを確認してください
アセンブラ上でブレークポイントを使用するには、C言語と同じくアセンブル時にエラーが出ないように何もしないマクロを利用します。ここでは例として、アセンブラで書かれたスタートアップコード「crt0.s」を参考に説明しますが、自分でアセンブラファイルを用意する場合は拡張子を「.as」としてください。
※今回から「.as」をアセンブラファイルとしてアセンブルし、さらに自動的にリンクするようバッチファイルが組まれています
まずはアセンブラの最初あたりに「$」という何もしないマクロを定義します。(crt0.sには既に記述されています)
そして、ブレークしたいコードの1行前に「$」と定義します。
※C言語のブレークポイントの設定方法とは違うので注意
ここでは例として、スタートアップコード内のmain関数を呼び出す直前にブレークを置いてみます。
ちなみに間違えてC言語と同じようにコード行の先頭に指定してしまうと、正しくアセンブル出来ずにビルドに失敗します。
※何もないコードが生成されたりする
この状態でビルドして実行すると、$の次の行のmain()関数が呼び出される直前でブレークがかかります。
あとはこの時のレジスタの状態を確認したり、上部の「Step(Asm)」や「Step(C)」を押してコードを1個分進めてみたり、「Continue」ボタンを押すことでプログラムを再開させることが出来ます。
なお、C言語と同じくアセンブラ上でも複数ブレークポイントを指定したり、実行中にもブレークポイントをON/OFFすることが出来るので、これによりアセンブラの開発もかなりしやすくなったと思います。
ちなみに「Breke(C)」のボタンは、プログラム実行中にCの関数単位でブレークをかけられる機能です。
今まではプログラム実行中は新たなブレークポイントを置かない限りプログラムを止めることは出来ませんでしたが、今回からこのボタンを押すことでC関数単位で止めることが出来るため、例えば永久ループで画面が止まってしまった場合など、どこでループしているのかというのが即座に分かるようになります。
※アセンブラ側のループバグなどでCのコードに一切来ない場合はブレークされません
※C言語のブレークポイントについては、以前の記事に詳しく載っているのでそちらを確認してください
アセンブラ上でブレークポイントを使用するには、C言語と同じくアセンブル時にエラーが出ないように何もしないマクロを利用します。ここでは例として、アセンブラで書かれたスタートアップコード「crt0.s」を参考に説明しますが、自分でアセンブラファイルを用意する場合は拡張子を「.as」としてください。
※今回から「.as」をアセンブラファイルとしてアセンブルし、さらに自動的にリンクするようバッチファイルが組まれています
まずはアセンブラの最初あたりに「$」という何もしないマクロを定義します。(crt0.sには既に記述されています)
.macro $ .endm
そして、ブレークしたいコードの1行前に「$」と定義します。
※C言語のブレークポイントの設定方法とは違うので注意
ここでは例として、スタートアップコード内のmain関数を呼び出す直前にブレークを置いてみます。
;------------------------------------ ; スタートアップコード ;------------------------------------ init: ;; スタックを最後のアドレスへ ; LD SP,#0xffff ; グローバル変数初期化 CALL gsinit ; メインルーチンを実行 $ CALL _main ; 終了処理は単にリターンするだけ RET
ちなみに間違えてC言語と同じようにコード行の先頭に指定してしまうと、正しくアセンブル出来ずにビルドに失敗します。
※何もないコードが生成されたりする
; メインルーチンを実行 $ CALL _main ; NG
この状態でビルドして実行すると、$の次の行のmain()関数が呼び出される直前でブレークがかかります。
あとはこの時のレジスタの状態を確認したり、上部の「Step(Asm)」や「Step(C)」を押してコードを1個分進めてみたり、「Continue」ボタンを押すことでプログラムを再開させることが出来ます。
なお、C言語と同じくアセンブラ上でも複数ブレークポイントを指定したり、実行中にもブレークポイントをON/OFFすることが出来るので、これによりアセンブラの開発もかなりしやすくなったと思います。
ちなみに「Breke(C)」のボタンは、プログラム実行中にCの関数単位でブレークをかけられる機能です。
今まではプログラム実行中は新たなブレークポイントを置かない限りプログラムを止めることは出来ませんでしたが、今回からこのボタンを押すことでC関数単位で止めることが出来るため、例えば永久ループで画面が止まってしまった場合など、どこでループしているのかというのが即座に分かるようになります。
※アセンブラ側のループバグなどでCのコードに一切来ない場合はブレークされません
g800エミュレータ(改)のソースコード
上記のビルドツールにはコンパイル済みの実行ファイルが入っているので、ポケコンの開発用途だけであればこちらは特に必要ありません。
※VC2010用のプロジェクトとなります
修正内容は以下の通りです。
g800(改) VC++ Build v1.02のソース | ダウンロード ※SDL2.0.7同梱 |
---|
修正内容は以下の通りです。
- グローバル変数をリアルタイムに確認するウィンドウの追加
- アセンブラ上でのブレークポイントの指定に対応
- 各デバッグ用ウィンドウの位置とサイズを保存
- メモリタブを開いた際に再描画するように修正
--------------------------------------------------------------------------------------- Copyright (c) 2005 ~ 2014 maruhiro All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---------------------------------------------------------------------------------------
コメント
コメントはまだ登録されていません。
コメントする