他人の空似

2015 年 2 月 7 日

.pngから.curを作成するアプリリリース

Filed under: 未分類 — 中の人 @ 5:09 PM

.pngから.cur(マウスカーソルとかのアレ)を作成する小物アプリ作りました。
http://wordpress.click3.org/garakuta/png2cur.zip

元々は友人の手伝いで作った小物なのであんまり実用性はない。
が、それでも一応作ったものなので、適当にreadmeとかこさえて公開しとく事にしました。
(いないと思うけど).curを大量作成する必要に駆られている人とかいたらどうぞ。

2015 年 2 月 2 日

花映塚のAIコンテストに参加したよという話

Filed under: 未分類 — 中の人 @ 5:15 AM

花枕杯に参加してきました。
これは花AI塚というツールを使い、東方花映塚上で動作するAIを実装し戦わせ最強を決めるコンテストです。
自分の総合順位は5位、ただし参加者総数は5人、つまり最下位でした。
まぁ結果は散々でしたが、参加はしたのでその感想とか、参加したAIの解説とか、そういうことを書いておこうと思います。

参加したAIはこちら。
名前:縦移動禁止花AI@メディスン
http://wordpress.click3.org/garakuta/tate_kinshi_hana_ai.zip

AI解説

ひとまず.zipの中に入れた解説を丸ごとペタリ。

縦移動を封印し、横移動だけに特化してそこそこ戦えるようにしたAI。
別になめプではなく、余裕こきすぎて24時間しか作業時間が取れなかったため、
実装内容を絞って時間対効率の最大化を図ったためこうなった。

アルゴリズムも最初考えていたものが処理速度上不可能と発覚したので即興。
その割には結構いい線いってると思うが現実は如何に。

@メディスンとあるが、別に専用カスタマイズされているわけではなく、誰でも動く。
単に作者がメディスン好きなだけである。
メディスンかわいいよメディスン。

一応LunaAI相手でもラウンド取れることもあるぐらいにはちゃんと戦える。
※ただし咲夜さんと映姫様除く。

仕組み:
自身のy座標一列分の配列(以下危険度マップ)を作り
弾や敵などに対し、そのy座標に到達するまでにかかる時間と、到達したときのx座標を算出。
その周囲を到達までの時間で埋めるを繰り返す。

すると、弾が到達するまでにかかる時間が長いx座標ほど安全という理屈により、
どの辺のx座標を目指せばいいかがなんとなくわかるので、
あとはそれを念頭に置いて移動したり色々するだけのAI。

一応軽いキャラ対策などは入れたが、後述の弱点のように問題点は多い。
それ以外にも、粒弾相手でも被弾してしまうようなコーナーケースのつぶしは足りないし、
長期的展望による回避行動もとらないし、C2の運用やコンボなど改善の余地はたくさんある。

弱点:
当然ながら縦には移動しない(というか後ろの弾がまったく見えていないので出来ない)ので、
咲夜さんのC2/C3は撃たれたら真横弾で落ちるのは確定。
映姫様の自機狙いレーザーの対策をとっていないので、されるとあっという間に死ぬ。
文さんのExは判定強いくせに早いので、ちょっと周囲の弾配置が悪いとすぐ詰む。
ルナサを相手にすると後ろから弾が飛んでくるため運ゲー化する。
チルノはパーフェクトフリーズで背後の弾がこっち向かってくると避ける手段がない。
ミスティアは未対策なので、チャージアタックの弾源に重なり落ちる光景がよく見られる。
てゐさんは中央からちょっと外れるとExの軌道に巻き込まれ、そのまま壁と挟まれて死ぬ。

中でも書いてある通り本体の実装に取れた時間がエントリー最終日である1月31日の24時間しかなかったため、突貫作業で実装されたAI。
時間がなくなった理由はこれとかこれのせい。
それ以外にも仕事で休日出勤が4日ほどあった。
まぁ期日が決まってるものがあり、不測の事態もありうるのに、他にかまけていた自分が悪いという話でした。

さてAIですが、最初は縦移動封印などする気はなく、まったく別のアルゴリズムで作るつもりでした。
画面全域を表す二次元配列を作り、そこの弾の予想軌道上に「到達までにかかる時間によって減算される危険度」を設定し、そこから判明する付近の安全領域に向かって移動する、という感じ。
単純にこれだと計算量がとんでもないことになると予想は出来ていたので、対象の弾を自分の周囲に限定したり、弾や自機のxyを割る5した値を使うなど軽量化案はいくつか考えていました。

