Make your own free website on Tripod.com





開発めも

LS版の機能を実装するにあたり、色々悩んだり進行中なことについてのメモです。(見ると目が腐ります&違っていたら教えてください。)。
日記形式は面倒なので、適時修正する形にしよう。数回の更新しかしないだろうし。

VBAのソース構成について

実行breakのやり方(パクリ)

とある解析を行うために、久々に触ったエミュレータがlabmaster氏のSDL版だった(今HPは死んでる?)。読み書き実行時に実行される関数をスクリプト側で定義できるようにしたい。ソースを比較することで後で分かったことですが、通常版のSDL版には書き込みbreakしかできず、読みbreakと実行breakについては、彼が独自に変更を行ったものらしい。逆に、こっちのやった作業はそれをWSHにあわせただけといえます。CPUWriteMemory/CPUReadMemoryと分かりやすい関数があって、そこでbreakする関数を呼んでいるのが分かった。またどのメモリーにアクセスがあったときにbreakするかを管理するために、freezeXXXXXというのがROMやRAMのサイズ分確保されているのを確認した。読み書きのbreakについては、win32とCOM/IScriptControlの仕様につまずきながら割とスグにできた。ところが実際に動かしてみた所、実行breakがきかないことに気がついた。

実行breakについてどうやっているのか調査したとこところ、SDL版(何の略か不明。SimpleDirectLayerだかの略ならなぜデバッグ機能のみに特化しているのか?)のデバッグ機能の管理はsdl/debugger.cppにあるらしい。
実際に動かすと出てくる文字列「Breakpoint 0 reached」などで検索すると、debuggerSignalという関数だった。どうやらOPCODEの0xbeのときに呼ばれることが分かったが、どいういう命令なのか不明だし、実行しても一向に出てこない。ふと思いついて0xbeでソースを検索するとみごとに見つかった。
どうやら、ダミーの命令に書き換えておいて、ダミーの命令が見つかったら、呼び出し、後でもともとの命令に差し戻すということをやっているらしい。命令は大抵空きの部分があるのでそれを使用しているということだと想像してみる。
デバッガの実装方法について詳しく知らないが、こうやるのが普通なのかな。(メモリーが沢山あれば、もう一つ用意してもよいのだろうけど)。
細かくいうと、

  1. 指定されたアドレスを実行break用の命令に書き換え
  2. PC(プログラムカウンタ)がその命令に到達すると、PCを1個分差し戻し、専用の関数に飛ばす
  3. 専用の関数にて、break処理を行った後に、そのアドレスの命令を元の命令に書き戻して、1命令分だけ実行を行う。
  4. そのあと、再度、実行break用の命令に書き換えて、次の命令へ

