再びEA、再びNFS、再びバグ。 補修

こんにちは、Habr! 再びNFSコミュニティのスピードを上げています。 また、古いおもちゃであるNFS Most Wantedを修復しています。 以前の 記事ですでにバグの修正について話しましたが、今日は解体のジャングルに少し深く入り込みたいと思いました。 猫の下で興味を持ってください。







背景



昔々 、EAが優れたNFSを公開したとき、最も有名なレースゲームの1つ-Most Wanted-がリリースされました。 悲しいかな、それは販売されたほどには書かれておらず、定期的に落ちました。 もちろん、普通の人はこれにほとんど注意を払っていません-さて、通過のために一度飛びました、それは大丈夫です。 しかし、これは私たちにとって非常に大きな問題を引き起こします。明確な症状なしに、偶発的な転倒によってどれほどの潜在的な記録が殺されたかです。 それはすべて、KuruHSが個人的に物事を整理するように頼むことで終わりました。 拒否できませんでした。



何がありますか







IDA-分解用

チートエンジン-メモリと命令の編集用

Visual Studio-デバッグ用(トレースポイント、非常に便利なことが判明)



たくさんのダンプがあります。 10ギガバイトというまともな束。それらから始めます-ゲームがどの命令に該当するかを分析します。 そして、いくつかのパターンを追跡することはできますが、かなりランダムに落ちます。 問題解決中に、ゲームを時々クラッシュさせる危険な場所をいくつか見つけました。 例:







文字列ハッシュ計算関数。 どうやら、開発者はこの場所にヌルポインターを取得することを期待していなかったため、チェックを追加しませんでした。 このため、まれにゲームがクラッシュしました。 修正はかなり平凡です-実行可能ファイルの最初の空の部分にジャンプして、edi、ediをテストします。 次に、最初にジャンプした場所からjz retunとjmpを実行しました。







別の同様のケースが次の手順で見つかりました

00057D105 mov edx, [ecx] ; ,







開発者は再びヌルポインターを取得することを期待していなかったため、ゲームがクラッシュしました。 修正は、前のものとまったく同じです。







落下の最も一般的な原因は、AllocateMemory関数にありました。 それを分解しようとすると、ゲームのクラッシュの問題に取り組んだすべての人が恐怖に陥りました。 ゲームには少なくとも5つの異なるメモリ管理サブシステムがあるという事実にすでに注意が払われています。 私が得たもの...







まあ、泣き言を言う時間はありません。逆にする必要があります。 このゴミを解析してから数晩後には報われました。コードはまだ読めませんが、より理解しやすくなりました。 どうやら、このサブシステムは標準的なスキームに従って動作します。一定量のメモリを一度に取得してブロックに分割し、二重にリンクされたリストに保存します。 リクエストに応じて無料の領域を提供し、存在しない場合はシステムからさらに多くの領域を取得しようとします。 ああ、2005年、メモリ操作がランダムに散らばるほど高価だった...







私の脳はそれらを処理しようとすることさえ完全に拒否するため、この機能のいくつかの場所は私に頭痛を引き起こします。 しかし、私には明らかなことが1つあります。リンクされたリストで構成されるこれらのリンクされたリストのどこかに、間違ったポインターがあります。 私に起こった唯一の解決策は、「use_best_fit」チェックを無効にして、サブシステムが最も適切なブロックを探すのではなく、最初に使用可能な空きブロックを返すようにすることでした。



もちろん、これは問題を完全には解決しませんでしたが、少なくともゲームは本当に安定しました-この特定の場所でのテストの週には数回しか落ちませんでした(KuruHSはゲームに1日10時間を費やしていると考えてください) 。



純粋な仮想関数呼び出し。



ヘッダーに示されているのと同じエラー。 C ++に精通している人々は、問題が何であるかをすぐに理解するでしょう。 ただし、ソースコードがないと、事態はさらに複雑になります。 CRTは状況を複雑にします。CRTは、パルチザンとして、このタイプのエラーをキャッチした場合に頑固にダンプを生成することを望みません。



Purecallは、コードが「純粋な仮想関数」(実装のない仮想クラス関数)を呼び出そうとしたことを意味します。 疑いもなく、彼はこれを行うことに成功していないので、彼がすることに決めた唯一のことは、これをユーザーに通知し、コード0で終わることです。 結果として、すべてがコードでうまくいくように見えますが、実際には、すべてが悪いです。



すばらしい機能である_set_purecall_handlerに感謝します。これにより、purecallハンドラーを置き換えることができます。 ファイル内の参照/リンクを探しています。関数自体を見つけます。 これで、ハンドラーを記述することができます。ハンドラーとしてインストールすることを忘れないでください。 これを行うには、ファイル自体に未使用コードの十分な大きさの部分を見つける必要があります。これをコードに書き換えることができます。 短い検索で、それが_CxxThrowException関数であることが示されました(リンクが見つかりませんでした)。 私たちは容赦なく彼女の全身を鼻で記録し、彼女の上に作成を開始します。