だが現実は予想以上に厳しかったのです。
なんと300*450(ゲーム内の移動可能範囲+α)の配列をローカル変数として定義しただけで、体感してわかるほどの強烈なラグが。
調査の結果、テーブルのリサイズがありえないほど重いようで、事前に値をつめておいて更新するだけならそれほどでもないことが分かったのですが、自分はこの事件で心が折れてしまいました。
なので、処理速度が問題にならないぐらい軽いアルゴリズムを検討するところから始めることになります、ちなみにこの時1月31日の午前5時5分(エントリー締め切りは1月31日24時00分)。

そこで考えたのが、自分は普段どうやって弾幕を避けているのか。
最初のアルゴリズムの軽量案である”xy割る5″も、自分がプレイするときは1ピクセル単位で見ているわけではなく、もっと荒い単位でとらえているはずだという点からきているからです。
そこで気づいたのが、避けるだけなら最下段に張り付いて、基本的に左右移動だけでなんとかしている、という事実。
実装に割ける時間も少なかったので元から攻撃に関することは諦めようとは思っていました、なので縦の移動を封印し横移動だけに特化すれば、簡単かつ高速に動作するAIが作れるのではないか、と。
考えている時間はありません、なので自分はこの閃きに賭けることにしました。

さて、横移動にだけ限ると言ってもアルゴリズムはいくらでも考え付きます、だがそんなのをまともに検討している時間はありません。
なので初案のアルゴリズムをそのまま横一列に限って使うことにしました。
これは単純に危険度マップのサイズを減らすだけに留まらず、考慮するべきフレーム数問題をも解決しています。
なぜならば、弾がそのy座標を通過する1フレームだけ考慮すれば基本的に十分だからです。
後に、実際には弾の大きさを考慮して前後数フレームは検討しなければならないことに気づきますが、それでも初案に比べれば劇的な高速化でした。
(画面全域、全弾対象、xyを整数に切り上げただけの値を使用、10フレーム先まで計算、と比較すると単純計算で実に1/4500の計算量)
ちなみにこの時午前5時22分。

この後は特に面白い話はなくて。
ひとまずベースの実装を終え(7時15分)
Easyにすら負けるレベルでバグバグだったのでひたすらCPUと対戦してバグをつぶし(10時16分)
朝ご飯を食べ(11時23分)
拡張にかかるコストが大きくなりすぎたのでリファクタをいれ(13時05分)
※この時点で自分では勝てない強さに到達してます、とはいえ自分は花はとても弱い(花Exが安定せず、クリアできたことが過去二回しかない)のであまり参考になりませんが。
LunaCPU相手にひたすら勝負を挑み被弾したら原因を調べて潰すを繰り返し(16時11分)
キャラごとの個別対策をひたすら入れ(21時52分)
全キャラ相手に一戦してそれだけで勝負が終わるほどの致命的バグがないことを確認し提出した(23時26分)

その後は
エントリー人数や使用キャラなどが公開され(02月01日 01時02分)
就寝(01時21分)
花枕杯開始(14時00分)
花枕杯終了(16時00分)
起床して最下位だということを知る(18時54分)
となっています。

さて、AIの解説に戻りましょう。
本AIで縦移動禁止や作成にかかった時間を除く大きな特徴として、マイクロスレッド上に実装されいるというものがあります。
マイクロスレッドというのはコルーチンやファイバーの親戚で、実態はシングルスレッドですがまるでマルチスレッドのように使え、なおかつスレッドの切り替えタイミングを自身で決められる仕組み、です。
これにより、1フレームに一回だけ実行する処理を簡潔に書けたり、フレームをまたいでもローカル変数を維持しておけたり、といった恩恵を受けられます。
と言ってもこれはあれば便利程度の物で、AI作成では大して重要でありません。
ですが、私はマイクロスレッドをとても愛好しているので導入しました(私制作のAIツール3種すべてでマイクロスレッドが提供されているのもそのため)。