となっていた。この方法の利点は、breakpointに引っかからない場合実行速度が全く落ちないということにあると思っています。まぁこのツールではまったく逆を目指していますが(´ー`)。ちなみに、読み書きbreakは該当メモリと同じサイズだけのフラグ用の領域を作って、フラグがONならbreakするという風になっています。ROM部分も同様で、33M近く*2とドデカイが、未初期化=falseが保障されるようで、その場合使用メモリが増えないようになっているみたいだ。本当に保障されるのかな?(←補足:ANSI-Cでは保証される模様。全然しらなかったよorz。ただ、この領域が、UNIX系でもWindowsでも書き込まれないうちは実際にはメモリが消費されないってところにクライアント系ツールなら意外と使い道あるかも。組み込み系は・・・シラン)。ARMの実行breakもちゃんと動いているのを確認した。テストしてないのに動いてたなんてラッキー。

実行breakの処理順序を変えた方がいいな。
変更前:break検知→スクリプト呼び出し→元命令に書き戻し→1サイクル実行→戻し
変更後:break検知→元命令に書き戻し→スクリプト呼び出し→1サイクル実行→戻し

CPU周り

レジスタはVBA.cppのregという変数で管理していた。
が、フラグレジスタ周りは2重管理されてて(もちろんSPSRのことではない)、書きかえる場合は利用者が同期させるようになっている模様(同期する関数は用意されている)。
で、N_FLAGとreg[16]の該当ビットのどちらがプライマリなのかが重要だが、CPUの命令を実行させているところの処理をみる限り(arm-new.hとか)、どうもN_FLAG模様。他も同様だろう。
CPUUpdateFlagsはreg[16]の内容を各フラグに適用するもの(reg[16]に書き込んだ後に呼び出す)。CPUUpdateCPSRは各フラグの内容をreg[16]に適用するもの(reg[16]を参照する前に呼び出す)。
なので、cpu.nで参照/書き込みしたときはN_FLAGだけ書き換え、reg[16]への反映はせず、cpu.reg(16)として参照しようとした場合にフラグの内容をreg[16]に反映してから読み込むようにした。
T,Iフラグに対応する変数はそれぞれarmState、armIrqEnableですが、値は反転して管理されていたので参照/書き換え時に気をつける必要があった。Fフラグはreg[16]&0x40がマスター。
breakloopとはなんだろう。1サイクル止めるとかそんな感じなのかな。
4byte書きと1/2/4byte読みの関数はGBAinline.hにあったが、2/1byte書きの関数はvba.cppにある。

その他

今のところ、ソフトの解析の高速化にはあまり役立たないと思うけど、何かしら方法はあると思ってる。といいつつ最近だんだんデバッガになってきた。解析した後に色々出来るようにしたいというのがそもそもの目的だけど。

イベント系の関数の仕様が結構ギチギチ。引数と戻り値が。これを解消するためには色んなオブジェクトを用意して、ユーザ側にそれを使用させるということをやればよいのだろうけど、今回のようなパターンであれば、小規模なのでそこまで必要がない気がする。逆にある方が分かりずらくなる気がするのでこのままにすることにしてみる。

イベントとタイマーとボタンを押すのが重なり合うかどうかについてだけど、GetCurrentThreadID()で確認したら全部同じスレッドIDだったので、問題ななさそう。←Windowsをよく理解してない人。←というかモーダレスダイアログボックスのメッセージ検知方法もhookという便利な仕組みによって実現されてた(メッセージループが別途必要だと勝手に思っていて、そのためにスレッド立てていると想像していたが、そういうわけではないみたい。)

どうでもいいけど、AboutBoxのところに名前を書く欄があって、labmasterさんのをパクったせいで名前が「Kenobi and Labmaster」となっていたところを、「Kenobi, Labmaster, denopqrihg and rasu」という風に書き直してみたけど、勝手にVBALinkさんとかの名前つかっちゃってもよいですよね。

ソースをマージする際、差分を見ながら手動で行ったのですが、リソースのマージをやったらリソースのコンパイルが通らなくなった。2重定義になってるとのこと。しかし、原因はリソースの「Version」というセクションが日本語、チェコ語、英語(アイルランド)と3つあったせいだった。とりあえず日本語とチェコ語を削ると動くようになった。

CPUの処理時間とか音との同期は全然把握していないので、問題がありましたらすみません。あとイベントやタイマーを設定した後に、ゲームを切り替えたりすると変になることに気づいた。まぁそんなにないからとりあえず放置。

ReleaseNoDevで作った実行ファイルだとなぜか、memread1とmemread2が値を0しか返却しなくなった。デバッグ版はちゃんと値を返すのに。しかたないので戻り値をlongからVARIANTに変更するとちゃんと値を返すようになった。謎。

IScriptControlについて

スクリプトコントロールは相当魅力があるものだと思っていたりするのですが(少なくともデバッグ用ツールなどでは)、意外と情報がありません。VBなどで簡単に出来るのは確認済みだったので、(.netなどならもっと楽だろうけど)、VCでもやることにした。出来ればわけわかなAPIではなく簡単なコントロールを仕様する方法で。APIの資料はそれなりに見つかるのですが、スクリプトコントロールについてはほとんどない。(CodeGuruのサンプルが一番使えたりした)。

あと用語。ホストとは何を言っているのか。ScriptSiteのSiteとは何を指して呼んでいるのか。COMどころかWindowsもおぼつかないためさっぱりだ。と思ったが英語の辞書みたら理解できた。
情報も日本語ならarton さんの所か、excelさんの所(丁度閉鎖した所ようですが)しかまともに見つけられなかった。追加、正直ここが分かりやすかった。
で、スクリプトコントロールの使い方は AddCodeはVBでやるとクラスをスクリプト側に公開するものだと分かったりするのですが、Reset一つとっても、何がResetされるか結構不明だったりした(オブジェクトをリセットといわれても汎用的すぎだとおもーた)。で、サンプルはグローバル関数の呼び出し方みたいなものはいくつか見つかったが(どこを見ても同じような感じ)、引数で渡されたものをどう扱ってよいか。ParseScriptTextした後の作られるオブジェクトへのアクセス方法について色々な方法が知りたい。スレッド構成はどうなっているのか。(どこにMutexを置けばよいのか)

最重要なこととして、エミュレータとスクリプトの同期がどうなっているかいまいち把握していいないけど、スクリプトコントロールだと複数スレッドに対応していないらしい→単一スレッドだけなので、エミュとスクリプトが上書きしあうことはないと考えてよいのかな。まぁ、同期しているようなので放置(スタックとか見ても別スレッドということはない)。最大の懸念点だったが、問題ないようで。

 

その他メモ

以下メモです。test1という関数がスクリプト側に公開した関数。test1が受け取ったオブジェクトをどう解析すれば目的のものをGETしたり呼び出したり出来るか。

スクリプト側がプログラム側で用意した関数を呼び出すとき

test1という関数の定義は
void test1(VTS_VARIANT); という感じになっているものとします。

○整数(Number)を渡す
var i = 0;
test1(i);
このときのvtはVT_I4で来た。JavaScriptのNumberは-0xFFFFFFFF〜0xFFFFFFFFとint型を超えたサイズを使えるが、0x90000000のような超えた値である場合はVT_R8で来た。

○オブジェクト(構造体)
function AAA(){ this.a=0;}
var i=new AAA();
→VT_DISPATCH
i.aのアクセスはGetIDsObNamesで本当にいけるかは未確認。←本当にいけることを確認した。
あと、オブジェクトに.aというプロパティやそれ以外にもプロパティが存在することを知るためにはどうすればよいのか。←どうやらCOMにはコレクションという概念があり、それを使えばできるらしい。多分、IEnumVariantかIEnumDispath。でもMFCであっても特にラップクラスがないためSDKみたいにがんばらないといけないらしい。
VT_DISPATCHが引数で渡された時はAddRef必須(渡されたものが、一時オブジェクトの可能性があり、時間がたつとスクリプト側で削除されるため、削除するなと知らせてやる必要がある)。

○オブジェクト(配列)
var i = [1,2,3]
→VT_DISPATCH
例えば、i[0]にアクセスしたいなら引数で渡される
LPDISPATCH->GetIDsObNames()で名前を"0"でDISPIDを取得し、それでINVOKE。
i[2]ならi.2と同じなのでGetIDsOfNames()では"2"を指定し、それでINVOKE。 この方法を書いているのはAXS_FAQでしか見つからなかった。
これで知りたいことが、存在しない箇所に投入可能かということ。

○オブジェクト(関数)
function AAA(){ this.a=0;}
test1(AAA);
→test1の引数のvtはVT_DISPATCHになる。
この関数を呼びたい場合は、
DISPIDを0でINVOKEする。(GetIDsOfNamesは呼ばない)。
スクリプトコントロールで提供されるIScriptProcedureを使えば引数で渡された関数を呼び出せるのかなと思いきや、グローバル配下であれば出来るが、一時オブジェクトとして作成された関数を呼び出す方法が見つからない。関数ぐらいラップしたクラスで呼べるとおもったらどうもGetIDsOfNames&Invokeしなくてはならないようで、コントロール使っているのは初期化が楽になるぐらいにしかなっていない気がする。出来るだけラップしたものを使用したい。

 

スクリプト側の動的作成した関数、オブジェクトにプログラム側がアクセスする場合

クラスウィザードにて、msscript.ocxを組み込んだ場合、runというインターフェースが「メソッド 'Run' は戻り値の型またはパラメータ型が無効なため表示できません。」という表示となり、使えない。runにて任意の関数の呼び出しが可能であることはVBで確認済みであるため、ぜひ利用したかったが、残念。#importなら使えるようですが。ただこのrunメソッドは名前指定で関数を呼べるが、同じことがIScriptProcedureCollectionにて関数の列挙が可能なので(by CodeGuruサンプル)、代用は利くかなと思ってたり。

IScriptProcedureCollectionの仕様について、一番知りたいと思っていたのが、Global直下の関数(オブジェクト)のみ列挙可能なのかそれとも現在のスコープでアクセス可能な関数の列挙なのかということ。調査した所、Global直下の関数のみだった。どいういうことかというと、IEであれば

1  <script type="text/javascript">
2 var x = null;
3 function funcA(){
4 function testB(){ x = new ActiveXObject("myApp.test"); } //仮にこういうものがあるとする。
5 testB();
6 x.method1(); //testBは参照可能であるはず。
7 }
8 testA();
9 x.method1(); //testBは参照不可能なはず。
10 </script>
11 <body> 〜 </body></html>

自前マクロの場合は2-9行目がAddCode/ParseScriptTextされる部分だと想定していますが、
method1内部で列挙した場合、VBで次のようなものを試すと、6行目部分で列挙してもtestB関数は列挙されなかった。
現在のスコープというのであれば、便利だったのに。残念。

For Each element In Form1.ScriptControl1.Procedures 'この処理自体はmethod1に該当する関数内部で定義されている
  MsgBox "function:" + element
Next

Proceduresっていうのは、オブジェクトブラウザの説明は「Collection of procedures that are defined in the global module 」とあり、global moduleっていうのは上のHTMLのソースの例だと2,8,9行目部分に該当する所だと解釈出来ると思ってみる。
オブジェクトブラウザやMS公式の言っている「Module」の概念っていうのは変数のスコープの範囲のことをさしているのかな。
Moduleの中にModulesがあるのであれば、そう理解できるんだけど。むぅ。

 

プログラム側がスクリプト側に公開するI/Fに関して

■引数

このへんはMFCのクラスウィザードでやっているのですが、使えると思ったのは、
真偽:VT_BOOL
文字列:VT_BSTR
数値:VT_I4
何でも: VT_VARIANT
関数かオブジェクト:VT_DISPATCH

で、省略可能な引数にしたければ、VT_VARIANTにすれば出来た。ただVARIANTは型変換メンドいでス。いい方法があるんだろうけど・・・
ちなみに、サクラエディタのマクロがどうなってるかを調べてみた所、CSMacroMgr.cppで管理しているようで、関数の引数に関しては数値か文字列しか扱ってない模様。(v1-5-5時のとき)
このツールの場合、0xFFFFFFFFまで扱いたいので、単純にVT_I4と一致するかではなく、型変換する努力が必要だった。というのは単に引数をVT_I4にするとスクリプト側で0xFFFFFFFFを渡そうとするとエラーが発生したため。また、戻り値もクラスウィザードでVT_I4にしちゃうと、スクリプト側でマイナスの値が返却された。DISP_FUNCTONの所の戻り値をVT_UI4と出来るかためしてもコンパイルが通らなくなった。そのため戻り値を、VARIANTにして、vtをVT_UI4にすれば0xFFFFFFFFを返却できるようになった。戻り値はdouble(VT_R8)にしてもよさそうだけど、スクリプト側がどこまで受け付けられるかの調査が発生するためやめた。

■オブジェクト返却

プログラム側が用意したカスタムオブジェクトを返却するには、CCmdTargetを継承したクラスにはGetIDispatchという関数が用意されているのでその戻り値をスクリプト側に返却すれば出来た。

//-- スクリプト.js --//
var R0 = vba.event.onread(...); //VBAがトップレベルのカスタムオブジェクト。EVENTもカスタムオブジェクト

//-- ソース VBA.cpp --//
LPDISPATCH VBA::GetEvent(void){ //この関数自体はVBAクラスのメンバで、クラスウィザード→オートメーション→メンバ追加したときに自動生成される関数。
return m_event->GetDispatch(true); //m_eventもCCmdTargetを継承したクラス。GetDispatchの引数をtrueにしないと、エラーとなった。ちなみに意味は返却前にAddRefしとくかどうかという意味らしい。
}

自作オブジェクトをスクリプト側に公開した時for inにてループさせるためには、CCmdTargetの返却するIDISPATCHではダメだった(for inしても1つもループされない)。MFCでやるにはどうすればよいのだろう。そもそも出来るのか。←これについては上のほうでも書いたとおりIEnumなんとかってやつでいけるらしい。

エラーの検知時の動作

スクリプトの記述がミスっていたときの、エラーハンドリング処理について、そもそものスクリプトコントロール構成を把握していなかったせいで、結構散乱したソースとなっています。(その前にC++でのデータ構造化技法が理解出来てないんだろうな)

それはともかく、 テキストエディタ系のマクロと違ってユーザが何もしなくても、がんがんユーザ定義の関数が呼び出される可能性があって(萌エディタとかは別なのかな?)、呼び出されるだけだとそれでもいいんだけど、そのときにスクリプトの記述ミスなどでがエラーである場合がある。

その場合の動作について、IEと同様に次の機能を実現したい。

なぜこんなことを言うかというと前提として、IEもFireFoxもそうですが、記述ミスしていた際は実行時の関数呼び出しで初めて分かるようになっているようです。例えば、IEで

function testA(){ a } //aは存在しない変数
setInterval("testA();", 5000);//5秒後にtestAを実行。この行自体は読み込み時に実行される。

とした場合、画面を表示して5秒後に警告が出ます。ファイル読み込み時にはtestA関数内の記述には関知しないということなのでしょう。( {}にズレがある場合はそもそも関数のくくりが違うのでエラーは出たりしますが)
この動作は、スクリプトコントロールでAddCodeしたときも同じでした(というかSDKでも同じですが)。
ちなみに、IEではさらに5秒たっても警告は出ないが(setInterval自体が無効になったと思われる)、FireFoxではエラーが出続けるという違いがあった。(JavaScriptコンソールを見ないと分からないのですけど^^;)
で、このツールでは、IEと同様の方向で行くことにした。

ついでにいうとIEでアニメーションみたいなことやりたい時、次のようなコードを書いたりするのですが、

function testA(){ setTimeout("testA();", 5000); a } //aは存在しない変数
setTimeout("testA();", 5000);//5秒後testAを実行。この行自体は読み込み時に実行される。

この場合、IEでもFireFoxでもエラーが出続けました。つまり、testA関数内でsetTimeoutした場合、そのタイマーは無効にはならないようです。(エラー発生箇所までの処理は有効。発生箇所より後の処理は実行されないという動作の模様)
しかし、このツールではエラーのダイアログが出続けないように、全てのタイマーを止めることにしました(つもり)。
少しでもエラーがあるとスグに止めるというチキンなやり方なのですが、IEなどと比べカスタマイズ度が当然低いわけで、逆にFireFoxのJavaScriptコンソールがあればこんなことしないのですが。

で、Invokeしたときのエラーハンドリングは次のようにした。(擬似コード)

EXCEPINFO errinfo;
hr = pDisp->Invoke( ... , &errinfo, ...); //スクリプト側の関数が呼び出される
if(!SUCCEEDED(hr) { AfxThrowOleDispatchException( errinfo ); } //エラーだったら、上位でIScriptErrorにて料理してもらう

ただ、event時にInvokeでエラーが発生した時は注意が必要だった。というのは、例えばonExecだと、プログラムカウンタとかスタックポインタなどCPU内部の値を変更した状態で呼び出しているため、Invokeしてすぐにエラーチェック→throwとすると、エラーのダイアログが出た後勝手にリセットされたり不思議な動作になってしまった。その辺の不整合がないようにthrowするようにした(つもり)。しかし、読み込み命令でonExec&onReadを組み合わせで発生した時にエラー発生後の動作が正しくなるかはテストしていない。

この方法で改善したいと思っているのが、IScriptErrorには次のようにどこが原因でエラーとなったか出力する機能があるのですが、

このうち、GetTextで何も得ることが出来なくなってしまった。おそらく、IActiveScriptErrorではGetSourceLineTextがそれに該当するものだろうが、Invokeでのエラー発生時にこの内容を出力したい場合、自前で管理しないといけないんだろうか。

その他

CSafeArrayは何個も引数を渡したいときに使用する。配列を作るためではない。おそらく、引数が何個になっているか分からないときに使うのだろう。

Resetメソッドでリセットされるのは、AddCodeで実行して出来る変数・関数やAddObjectしたクラスが消える。言語の設定やタイムアウトの設定はクリアされない。

event.onread(0x02000000, function AAA(){}));とした場合、第2引数のポインタは時間がたつとエラーになった。でもポインタを受け取ったらaddrefして、解放時にreleaseするようにしたら大丈夫になった。ポインタ系はそんなことしてやらないといけないようだ。おそらく、一時オブジェクトなのでガーベージコレクトされるんだろうな。

WSHは簡単にスクリプト言語を切り替えられるが、スクリプト言語によって実装されている機能とされていない機能の差って重要な気がする。onreadのような関数渡しってVBScriptでは出来ない(Global配下の関数のみという風な制約をつければVBScriptでも問題ないだろうがそれは制約になってしまう)。←というのがどっかの日記にも同じことが書いてあった。

・各言語でアプリケーション側の引数をVARIANTにしたときに、スクリプト側でその関数を呼んだときのvtの値が何になるか。
VBScript
JScript ←どこ?

・上記の逆。戻り値のパターンによってアプリケーション側が受け取るオブジェクトはどの型になるか。またはスクリプト側にある戻り値を返したいときにどう返せばいいか。実際使えるか。