GTAオンラインの読み込み時間を70%短縮する方法

GTAオンライン。読み込み遅いことで有名なマルチプレイヤーゲーム 私は最近、いくつかの強盗を完了するために戻ってきました-そして、それが7年前にリリースされた日と同じくらいゆっくりとロードされることにショックを受けました。



それの底に到達する時が来ました。



インテリジェンスサービス



まず、誰かがすでに問題を解決しているかどうかを確認したかった。しかし、私はゲームの非常に複雑なストーリー、ロードに非常に時間がかかる理由、ネットワークp2pアーキテクチャ がゴミであるストーリー (そうではないが)、ストーリーモードロードしてからにロードするいくつかの複雑な方法についてのストーリーしか見つけ ませんでした 単一のセッション、および起動時にR *ロゴビデオをスキップするためのさらにいくつかのmod。フォーラムをもう少し読んだ後、これらすべての方法を一緒に使用すると、なんと10〜30秒節約できることがわかりました。



その間、私のコンピューターでは...



基準



シーンの読み込み:〜1分10秒
オンラインローディング:〜6m
R *ロゴからゲームプレイまでのブートメニューはありません(ソーシャルクラブへのログインはありません)。

古いがまともなパーセント:AMD FX-8350
安価なSSD:KINGSTON SA400S37120G
RAMを購入する必要があります:2x Kingston 8192 MB(DDR3-1337)99U5471
通常のGPU:NVIDIA GeForce GTX 1070


ハードウェアが古くなっていることはわかっていますが、オンライン時にダウンロードが6倍遅くなる可能性があるのはなぜですか。他の人が行ったように、ストーリーモードからオンラインにロードするときの違いを測定できませんでし 動作しても差は小さいです。



私は一人じゃない



この調査よると、 この問題は広範囲に及んでおり、80%以上のプレイヤーにとってやや煩わしいものです。 7年になります!







私は、3分未満でロードする約20%の幸運なものに関する情報を少し検索し 、トップゲーミングPCと約2分のオンラインロード時間でいくつかのベンチマーク見つけました 。私はそのようなコンピューターのためにハッキングされた誰か殺したでしょう !それは本当にハードウェアの問題のように見えますが、何かが足し合わない...



ストーリーモードの読み込みにまだ約1分かかるのはなぜですか?(ちなみに、M.2 NVMeから起動する場合、ロゴ付きのビデオは考慮されませんでした)。さらに、ストーリーモードからオンラインでダウンロードするのに1分しかかかりませんが、私には5つほどあります。私は彼らのハードウェアがはるかに優れていることを知っていますが、5倍ではありません。



高精度測定



タスクマネージャーのような強力なツールを 装備して、私はボトルネックを見つけることに着手しました。







ストーリーモードとオンラインの両方に必要な共有リソースをロードするのにほぼ1分かかり(ほぼトップエンドPCと同等)、GTAは1つのCPUコアを4分間完全にロードし、他には何もしません。



ディスクの使用状況?いいえ!ネットワークの使用?少しありますが、数秒後には主にゼロになります(回転する情報バナーの読み込みを除く)。GPUの使用?ゼロ。記憶?何もありません...



それは、ビットコインマイニングか何かですか?ここでコードの匂いがします。非常に悪いコード。



シングルストリーム



私の古いAMDプロセッサには8つのコアがあり、それでも素晴らしいですが、古いモデルです。AMDのシングルスレッドのパフォーマンスがIntelのパフォーマンスよりもはるかに低かったときに元に戻されました。これがおそらく、このようなロード時間の違いの主な理由です。



奇妙なのは、CPUの使用方法です。私は、p2pネットワーク上でセッションをセットアップするために、大量のディスク読み取りまたは大量のネットワーク要求を期待していました。しかし、それはそうですか?ここにはおそらくいくつかの間違いがあります。



プロファイリング



プロファイラーは、CPUのボトルネックを見つけるための優れた方法です。問題は1つだけです。それらのほとんどは、プロセスで何が起こっているのかを完全に把握するためにソースコードインストルメンテーションに依存しています。そして、私はソースコードを持っていません。また、完全なマイクロ秒の読み取り値は必要ありません。4分のボトルネックがあり ます。