マイクロスレッドはmy_lib.lua上に実装されていて、create_([head|tail]_)?threadで作成、yield関数を呼び出すと次のマイクロスレッドに実行が移り、すべてのマイクロスレッドが一度ずつ実行されると次のフレームへ進む。
headやtailはマイクロスレッドの中での実行順序を指定するための物で、headで作られたものはそうでない物すべてよりそのフレーム内で先に呼ばれる、tailは逆に後に呼ばれる。
たとえばtailはキー操作に使われており、他のスレッドでキー操作をしたくなったらグローバルの配列上にポンポン投げ入れ、tailのマイクロスレッドでその結果を一つにまとめてsendKeys関数に渡している。

感想とか色々

花枕杯は非常に楽しかった。
一時期TopCoderをやっていた時も思いましたが、参加者たちがさまざまな方法論でプログラムを作成し、それらを競わせるというものは知的遊戯として非常に面白い物です。
結果は残念なことに最下位で、それ自体は非常に悔しいのですが、そういう感情すらただプログラムを書いているだけでは味わえないもので、醍醐味の一つだと思います。

本選をリアルタイムで見れなかったのは地味に凹みました。
いや前日に30時間以上連続で起きていたのだから当然ではあるのですが、なぜ14時までに起きられなかったのか……。

自分が独自アルゴリズムで参加したのは、作業時間的に劣る自分が勝てるとしたらアルゴリズムで優るしかないという思いからだったのですが、結果だけ見るとそんなことしない方が結果は良かったかもしれません、反省。
だけど、自分のAIの弱さがまるでアルゴリズムが劣るせいととられていそうなのが心残り。
なので、次回があればこのAIの正当進化系で参加しようと思っています。
もしかしたら次回を待たずに余暇で改良して公開するかもしれないですが、その時は適当に遊んでこいつ相変わらず弱いなーなどと楽しんでください。

それと作者さんが参加者の一人だったせいか、期間中に花AI塚にバグが次々と見つかり日々修正されていくというのもなかなかに面白い事態でした。
花AI塚のソースコードが公開されていたりすると、原因行数まで特定してのユーザーによるデバッグも行われてさらに楽しそうですが、ツールの性質上花映塚の対人チートに利用できるため無理なんですよね、残念。

実はAI提出時点では自分はエキシビジョンの存在をすっかり忘れていました。
提出時に主催であるろくしーさんさんに聞かれて思い出したものの、そこから実装を変更するには時間が足りず、そのまま提出することになりました。
結果はエキシビジョンも同点最下位、まぁ当然ですね。
一応最下位のもう一方は被弾時以外C1しか使わない妖夢AIさんであったため、もし最下位決定戦が成立していたら勝てたのかもしれませんが、お互いにエキシビジョン専用処理なしなので所詮どんぐりの背比べですかね。

最後に、主宰のろくしーさん、ツール開発の@ide_anさん、自分以外の参加者さん、とても楽しかったです、ありがとうございました。

2015 年 1 月 28 日

THxxBGMの心綺楼と深秘録体験版対応を追加するアプリリリース

Filed under: 未分類 — 中の人 @ 2:50 AM

THxxBGMに心綺楼と深秘録体験版再生機能を足すアプリを作りました。
http://wordpress.click3.org/garakuta/thxxbgmTh135Patch.zip

使い方は、中のwinmm.dllをTHxxBGM.exeと同じディレクトリに置くだけ。
うまいこと行けばpath設定に心綺楼と深秘録体験版が増えて、該当ディレクトリを設定すれば聞けるようになるはず。

2015 年 1 月 11 日

squirrel逆コンパイラCNutConverterリリース

Filed under: 未分類 — 中の人 @ 10:34 AM

squirrelのコンパイル済みバイナリスクリプトの逆コンパイラを作りました。
http://wordpress.click3.org/garakuta/cnut_converter.zip

touhouSE ver1.18の副産物で、単体でも需要ありそうだったので分離してきたものになります。
とはいえ、逆コンパイラといいつつオレオレ言語に変換されるだけなので、元のソースコードと等価なsquirrelスクリプトを得ようとするなら、自力でガシガシ書き直すしかないわけですが。
まぁバイナリエディタとオペコード表を見つつ唸るよりはよっぽど楽だと思うので、必要な人はご自由にご利用ください。

touhouSE ver1.18リリース