これは、新しいプロシージャの擬似コードの外観です。



 new_handler: xor eax, eax ; return *(0); mov eax, [eax] ;    ret set_handler: push new_handler call _set_purecall_handler ; _set_purecall_handler(new_handler); add esp, 4 ; cdecl,   ret
      
      





コンパイルし(私の場合、手でCheat Engineに送ります)、コードに貼り付けます:







次に、このプロシージャを呼び出す適切な場所を見つける必要があります。 適切なものは見つかりませんでしたが、ゲームのメインループですぐに1つの素晴らしい空の関数を見つけたので、その呼び出しは、作成した関数の呼び出しのサブメニューです。 パッチを作成し、テストできます。







唯一の問題は、このエラーが非常にまれであり、何時間も目的のないプレイをしたくないということです。 それにもかかわらず、私はそれを自分でテストすることに決め、嬉しく驚きました-ゲームは10分間のゲームプレイ後に文字通り落ち、今書いたサイトに落ちました。 呼び出しスタックに沿ってもう少し上に移動します。



 0043E005 call dword ptr [edx+80h]
      
      





「はい、これは仮想関数の呼び出しです」以外は何も言えません。 最初の考えは-それがなければどうなるでしょうか? 私たちはnopで切り取り、テストします-私たちが生きているように。 ゲームは正常に機能します。 副作用はありません。 パッチを収集し、テストのために送信します。 1日後、同じ手順が数バイト下にあるダンプが到着します。 彼女も見ました-ゲームは落ち始めます。 すべてが、より深刻な解決策について考える必要があるという事実につながります。 しかし、私の頭には何も入らないので、無期限に延期されます。



夜の間に私はそれを熟考する時間があり、結論に達しました。 あなたは、C ++は実行時にオブジェクトのタイプを決定する方法を知らないと言いますか? そして、私はそれができると言います。 そして非常にシンプル-メモリ内の仮想テーブルのアドレスで。 ダンプを調べた後、間違ったクラスが定期的にプロシージャ(vtbl @ 0x00890970)に飛び込むという結論に達しました。つまり、この状況をキャッチできるということです。



 cmp edx, 00890970h jnz good_class xor eax, eax jmp return good_class: call dword ptr[edx+80h] jmp continue
      
      





ただし、1つ問題があります。これは多くのスペースを占有するため、これをプロシージャに組み込む必要があります。 十分なスペースを見つけることはできません。関数の前に数バイトの空の断片がいくつかあります。 それらの多くがあり、それらが近いという事実を既にありがとう。 したがって、私たちはスパゲッティを書き、ほぼ各命令の後にある場所から別の場所にジャンプします。







歌詞
たぶん私は少し興奮しましたが、これを一度関数_CxxThrowExceptionにプッシュする価値がありました。 しかし、悲しいかな、彼は彼がしたようにした。 先日、この修正をリメイクしようと思います。



パッチを適用して実行します。 また、同じ問題が発生します。このクラッシュは非常にまれであり、テストのほぼ4時間でこのコードが数回実行され、正しいクラスが常に受信されました。



そのままにしておくこともできましたが、これが本当に機能することを確認する必要がありました。 したがって、我々はさらに逆行し、私たちの手で例外的な状況を引き起こそうとします。



簡単な検査では、引数の1つがゼロ以外の場合、ゲームが落ちる可能性があることが示されました。 プロシージャ自体は2か所でのみ呼び出され、1つのケースでは、同じ引数を0に設定して呼び出されます。したがって、別の関数を見ています。







「余分な」チェックを最大限削除し、この関数を強制的に呼び出します。 テストを開始し、最終的に間違った入力クラスを取得します。 スタジオデバッガーがテキストを完了し、ゲームがハングし、...動作し続けるまで待機します。 やった!





スクリーンショットは、ストリームからの録音用にせっけんです



おわりに



解決策が見つかりました-何かが間違っていてもゲームはクラッシュしなくなりました。 これは上記のスクリーンショットで顕著です-フェンスの一部が欠落しています。ゲームが何かを間違えようとしたためです。 暗闇に覆われた謎とは正確に何なのかはわかりませんが、遅かれ早かれ私たちはそれを発見するでしょう。



一般に、状況は著しく改善されました。KuruHSは、1回のドロップなしでゲームで約20時間を完全に費やすことができました。



ThirteenAGのワイドスクリーンパッチの原則に従って、修正全体をasiスクリプトの形式で修正することにしました。 githubでソースを読み、スクリプトをダウンロードできます。



ご清聴ありがとうございました!



All Articles