だから、スタックサンプリングへようこそ。クローズドソースアプリケーションの場合、これが唯一のオプションです。実行中のプロセススタックと現在の命令ポインタの場所をリセットして、指定した間隔で呼び出しツリーを構築します。次に、それらをオーバーレイして、何が起こっているかに関する統計を取得します。Windowsでこれを実行できるプロファイラーは1つしか知りません。そして、それは10年以上更新されていません。それの ルークStackwalker誰かがルークに愛を与えてください:)







通常、ルークは同じ関数をグループ化しますが、私にはデバッグシンボルがないので、近くのアドレスを調べて一般的な場所を探す必要がありました。そして、私たちは何を見ますか?1つではなく、2つのボトルネック!



不思議な方向へ転がる



私の友人から標準的な逆アセンブラーの完全に合法的なコピーを 借りた後 (いいえ、私は本当にそれを買う余裕はありません...私はヒドラをマスターするでしょう )、私はGTAを分解しに行きました。







完全に間違っているように見えます。はい、ほとんどのトップゲームには、海賊、チート、改造者から安全に保つためのリバースエンジニアリング保護が組み込まれています。それが彼らを止めたことはありません...



ここでは、ある種の難読化/暗号化が適用されており、ほとんどの命令がぎこちないものに置き換えられているようです。心配しないでください。私たちが見たい部分を実行している間、ゲームのメモリをリセットする必要があります。指示は、何らかの方法で起動する前に難読化を解除する必要があります。近く ProcessDumpがあったので、それを取りましたが、同様のタスクのための他のツールがたくさんあります。



問題1:それは... strlen?!



ダンプをさらに分析strlen



すると、どこかから来た特定のラベルが付いたアドレスの1つが明らかになりました 。コールスタックを下に行くと、前のアドレスはとしてマークされ vscan_fn



、その後、ラベルは最終的になりますが、確かにそうです sscanf







スケジュールなしでどこでできますか



彼は何かを解析します。しかし、何ですか?論理解析には時間がかかるため、x64dbgを使用して実行中のプロセスからいくつかのサンプルをダンプすることにし ました。デバッグのいくつかのステップの後、これは... JSONであることがわかります! JSONを解析します。 なんとの10メガバイトにJSONを 63,000アイテム



...,
{
    "key": "WP_WCT_TINT_21_t2_v9_n2",
    "price": 45000,
    "statName": "CHAR_KIT_FM_PURCHASE20",
    "storageType": "BITFIELD",
    "bitShift": 7,
    "bitSize": 1,
    "category": ["CATEGORY_WEAPON_MOD"]
},
...
      
      





それは何ですか?いくつかのリンクから判断すると、これは「オンライン取引ディレクトリ」のデータです。 GTAオンラインで購入できるすべてのアイテムとアップグレードのリストが含まれていると思います。



いくつかの混乱を解消するために、これらはマイクロトランザクションに直接関係しないゲーム内のお金のアイテムであると私は信じています。



10メガバイト?原則として、それほど多くはありません。sscanf



最適な方法で使用されてませんが、もちろんそれほど悪くはありませんか?ええと...







はい、そのような手順には時間がかかります...正直なところ、ほとんどの実装がsscanf



呼び出すと は思いもしませんでした strlen



だから私はこれを書いた開発者を本当に責めることはできません。私はそれがバイトごとにスキャンしているだけで、で停止できると思い NULL



ます。



問題2:ハッシュ...配列を使用しましょう?



2番目の犯罪者は最初の犯罪者の直後に呼び出されることが判明しました。同じ構造if



も、 この醜い逆コンパイルからわかるように、







すべてのラベルは私のものであり、関数/パラメーターが実際に何と呼ばれているのかわかり ません。



2番目の問題?要素が解析された直後に、配列(またはC ++インラインリスト?わからない)に格納されます。各エントリは次のようになります。



struct {
    uint64_t *hash;
    item_t   *item;
} entry;
      
      





そして保存する前に?リストにあるかどうかに関係なく、各要素のハッシュを比較することにより配列全体をチェックします (n^2+n)/2 = (63000^2+63000)/2 = 1984531500