Filed under: 未分類 — 中の人 @ 9:40 AM

東方系dat展開ツールtouhouSE更新しました。
touhouSE

今更ですが心綺楼対応入れました。
それ以外の新規対応は裏でひっそり対応していて、ここに書いてなかっただけです。
運が良ければ一週間以内に深秘録体験版の対応も入る予定。

※追記:
運が良いどころか大凶を引き当てたので深秘録対応版は当分出ない運びとなりました。

2014 年 10 月 25 日

VC++2013でxorが違う値を参照するバグ

Filed under: 解析 — 中の人 @ 9:08 PM

VisualStudio2013のVC++にてReleaseビルドだとxor命令で参照する値が別の値になり、結果期待した動作をしないバグに遭遇した。
原因は不明だが、最小再現コードを作ったので公開しておく。

vs2013_xor_bug.cpp

#include  <cstdio>
#include <cstring>

#include <memory>

const unsigned int KEY_SIZE = 8;
const unsigned int HEADER_SIZE = 16;

void decrypt(const unsigned int size, const unsigned char * const key, unsigned char * const buf) {
  for (unsigned int i = 0, table_index = 0; i < size; i++) {
    buf[i] ^= key[table_index];
    table_index = (table_index + 1) % KEY_SIZE;
  }
}

bool readHeaderAndKey(const unsigned char * const in, const unsigned long long int file_size, const std::shared_ptr<unsigned char> &header, unsigned char * const key) {
  std::memcpy(header.get(), in, HEADER_SIZE);
  const unsigned int file_name_table_address = *reinterpret_cast<const unsigned int *>(&in[12]) ^ 0xF8A42792;
  if (file_name_table_address > file_size) {
    return false;
  }
  const unsigned int footer_size = static_cast<unsigned int>(file_size)-file_name_table_address;
  key[4] = header.get()[4] ^ reinterpret_cast<const unsigned char *>(&footer_size)[0];
  key[5] = header.get()[5] ^ reinterpret_cast<const unsigned char *>(&footer_size)[1];
  key[6] = header.get()[6] ^ reinterpret_cast<const unsigned char *>(&footer_size)[2];
  key[7] = header.get()[7] ^ reinterpret_cast<const unsigned char *>(&footer_size)[3];

  decrypt(HEADER_SIZE, key, header.get());
  return *reinterpret_cast<const unsigned int *>(&header.get()[12]) < file_size;
}

int main(unsigned int argc, const char * const argv[]) {
  const unsigned char data[HEADER_SIZE] = {0x00, 0x00, 0x00, 0x00, 0x80, 0xDF, 0x90, 0x52, 0x00, 0x00, 0x00, 0x00, 0x9E, 0x4F, 0xA4, 0xF8};
  const std::shared_ptr<unsigned char> header(new unsigned char[HEADER_SIZE]);
  unsigned char key[KEY_SIZE];
  readHeaderAndKey(data, 27420, header, key);
  for (const unsigned char c : key) {
    std::printf("%02x ", c);
  }
  readHeaderAndKey(data, sizeof(data), header, key);
  decrypt(0, NULL, NULL);
  return 0;
}

上記のコードを以下のようにコンパイルすると発生。

cl vs2013_xor_bug.cpp /O2 /Oy-

期待した動作としては
00 00 00 00 90 dc 90 52
と出力されてほしいのだが
00 00 00 00 90 cf 90 52
と出力される。
(6番目の出力がずれている)

試しにprintfしているforをstd::printf(“%02x\n”, key[5]);とでも書き換えてみると、コンパイルオプションに関わらず常にdcと表示されるようになるので、明らかにバグ。

23行目から先の逆アセンブラ画面。

選択行の部分がkey[5]へ代入する値を作っている部分、本来XOR AL,BHであるべきで実際そう書き換えると治る。

mainのprintfの後のreadHeaderAndKeyやdecryptは意味がないように見えるが、ないと発生しなくなる。
おそらく一か所でしか実行されない関数は専用の最適化が走り、それによりバグが発生する最適化の条件から外れるのだと思われる。

headerがstd::shared_ptrなのは、元はちゃんとしたクラスだったのだがshared_ptrでくるんであれば何でもいいようだったのでunsigned charにしてある。

