「コサック」のリバースエンジニアリング、パート2:キューを増やす





ほとんどの場合、「ターン」という言葉は、特に「増加」という言葉との組み合わせでは、ポジティブな感情を引き起こしません。 しかし、ゲームの開始時に数百万単位のリソースでプレイしたい場合は、10分以内に数千人の兵士を戦闘に投入できますが、Shiftキーを使用した標準の5単位のユニットでは不十分です。 今、あなたがそれぞれ20または50人の兵士を注文することができた場合、またはさらに良い場合は、いくつかの異なる修飾キーを持っています...



エントリー



前の記事の出版とLCNコミュニティ側の関心の後に、軍事ユニットの注文のキューを5から20または50に増やすことができるかどうかを尋ねられました。「なぜだ」と思ったのです。 1バイトを0x05から0x14に置き換えるだけでよく、それだけです。」



それがどうなるかわかっていたら...しかし、私は知りませんでしたので、行きましょう!



どこから始めますか?



当然、マシンコードの目的のセクションを検索します。 選択肢があります。マウスクリックが処理される場所を探し、軍隊の注文を担当する支部までコードを追跡してみてください。 または、Shiftキーの状態がチェックされるすべての場所をバイパスできます。 2番目の選択肢は、より有望なように思えました。 私たちはそのようなことのためにみんなのお気に入りのプログラムを開始し、インポートされた関数のリストを調べます。



うーん、GetKeyStateは有望に聞こえます。 相互参照には何がありますか? 118件の電話? 多すぎる場合は、Shiftキーがチェックされていない呼び出しを除外する必要があります。 GetKeyState関数は、Shiftの場合は0x10であるキーコードという1つのパラメーターのみを受け入れます。 私のdmcr.exeでは、これは次のマシンコードに対応しています。



push 10h 6A 10 call GetKeyState FF 15 EC C1 5C 00
      
      





このバイトシーケンスを検索すると、GetKeyState(VK_SHIFT)が呼び出された38個のアドレスが返されました。 それぞれにブレークポイントを設定し、デバッガーを起動して、目的の手順に到達するまで余分なものを削除します。 正確には、2つの手順があります。1つは戦闘ユニットの注文用、もう1つはキャンセル用です。 しかし、それらは呼び出された関数のアドレスのみが異なるため、それらを1つの手順と見なします。



ここに私たちを待っているものがあります:







もちろんです。 コンパイラーは、少数ではあるが一定の実行回数の小さなループを検出した場合、何をしますか? 右、それを身体サイクル指示の繰り返しシーケンスに展開します。 簡単に波打つシングルバイトのパッチを期待しています。



アセンブラーで1つまたはループにパッチを適用する



サイクルには、カウンター、インクリメント、比較、条件付きジャンプが必要です。 呼び出し命令の新しいアドレスに従って関数呼び出しのオフセットを編集する以外は、ループの本体は変更されません。



ドキュメント を学習するため のマニュアル 少し吸った後、マシンコードの次のスケッチが作成されました。



 ;     push cx 66 51 ;   xor cx, cx 66 31 C9 ;   ;  -     push cx 66 51 ;  ,      mov dx, word ptr [ebp+arg_0] 66 8B 55 F0 push edx 52 xor eax, eax 33 C0 mov al, byte_10FC290 A0 90 C2 0F 01 xor eax, 85h 35 85 00 00 00 push eax 50 call sub_4FD01E E8 .. .. .. .. add esp, 8 83 C4 08 ; ,    - pop cx 66 59 inc cx 66 41 cmp cx, 14h 66 83 F9 14 ;    ,    20 jl 7C DA ;      pop cx 66 59
      
      





cxと66hに関する小さな余談
cxレジスタは「ループレジスタ」と見なされ、ループステートメントで使用されます。 ループの代わりにinc、cmp、jlの通常の組み合わせを使用することにしましたが、cxをレジスタカウンタとして残しました。 しかし、機械語命令を選択する際に問題が発生しました。その結果、ecxレジスタを使用した操作は、弟の代わりに常に発生しました。 オンラインアセンブラーの助けを借りなければなりませんでし 。 私のスケッチに反応して、彼がほとんど同じ操作コードを与えたが、プレフィックス0x66を付けたとき、私は驚きました。 ドキュメントでは、オペレーティングコード66hを「Operand-size override prefix。」と説明しています。 予約されており、予測できない動作を引き起こす可能性があります。 このような説明で、彼が以前に私の目に留まらなかったのは驚くことではありません。 0x66プレフィックスは、32ビットレジスタで動作するマシンコードを16ビットの対応するものに強制的に切り替えます。