計算を間違えなければ、63千のエントリがあるので、これはおよそ です。そして、これらはほとんど役に立たないチェックです。独自のハッシュがあるので、ハッシュテーブルを使用してみませんか。







リバースエンジニアリング中に名前を付けました hashmap



が、それは明らか _hashmap



です。そして、それはさらに興味深いものになります。このハッシュ配列リストは、JSONをロードする前は空です。そして、JSONのすべての要素は一意です!リストに載っているかどうかを確認する必要もありません。直接要素挿入機能もあります!使うだけ!真剣に、みんな、なんてこった!?



コンセプトの証明



これはすべて素晴らしいことですが、実際のコードを記述して読み込みを高速化し、投稿のクリックベイトタイトルを作成するまで、誰も私を真剣に受け止めません。



計画は以下の通りです。1. .dllを作成し、2。GTAに実装し、3。 いくつかの関数をフックし、4。???、5。利益を上げます。すべてが非常に簡単です。



JSONの問題は重要であり、パーサーを実際に置き換えることはできません。sscanfをstrlenに依存しないものに置き換える方が現実的です。しかし、さらに簡単な方法があります。



  • フックstrlen

  • 長蛇の列を待つ

  • 「キャッシュ」の開始と長さ

  • 別の呼び出しが文字列の範囲内にある場合は、キャッシュされた値を返します


このようなもの:



size_t strlen_cacher(char* str)
{
  static char* start;
  static char* end;
  size_t len;
  const size_t cap = 20000;

  //  ""     
  if (start && str >= start && str <= end) {
    // calculate the new strlen
    len = end - str;

    //    , 
    //        
    if (len < cap / 2)
      MH_DisableHook((LPVOID)strlen_addr);

    //  !
    return len;
  }

  //   
  //      JSON
  //   strlen   
  len = builtin_strlen(str);

  //     
  //     
  if (len > cap) {
    start = str;
    end = str + len;
  }

  // ,  
  return len;
}
      
      







ハッシュ配列の問題については、値が一意であることがわかっているため、すべてのチェックを完全にスキップして要素を直接挿入します。



char __fastcall netcat_insert_dedupe_hooked(uint64_t catalog, uint64_t* key, uint64_t* item)
{
  //   
  uint64_t not_a_hashmap = catalog + 88;

  //  ,   ,   
  if (!(*(uint8_t(__fastcall**)(uint64_t*))(*item + 48))(item))
    return 0;

  //  
  netcat_insert_direct(not_a_hashmap, key, &item);

  //      
  //   .dll,   :)
  if (*key == 0x7FFFD6BE) {
    MH_DisableHook((LPVOID)netcat_insert_dedupe_addr);
    unload();
  }

  return 1;
}
      
      





完全なPoCソースコードは こちらです。



結果



それで、それはどのように機能しますか?



オンラインでの以前の読み込み時間:約6m
重複のパッチチェックの時間:4分30秒
JSONパーサーの使用時間:2分50秒
2つのパッチを一緒にした時間:1分50秒

(6 * 60-(1 * 60 + 50))/(6 * 60)= 69.4%の時間改善(クラス!)


はい、くそー、それはうまくいきました!:))



これはおそらくすべての起動の問題を解決するわけではありません-異なるシステムに他のボトルネックがあるかもしれませんが、それは非常にギャップのある穴なので、R *が何年にもわたってそれを見逃した方法がわかりません。



概要



  • GTAオンラインを起動するときにシングルスレッドのボトルネックがあります

  • GTAが1MBのJSONファイルの解析に苦労していることが判明

  • JSONパーサー自体は不十分/ナイーブで

  • 解析後、重複を削除するための遅い手順があります


R *訂正してください



情報が何らかの形でロックスターのエンジニアに届いた場合、1人の開発者の努力で数時間以内に問題を解決できます。皆さん、これについて何かしてください:<



ハッシュテーブルに移動して重複を削除するか、クイックフィックスとして起動時に重複排除を完全にスキップすることができます。JSONパーサーの場合は、ライブラリをよりパフォーマンスの高いものに置き換えるだけです。もっと簡単な選択肢はないと思います。



ty <3



All Articles