他にもreadHeaderAndKeyの無意味そうなifや返り値も外すと発生しなくなるし、key[5]以外のxorを外したりintにまとめて一回で済ませようとしても発生しなくなる。

Visual Studio 14 CTPでも同様に発生するが、Visual Studio 2012では発生しないことを確認している。

2014 年 6 月 8 日

PrivateNamespaceを使ったAppContainerとのプロセス間通信

Filed under: 未分類 — 中の人 @ 4:17 AM

以前の記事でAppContainerの内外で1対1プロセス間通信する方法を紹介しました。
しかし、指定したAppContainerに紐づく名前空間を使用する関係上、不特定多数のAppContainerアプリからプロセス間通信を受ける場合などには使用できませんでした。
またそれ以外にも、AppContainerのプロファイルを作った後でなくてはいけなかったり、AppContainerのプロファイル名を知らなければならなかったりと通常のプロセス間通信よりも制限が多いという難点もありました。
そこで今回はPrivateNamespaceを使用して、これらの問題を解決してみようと思います。

いつものようにサンプルコード
http://wordpress.click3.org/garakuta/TestPrivateSharedMemory.zip

PrivateNamespace空間とは

Windowsのハンドルはそれぞれ所属する名前空間があります。
これらは通常WinObjなどを使えば見ることが可能ですが、実は例外としてPrivateNamespaceというものが存在します。

PrivateNamespaceは例外的に誰でも(AppContainerも含む)アクセスできる場所に配置されており、通常の名前空間のように相対パスや絶対パスなどを気にする必要がありません。
そのため、AppContainerへのアクセスを許可しただけではAppContainer側からそこへたどり着く方法がないという従来のハンドル制限を無視することができます。
とはいえ個々のPrivateNamespaceにはアクセスコントロールが効いているので、AppContainerから利用する場合はWinBuiltinAnyPackageSidなどを許可する必要はあります。

実際にやってみよう

サンプルコードのExampleApp.exeをTestPrivateSharedMemory.exeにD&Dすると黒窓が二つ上がり、ExampleAppのほうに数字を打ち込むともう片方にも反映されます。
その状態でさらにExampleApp.exeをダブルクリックして起動し、そちらに数字を打ってもちゃんと反映されます。
D&Dで起動したものはAppContainerで、通常起動は普通のデスクトップアプリとして動作しています。

次は具体的な実装方法を説明します。

コード説明

具体的な動作としては、PrivateNamespaceを作成もしくはオープンし、共有メモリーを開く際にそのPrivateNamespaceをオブジェクト名の前につけて作るだけです。
では実際のコードを見ていきましょう。

まずはPrivateNamespaceの作成から

bool CreatePrivateNamespace(SHARED_HANDLE &privateNamespace, const boost::shared_ptr<SECURITY_DESCRIPTOR> desc, const wchar_t * const boundaryName, const wchar_t * const name) {
  SECURITY_ATTRIBUTES attr;
  attr.nLength = sizeof(SECURITY_ATTRIBUTES);
  attr.bInheritHandle = FALSE;
  attr.lpSecurityDescriptor = desc.get();
  std::vector<SHARED_SID> sidList;
  sidList.push_back(GetWellKnownSid(WinBuiltinAnyPackageSid));
  HANDLE boundaryImpl = ::CreateBoundaryDescriptorW(boundaryName, 0);
  if (boundaryImpl == NULL) {
    return false;
  }
  BOOST_FOREACH(const SHARED_SID sid, sidList) {
    if (::AddSIDToBoundaryDescriptor(&boundaryImpl, sid.get()) == FALSE) {
      ::DeleteBoundaryDescriptor(boundaryImpl);
      return false;
    }
  }
  const SHARED_HANDLE boundary(boundaryImpl, &MyDeleteBoundaryDescriptor);
  privateNamespace.reset(::CreatePrivateNamespaceW(&attr, boundary.get(), name), &MyClosePrivateNamespace);
  if (!privateNamespace) {
    return false;
  }
  return true;
}