このパッチは望ましい結果をもたらしますが、1つの大きな欠点があります。ゲームロジックに干渉することなく、Shiftキーを使用して作成されたプロダクションキューのサイズをオーバーライドできますが、それ以上のことはできません。 ゲームの条件によっては、各ゲームの前にキューにパッチを適用することはあまり魅力的ではないため、Shift、Alt、および〜キーで異なるキュー修飾子を使用したいという要望がコミュニティですぐに表明されました。 さて、電話は受け入れられました!



パッチ2または「最大プログラム」



5つの繰り返しブロックのシーケンスを1サイクルに置き換えて、適切な量のバイトを解放しました。 しかし、いくつかのキーのチェックを統合し、結果のスペースの結果に応じてサイクルを調整する方法は? 私の意見では、最も単純な解決策は、ループ内のカウンターが比較される対応する値のレジスターへの割り当てを交互に行うGetKeyStateの順次呼び出しです。 キーが押されていないことがGetKeyStateの呼び出しで示された場合、割り当てステートメントはジャンプします。 したがって、フォークの代わりに、キーの状態に応じて、一連の連続したチェックと割り当てがあり、1サイクルで終了します。



 ;   ,      ,     mov ebx, 01h BB 01 00 00 00 ;   push 10h 6A 10 call GetKeyState FF 15 EC C1 5C 00 movsx ecx, ax 0F BF C8 and ecx, 8000h 81 E1 00 80 00 00 test ecx, ecx 85 C9 ;      mov,      jz 74 05 mov ebx, 05h BB 05 00 00 00 ;   push 12h 6A 12 call GetKeyState FF 15 EC C1 5C 00 [...]
      
      





今回、ebxレジスタを使用してループ実行回数を保存し、esiレジスタをループカウンタとして使用することにしました。 これには2つの理由があります。 関数呼び出し規約に従ってこれらのレジスタは「定数」です。 関数の本体で変更が行われた場合、関数は値をスタックに保存し、完了する前に復元する必要があります。 これにより、各ループを実行する前に自分でプッシュしたりポップしたりする必要がなくなります。 2番目の理由は、cxレジスタとは異なり、0x66プレフィックスが不要になったことです。これにより、mov以外のレジスタを使用する各操作で1バイトが節約されます。



その結果、Shift、Alt、TAB、F1、およびF2修飾キーがあります。 〜キーは、たとえばVK_OEM_3とVK_OEM_5のように、異なるレイアウト上の異なる識別子に対応するため、破棄する必要がありました。



最終パッチコード
 ;     push ebx 53 ;   ,      ,     mov ebx, 01h BB 01 00 00 00 ; Shift: 5   push 10h 6A 10 call GetKeyState FF 15 EC C1 5C 00 movsx ecx, ax 0F BF C8 and ecx, 8000h 81 E1 00 80 00 00 test ecx, ecx 85 C9 jz 74 05 mov ebx, 05h BB 05 00 00 00 ; Alt: 20   push 12h 6A 12 call GetKeyState FF 15 EC C1 5C 00 movsx ecx, ax 0F BF C8 and ecx, 8000h 81 E1 00 80 00 00 test ecx, ecx 85 C9 jz 74 05 mov ebx, 14h BB 14 00 00 00 ; TAB: 50   push 09h 6A 09 call GetKeyState FF 15 EC C1 5C 00 movsx ecx, ax 0F BF C8 and ecx, 8000h 81 E1 00 80 00 00 test ecx, ecx 85 C9 jz 74 05 mov ebx, 32h BB 32 00 00 00 ; F1: 15   push 70h 6A 70 call GetKeyState FF 15 EC C1 5C 00 movsx ecx, ax 0F BF C8 and ecx, 8000h 81 E1 00 80 00 00 test ecx, ecx 85 C9 jz 74 05 mov ebx, 0Fh BB 0F 00 00 00 ; F2: 36   push 71h 6A 71 call GetKeyState FF 15 EC C1 5C 00 movsx ecx, ax 0F BF C8 and ecx, 8000h 81 E1 00 80 00 00 test ecx, ecx 85 C9 jz 74 05 mov ebx, 24h BB 24 00 00 00 ;    -  push esi 56 xor esi, esi 31 F6 ;   mov dx, word ptr [ebp+arg_0] 66 8B 55 F0 push edx 52 xor eax, eax 33 C0 mov al, byte_10FC290 A0 90 C2 0F 01 xor eax, 85h 35 85 00 00 00 push eax 50 call sub_4FD01E E8 .. .. .. .. add esp, 8 83 C4 08 ; , ,     inc esi 46 cmp esi, ebx 39 DE jl 7C E1 ;   pop esi 5E pop ebx 5B
      
      





あとがき



この場所で、タスクが完了したと言うことができ、手を洗いに行きます。 または、プレーヤーが各修飾キー自体のキューサイズを設定できるようにするミニチュアパッチャーを作成することもできます...しかし、それについては次の記事で詳しく説明します。



参照資料



All Articles