1. 程式人生 > >[譯]我是如何將GTA線上模式的載入時間縮短70%的

[譯]我是如何將GTA線上模式的載入時間縮短70%的

[譯]我是如何將GTA線上模式的載入時間縮短70%的 譯註: 最近在網上發現了一篇有意思的文章, 一個國外大神受不了GTA5線上模式的載入時間, 一怒之下反彙編了GTA5的原始碼, 並最終發現了問題的原因是因為R星寫了一段非常爛的程式碼來讀取JSON! 隨後大神製作了優化補丁將載入時間縮短了70%, 並開源在GITHUB上! 他將從定位問題, 分析問題, 到解決問題的完整過程記錄下來寫成了一篇乾貨滿滿的技術文章. 文章用詞幽默, 充滿了對R星的吐槽, 一經發出很快登上了HackerNews的排行榜, 可見其熱度. WAKU將其完整地翻譯為中文, 供大家學習交流, 翻譯使用意譯方式, 水平有限, 有錯誤請指出:) 原文地址: https://nee.lv/2021/02/28/How-I-cut-GTA-Online-loading-times-by-70/ 原作者: T0ST 日期: 2021-02-28 --- GTA的線上模式.以漫長載入時間而臭名昭著.當我再次進入遊戲來完成一些新的搶劫任務時,我震驚地發現它仍然像7年前釋出的那天一樣慢. 是時候了.是時候來研究下這個問題了. ## #偵察 首先,我想看看是否有人已經解決了這個問題.我發現的大多數結果都是些個人經驗, 說[遊戲如此的複雜,需要載入這麼長時間](https://metro.co.uk/2017/11/01/why-does-gta-v-take-so-long-to-load-7041927/),和一些說P2P架構如何垃圾的故事(不是說它不是),也有建議[先載入故事模式然後再進入線上模式](https://gtaforums.com/topic/908000-fastest-way-to-load-into-gtao-single-player-first-or-straight-in/), 還有能在啟動時跳過R星那個LOGO視訊的Mod.繼續深入的閱讀,我發現可以通過組合這些方法來節省10到30秒! 此時在我的電腦上...... ## #測試 ``` 故事模式載入時間:~1分10秒 線上模式載入時間:~6分鐘 啟動選單禁用了,從R*的LOGO一直到進入遊戲(未計算社交俱樂部登入時間). 老款但正經的CPU:AMD FX-8350 便宜的SSD:金士頓SA400S37120G 必須得有的記憶體:兩條 金士頓 8192 MB(DDR3-1337)99U5471 不錯的GPU:NVIDIA GeForce GTX 1070 ``` 我知道我的配置過時了,但為啥需要6倍的時間才能進入線上模式?我用"先故事, 然後線上"這種載入技術也看不出有任何區別, [之前其他人已經做過類似測試](https://www.reddit.com/r/gtaonline/comments/kycy7a/gtao_loading_times_using_different_methods/). 即使這招確實好使,結果也不會很明顯. 我(並不)孤單 如果[這個調查](https://www.reddit.com/r/gtaonline/comments/ht4i56/your_average_online_loading_time/)可信,那麼這個問題就足以讓超過80%的玩家惱火.7年了, R星! ![](https://img2020.cnblogs.com/blog/648050/202103/648050-20210303141641305-80211077.png) 在四處尋找看誰是那20%能在3分鐘內載入完的幸運兒時, 我看到了用高階遊戲PC進行的[一](https://www.youtube.com/watch?v=RdCqDdjp6iU) [些](https://www.youtube.com/watch?v=pJzr3qfyCyg) [測試](https://www.youtube.com/watch?v=RK7BUFx_NGk), 能達到大約2分鐘的載入時間!2分鐘!讓我死吧! 看起來硬體似乎是關鍵,但事情並不是這麼簡單...... 他們的故事模式為何仍然需要載入近一分鐘?(隨便說一下M.2那個沒有計算啟動LOGO的時間.)另外, 從故事到線上的載入時間只花了他們1分多, 而我是5分多.我知道他們的硬體規格更好,但肯定沒好到5倍. ## #高精度測量 藉助工作管理員這種強大的工具, 我開始調查哪塊兒可能是瓶頸. ![](https://img2020.cnblogs.com/blog/648050/202103/648050-20210303141653756-1979961503.png) 在花了一分鐘用來載入故事和線上模式使用的共同資源後(這時間與高階PC差不多), GTA決定用4分鐘挑戰一下我電腦單核的極限,除此之外就沒有別的了. 磁碟使用?沒有!網路使用?有一點,但在幾秒鐘後,它基本上下降到零(除了載入那個旋轉的資訊橫幅). GPU使用?零.記憶體使用情況?平常平穩...... 那是什麼呢,是在挖礦嗎還是什麼?我感覺到了一些程式碼.非常糟糕的程式碼. ## #單執行緒 雖然我的舊AMD CPU有8個核心而且工作良好,但它是以前生產的.在AMD的單執行緒效能落後於英特爾的年代.這可能沒法解釋所有這些載入時間的差異,但應該能解釋大部分了. 奇怪的是它只使用CPU.我本來以為會有大量的磁碟讀取或者在P2P網路中進行頻繁的網路請求.但瞅現在這個德性? 應該是有BUG了. ## #分析 分析器是尋找CPU瓶頸的一種好方法.但是有一個問題 - 它們中的大多數都依賴於原始碼來洞悉程序中正在發生的事情.我沒有原始碼.而我也不需要微秒級完美的讀數 - 我有4分鐘的瓶頸呢. 使用堆疊取樣:對於沒有原始碼的應用程式,只有這一個選項.定期轉儲(Dump)正在執行程序的堆疊和當前指令指標的位置來建立一個呼叫樹.然後將它們新增到當前的統計資訊中.我只知道一個能在Windows幹這個事兒的分析器(可能孤陋寡聞了).它已經超過10年沒更新了.它就是[Luke Stackwalker](http://lukestackwalker.sourceforge.net/)!有沒有人, 拜託了, 請給這個專案一些愛:) ![](https://img2020.cnblogs.com/blog/648050/202103/648050-20210303141715581-1277173333.png) 通常Luke會將相同的函式分組在一起,但是因為我沒有除錯符號,我不得不用肉眼來看周圍的地址,以猜測它是否是同一個.我們看到了啥?不是一個瓶頸,而是倆! ## #深入虎穴 借用了我朋友的業界標竿的正版反彙編器(不,我確實負擔不起這玩意......我這兩天得學學[Ghidra](https://ghidra-sre.org/)了(譯註:一個開源的逆向工程工具)),我把GTA開了瓢. ![](https://img2020.cnblogs.com/blog/648050/202103/648050-20210303141725666-2029369673.png) 看起來不太妙啊.我們知道大多數知名遊戲都有內建保護,防止逆向工程,以遠離盜版,作弊器和修改器.儘管也沒怎麼防住. 這裡似乎有某種混淆/加密,使用花指令替換了大多數正常的指令.不過不用擔心,我們只是需要在遊戲執行我們關心的那塊兒時轉儲遊戲的記憶體. 而在執行之前, 這些指令肯定是要還原為正常指令的. 我正好手頭有[Process Dump](https://github.com/glmcdona/Process-Dump), 所以我用它了, 但是有很多其他的工具也可以完成這個事兒. ## #問題1: 就是… strlen?! 通過反彙編該"輕微混淆"的轉儲檔案顯示, 其中一個地址被打上標記了!這是`strlen`?沿呼叫堆疊向下找, 下一個被標記的`vscan_fn`,再之後,標籤結束了,但我很自信它應該是[sscanf](https://github.com/chakra-core/ChakraCore/blob/master/pal/src/safecrt/sscanf.c#L47). ![](https://img2020.cnblogs.com/blog/648050/202103/648050-20210303141736411-1596280397.png) 這是在解析什麼東西.解析啥呢?跟這些反彙編糾纏起來沒完沒了, 所以我決定使用x64dbg來轉儲一些程序的取樣.在一些除錯步進後, 結果出來了那就是......JSON!他們正在解析JSON.一個有6萬3千個專案的10MB的JSON. ~~~ JSON { "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線上模式購買的所有可能專案和升級的列表. 這裡澄清一下:我認為這些是遊戲中可購買的物品,與[微交易](https://gta.fandom.com/wiki/Cash_Cards)沒關係. 但10MB?沒事兒!使用`sscanf`可能不是最優的,但肯定不是那麼糟糕?好吧… ![](https://img2020.cnblogs.com/blog/648050/202103/648050-20210303141745860-527390940.png) 是的,這會花一段時間......公平的講,我之前也不知道大部分`sscanf`的實現都呼叫了`strlen`,所以我也不能怪罪寫這個的開發者.我會假設它只是一個位元組一個位元組的掃描,碰到NULL後停止. ## #問題2: 讓我們使用雜湊- ... 陣列? 看起來第二個罪魁禍首是緊接著第一個被呼叫的. 從這個醜陋的反編譯程式碼中能看到它們是在同一個if語句裡被同等呼叫的. ![](https://img2020.cnblogs.com/blog/648050/202103/648050-20210303141757421-302611939.png) 所有的標籤都是我起的,不知道實際呼叫的函式/引數是什麼. 第二個問題?在解析一個專案後,將它儲存在陣列中(或內聯的C++列表?不確定).每個條目看起來長這樣: ~~~ c++ struct { uint64_t *hash; item_t *item; } entry; ~~~ 但在儲存之前?它一個接一個地檢查整個陣列,將專案的雜湊值進行比較,以檢查它是否已經在列表中.大約有6萬3000個專案,如果我沒算錯的話就是`(n^2+n)/2 =(63000^2+63000)/2 = 1984531500`次檢查.絕大多數檢查都沒有用. 你已經有了唯一的雜湊值為什麼不使用雜湊表. ![](https://img2020.cnblogs.com/blog/648050/202103/648050-20210303141808395-1620891672.png) 我在逆向的時候將它命名為"雜湊表",但顯然它"不是一個雜湊表".更絕的是.在載入JSON之前,這個"雜湊陣列列表"是空的.JSON裡所有專案都是唯一的!他們甚至不需要檢查它是否在列表中!他們甚至可以直接插入專案!用啊!真是的, 搞毛呢!? ## #可行性驗證(PoC) 挺好,但是沒人會把我當回事, 除非我測試一下,這樣我就可以給這個帖子起個騙點選的標題. 計劃?寫一個`.dll`,注入進GTA,[hook](https://github.com/TsudaKageyu/minhook)一些函式,???,獲利 JSON問題有點棘手,我無法實際替換他們的解析器.用一個不依賴`strlen`的`sscanf`更現實一些.但是還有一種更簡單的辦法. * hook strlen函式 * 等待一個長字串 * "快取"它的開始位置和長度 * 如果在字串範圍內被再次呼叫的話, 返回快取的值 例如: ~~~ c++ 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) { // 計算新的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; } ~~~ 至於"雜湊陣列"的問題,它更加簡單 - 只需完全跳過重複檢查,直接插入專案,因為我們知道這些值是唯一的. ~~~ c++ 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; } ~~~ 可行性驗證完整程式碼在[這裡](https://github.com/tostercx/GTAO_Booster_PoC). ## #結果 所以, 好使了嗎? ~~~ 原線上模式載入時間: 大概6分鐘 只打了重複檢查補丁的時間: 4分30秒 只打了JSON解析器補丁的時間: 2分50秒 兩個都打的時間: 1分50秒 (6*60 - (1*60+50)) / (6*60) = 69.4% 載入時間改善(棒!) ~~~ 我去,成功了! :)) 也有可能這不會解決所有人的載入時間 - 不同系統可能還有其他瓶頸,但這是一個如此巨大的漏洞,我不知道為什麼R*這些年來都沒有注意到. ## #長求總 * 在啟動GTA線上模式時有一個單執行緒CPU瓶頸 * 看起來GTA在解析一個10MB的JSON檔案時挺費勁 * JSON解析器自身實現的很爛, 並且 * 在解析之後有一個很慢的專案去重步驟 ## #R*請修復吧 如果Rockstar看到了本文:這個問題一個開發應該用不了一天就能解決.乾點事兒吧 :< 你可以要麼切換到雜湊表來去重, 要麼在啟動時完全跳過它, 這樣可以更快. 對於JSON解析器 - 只需換一個性能更好的.我想沒有更簡單的辦法了. 謝謝 <3 ## #小更新 我本來只期待能有一點兒關注,但沒想到這麼火!在登上[HN](https://news.ycombinator.com/item?id=26296339)的排行榜後,這篇文章像野火一樣傳播!謝謝你們的潮水般的迴應:) 如果還有興趣的話, 我會繼續寫一些,但不要指望會很快 - 這裡有很多運氣成份. 一些人建議將這篇文章甩給Rockstar的支援 - 可別!我相信他們現在已經看到了.繼續搞下去其他人的問題可能就沉了.我覺得社交媒體應該是公平的. 有一些HN評論建議我新增一個捐贈按鈕,因為他們想給我買瓶啤酒(謝謝!)所以我在頁尾中放了一個連結. 感謝閱讀和所有的支援:) 版權所有 © 2021 t0st 如果喜歡的話請考慮[給原作者買杯咖啡](https://buymeacoffee.com/t0st)(譯註: 不是啤酒嗎? 到底喝啥)