SECURITY_ATTRIBUTESとBoundaryDescriptorを構築、それをもとにCreatePrivateNamespaceWを呼び出しています。
nameが実際に共有メモリーなどを作成する際につける名前空間の名前で、boundaryNameは別プロセスから同じPrivateNamespaceを作る際に使用する識別子になります。
boundaryNameさえあっていれば同じ名前空間になるので、OpenPrivateNamespaceでnameを変えて実行すれば別名だが同じものとして動作するハンドルを取得できます。
また、AddSIDToBoundaryDescriptorでアクセスコントロールを行っており、WinBuiltinAnyPackageSidを指定することでAppContainerからのアクセスを許可しています。

次に呼び出し側

  SHARED_HANDLE privateNamespace;
  if (!CreatePrivateNamespace(privateNamespace, desc, L"boundaryName", L"privateNamespaceName")) {
    std::wcout << L"Error: PrivateNamespaceの生成に失敗しました" << std::endl;
    std::wcout << ::GetLastError() << std::endl;
    std::wcin.ignore();
    return 1;
  }
  const wchar_t * const sharedMemoryName = L"privateNamespaceName\\AppConteinerTestMemory";
  HANDLE handle;
  if (!CreateSharedMemory(handle, sharedMemoryName, sizeof(SharedStruct), desc)) {
    std::wcout << L"Error: 共有メモリーの生成に失敗しました" << std::endl;
    std::wcout << ::GetLastError() << std::endl;
    std::wcin.ignore();
    return 1;
  }

特に説明することはありません、「#{PrivateNamespaceの名前空間名}\#{オブジェクトの名前}」で共有メモリーを作成しているだけです。

その他AppContainerでのexeの起動などは以前解説したことがあるので省き、次はExampleAppのPrivateNamespaceのオープンから。

bool OpenPrivateNamespace(SHARED_HANDLE &privateNamespace, const wchar_t * const boundaryName, const wchar_t * const name) {
  std::vector<SHARED_SID> sidList;
  sidList.push_back(GetWellKnownSid(WinBuiltinAnyPackageSid));
  HANDLE boundaryImpl = ::CreateBoundaryDescriptorW(boundaryName, 0);
  if (boundaryImpl == NULL) {
    return false;
  }
  BOOST_FOREACH(const SHARED_SID sid, sidList) {
    if (::AddSIDToBoundaryDescriptor(&boundaryImpl, sid.get()) == FALSE) {
      ::DeleteBoundaryDescriptor(boundaryImpl);
      return false;
    }
  }
  const SHARED_HANDLE boundary(boundaryImpl, &MyDeleteBoundaryDescriptor);
  privateNamespace.reset(::OpenPrivateNamespaceW(boundary.get(), name), &MyClosePrivateNamespace);
  if (!privateNamespace) {
    return false;
  }
  return true;
}

OpenPrivateNamespaceWで先ほど別プロセスで作成したPrivateNamespaceをオープンしています。
この時boundaryの内容が作成側と異なると失敗するので注意が必要です。
また、前述のようにnameに関しては作成時と同一にする必要はありません。

次はこちらも呼び出し側

  SHARED_HANDLE privateNamespace;
  if (!OpenPrivateNamespace(privateNamespace, L"boundaryName", L"privateNamespaceName")) {
    std::wcout << L"Error: 名前空間のオープンに失敗しました" << std::endl;
    std::wcout << ::GetLastError() << std::endl;
    std::wcin.ignore();
    return 1;
  }
  void * ptr;
  const wchar_t * const sharedMemoryName = L"privateNamespaceName\\AppConteinerTestMemory";
  if (!CreateSharedMemory(ptr, sharedMemoryName, sizeof(SharedStruct))) {
    std::wcout << L"Error: 共有メモリーの生成に失敗しました" << std::endl;
    std::wcout << ::GetLastError() << std::endl;
    std::wcin.ignore();
    return 1;
  }

こちらも特に説明することはなく、オープンしたPrivateNamespaceで共有メモリーを作成しているだけです。

その他実際にプロセス間通信を行っているところなどはPrivateNamespaceとは関係がなく、ググればすぐにわかるので割愛します。

終わりに

これでAppContainerを含む複数のプロセスで1対多や多対多のプロセス間通信が行えるようになりました。
また今回は共有メモリーで行いましたが、Mutexやパイプなどその他のオブジェクトに関しても同様の手順で使用することができます。
あまり需要はないと思いますが、参考にしてくれる人がいれば幸いです。

ではまた。

2014 年 5 月 24 日

touhouSE ver1.15リリース

Filed under: 未分類 — 中の人 @ 10:12 PM

東方系dat展開ツールtouhouSE更新しました。
touhouSE

今回は弾幕アマノジャク対応。
とはいえ、新規フォーマット追加ではなくフォーマット誤認による展開失敗修正と、msgファイルの新規追加機能対応だけです。

2014 年 5 月 18 日

GetProcessMemory

Filed under: 未分類 — 中の人 @ 7:47 PM

指定プロセスを監視しメモリー使用量の推移を記録するツール。
http://wordpress.click3.org/garakuta/GetProcessMemory.zip

単に指定exeのメモリー使用量の推移をcsvに書きだすだけのツールです。

mrubyのバイトコードフォーマット解説その2

Filed under: 未分類 — 中の人 @ 12:57 AM

2014/05/14、mrubyでローカル変数情報を扱うようになりlocal_variablesメソッドも使用可能に。
それに伴いバイトコードのフォーマットも変更となり、新たにLVARセクションが追加されました。

そこで今回は追加されたLVARセクションについて、簡単にフォーマット紹介をしようと思います。

過去の記事の続きなので、まだの人はそちらからどうぞ。

いつものようにサンプルコード
http://wordpress.click3.org/garakuta/parse_mruby3.zip
内容は過去の記事の物をベースにローカル変数情報を追加してあります。
動作としては、parse_mruby.exeに.mrbファイルをD&Dするとローカル変数情報とirep情報を表示する、というものです。

この記事で対象としているmrubyは2014/05/17当時(git hash: 13db4da204d2bedec4c0c5de939e662a44d477a6 )の物です。
それ以降のmrubyを使用する場合は以下の解説の通りではない可能性があります。

セクション

ローカル変数は新設されたセクションに含まれています。
過去の記事でも書いていますがセクションには共通のセクションヘッダーと呼ぶべきものを含んでおり、それは以下の通りになります。

ubig8_t signature[4];
ubig32_t size;

LVARセクション

ローカル変数名を保持するセクションで、signatureは0x4C 0x56 0x41 0x52(ASCIIでLVAR)になります。
LVARセクション専用の追加ヘッダーは特に存在しません。

LVARセクションは一つのmrbファイル一つにつき一つのみ存在し、複数のメソッドが含まれている場合はそれらすべてのローカル変数情報を持ちます。
LVARセクションはセクションヘッダーを除くと二つの領域からなりローカル変数名を表すシンボルテーブルと、各レジスタとシンボルを紐づけるレコードテーブルに分けられます。

シンボルテーブル

ローカル変数名のシンボルを格納した領域。
含まれるirepすべてのローカル変数名情報のリストであり、重複も排除されている。
構造は以下の構造の通りになる。

ubig32_t count;
struct symbol {
  ubig16_t len;
  ubig8_t body[len];
} symbolList[count];

サンプル内で実際に読み込んでいるコードは以下の通り。

  static boost::shared_ptr<const LocalVariableSymbolList> Read(const uint8_t *ptr, unsigned int size) {
    boost::shared_ptr<const LocalVariableSymbolList> result;
    if (size < 4) {
      return result;
    }
    const unsigned int count = *reinterpret_cast<const endian::ubig32_t *>(ptr);
    ptr += 4;
    size -= 4;
    if (count * 1 > size) {
      return result;
    }
    std::vector<std::string> list(count);
    BOOST_FOREACH(std::string &str, list) {
      if (size < 2) {
        return result;
      }
      const unsigned int len = *reinterpret_cast<const endian::ubig16_t *>(ptr);
      ptr += 2;
      size -= 2;
      if (len > size) {
        return result;
      }
      str.assign(reinterpret_cast<const char *>(ptr), len);
      ptr += len;
      size -= len;
    }
    result.reset(new LocalVariableSymbolList(list));
    return result;
  }

最初にシンボルの個数を取得し、その個数分だけループをまわして文字列長を取得した後その長さだけ文字列として読み込み、最後にそれを保持するインスタンスを作成して返しているだけです。
irepのシンボルテーブルと違い終端文字列は含まない点に注意。
また現在の版だとブロック引数に(たとえ使用していなくても)常に空文字列がローカル変数名として割り当てられており、それがこのシンボルテーブルにも含まれていることがあります。

レコードテーブル

ローカル変数シンボルと実際にローカル変数が保持されているレジスタを紐づけるテーブル。
irepデータの数だけ存在し、深さ優先探索的にならんでいる。
構造は以下の通り

ubig16_t symbolIndex;
ubig16_t registerIndex;

symbolIndexは前述のシンボルテーブル内のインデックス、registerIndexはirep内の該当するローカル変数に紐づくレジスタのインデックスです。
サンプルで実際に読み上げている処理は以下の通り。

  static boost::shared_ptr<const LocalVariableData> Read(const uint8_t *ptr, unsigned int size, const boost::shared_ptr<const IrepRecord> irep, const boost::shared_ptr<const LocalVariableSymbolList> symbolList) {
    boost::shared_ptr<const LocalVariableData> result;
    if (static_cast<unsigned int>(2 + 2) * (irep->header->localCount - 1) > size) {
      return result;
    }
    std::vector<Record> records(irep->header->localCount - 1);
    BOOST_FOREACH(Record &record, records) {
      const unsigned int nameIndex = *reinterpret_cast<const endian::ubig16_t *>(ptr);
      if (nameIndex > symbolList->list.size()) {
        return result;
      }
      ptr += 2;
      size -= 2;
      record.name = &symbolList->list[nameIndex];
      record.registryIndex = *reinterpret_cast<const endian::ubig16_t *>(ptr);
      ptr += 2;
      size -= 2;
    }
    std::vector<boost::shared_ptr<const LocalVariableData> > childs(irep->child.size());
    for (unsigned int i = 0; i < childs.size(); i++) {
      const boost::shared_ptr<const LocalVariableData> child = Read(ptr, size, irep->child[i], symbolList);
      if (!child) {
        return result;
      }
      childs[i] = child;
      ptr += child->GetSize();
      size -= child->GetSize();
    }
    result.reset(new LocalVariableData(records, childs));
    return result;
  }

最初に該当するirepのローカル変数の数を取得している。
-1しているのは自信を指すselfの分は含まれないためだ。

次はローカル変数の数だけレコードを読み込み、シンボルとの関連付けを解決している。

その後子のirepに対して再帰実行して読み込んだのち、データを保持するインスタンスを作成して返している。

irepとの紐づけ

irepとの紐づけといってもレコードテーブルの時点で終わっているも同然です。
すでにirepごとに変数の紐付けは終わっており、あとはirepでの処理の際に該当するレコードからシンボルを引っ張ってくるだけです。

一応、サンプルにおけるローカル変数情報の表示部分を紹介します。

bool PrintLocalVariable(const boost::shared_ptr<const LocalVariableData> data, std::vector<unsigned int> &index) {
  std::cout << boost::format("lvar%s\n") % std::accumulate(index.begin(), index.end(), std::string(), IndexListJoin());
  BOOST_FOREACH(const LocalVariableData::Record &record, data->records) {
    std::cout << boost::format("reg[%d] : %s") % record.registryIndex % *record.name << std::endl;
  }
  std::wcout << L"\n";
  unsigned int i = 0;
  BOOST_FOREACH(const boost::shared_ptr<const LocalVariableData> &child, data->childs) {
    index.push_back(i);
    if (!PrintLocalVariable(child, index)) {
      return false;
    }
    index.pop_back();
    i++;
  }
  return true;
}

indexは階層表示用の配列でlvar->child[3]->child[2]->child[1]のirepのローカル変数情報を表示する際には[3, 2, 1]と保持しており、それを文字列に変換して使用しています。
あとは単にforeachで配列を展開して表示し、childがいれば再帰して呼び出しているだけです。

終わりに

いかがだったでしょうか。
新しくセクションが増えるというそれなりに大きな変更ですが、実はバージョン番号などには変化がありません。
Matzさん曰く「sectionが追加されただけで、かつ知らないsectionは読み飛ばす(はず)ですから、変更しなくても大丈夫だと思いました。」とのことで、過去の版で動作する限りは特にバージョンは上がらないようです。

今回の更新でローカル変数をevalで取得するようなコードも動くようになったので、また一歩rubyとの互換性が上がったと言えます。
この記事を読んで少しでもmrubyの動作について理解が深まれば幸いです。
では、また。

Older Posts »

Powered by WordPress