試:dnSpy 反編譯 IL 與斷點(diǎn)實(shí)戰(zhàn))
簡介這是一套面向 Unity 游戲開發(fā)與逆向分析學(xué)習(xí)者的 dnSpy 反編譯工具包主要用于查看、調(diào)試和修改 .NET 程序集適合需要排查 Unity 項(xiàng)目代碼邏輯、研究源碼結(jié)構(gòu)或進(jìn)行程序集級調(diào)試的開發(fā)者。壓縮包為 rar 格式共收錄 1736 個文件整體約 134.32MB其中 dll 程序集約 1583 個構(gòu)成核心組件與依賴庫pdb 調(diào)試符號 76 個便于定位方法級問題另有 json、xml 配置說明txt 文檔以及 exe 可執(zhí)行程序、dntheme 界面主題等配套內(nèi)容層次較為清晰?,F(xiàn)有 3954 人學(xué)習(xí)下載說明其在 Unity 反編譯與源碼分析場景中具有一定參考價值。讀者可據(jù)此獲得可運(yùn)行的 dnSpy 工具環(huán)境、較完整的依賴鏈與調(diào)試符號減少自行搜集組件和排查版本兼容問題的時間并為進(jìn)一步理解 Unity 程序集加載、代碼查看與調(diào)試流程提供工具基礎(chǔ)可較快進(jìn)入程序集查看與調(diào)試環(huán)節(jié)。1. 從一行被裁掉的堆棧說起dnSpy 在 Unity 項(xiàng)目里能頂什么用Unity 項(xiàng)目打到真機(jī)上崩了日志只給一行 NullReferenceException堆棧被裁剪得只剩三個方法名。這種時候如果能在游戲引擎產(chǎn)物里直接翻到源碼級 IL 并下斷點(diǎn)比對著日志猜快得多。dnSpy 干的就是這件事它不是編輯器插件而是一個 .NET 程序集的反編譯加調(diào)試工具把 Assembly-CSharp.dll 拖進(jìn)去C# 源碼、IL、元數(shù)據(jù)三層可以對著看還能附加到進(jìn)程上單步。這篇講的是我在自研包體上真刀真槍跑過的一整套流程工具怎么選、程序集怎么從包里拿出來、斷點(diǎn)怎么下、哪些操作是給自己找麻煩。新手能照著跑通熟手能對上判斷依據(jù)。2. 工具對位dnSpy、ILSpy、命令行反匯編三條路怎么選2.1 四條對比維度從「能不能下斷點(diǎn)」開始我判斷一個反編譯工具值不值得留在機(jī)器上第一看的不是反編譯出來的代碼有多漂亮而是它能不能讓程序停在我想看的那一行。代碼漂亮只是讀起來舒服能停下來才是證據(jù)。圍繞這個核心我一般會按四條維度去比。第一條是可調(diào)試性。只讀反編譯和可調(diào)試反編譯是兩個物種。只讀工具給你一份近似源碼你得靠讀代碼推運(yùn)行時狀態(tài)可調(diào)試工具能附加進(jìn)程、打斷點(diǎn)、看局部變量、看調(diào)用棧等于把黑匣子撬開一條縫。排查「為什么這個參數(shù)傳進(jìn)來是 null」這類問題時兩者的效率差出量級。第二條是能不能編輯 IL 并回寫模塊。這條決定你能否做「改一行、跑一次、看結(jié)果」的快速驗(yàn)證。有些猜想光靠讀代碼驗(yàn)證不了比如某個分支到底走沒走、某個閾值改成 0 會怎樣直接在方法體里改一句再保存比重新打包整個工程快幾十倍。當(dāng)然這個能力只對自研包體開放理由后面會講。第三條是依賴解析和 Unity 特化。Unity 的程序集不是孤立的Assembly-CSharp.dll 會引用 UnityEngine.CoreModule、UnityEngine.PhysicsModule、各種第三方托管 DLL。工具能不能自動從同目錄把引用鏈補(bǔ)齊直接決定你打開文件時看到的是一片正常的類名還是一片紅色的「未能解析」。第四條是大程序集的加載和搜索性能。商業(yè)項(xiàng)目裁完還剩幾十個程序集、幾萬個類型很正常搜索一個方法名要轉(zhuǎn)十幾秒的工具用兩天你就會放棄它。這一條很少有人寫在評測里但它決定了工具是「偶爾用用」還是「常駐任務(wù)欄」。2.2 對比表與結(jié)論什么場景該摸哪個把上面四條落成表結(jié)論就清楚了。下面這張表是我自己機(jī)器上長期保留的三個組合按「日常只讀」「要下斷點(diǎn)」「要批量處理」三類任務(wù)分工。維度dnSpyILSpy命令行反匯編monodis / ildasm 類反編譯可讀性好C# 視圖和 IL 視圖可同屏對照很好語法高亮和折疊體驗(yàn)更順差輸出的是 IL 文本需自己腦內(nèi)翻譯附加進(jìn)程下斷點(diǎn)支持可單步、看局部變量、看調(diào)用棧不支持不支持編輯 IL 并回寫支持改完可直接保存模塊不支持不支持依賴解析支持配置程序集搜索目錄能補(bǔ)齊引用鏈支持拖入依賴目錄即可需手工喂全部依賴資源與元數(shù)據(jù)瀏覽支持可看字符串、資源、特性支持弱大程序集加載一般超大模塊首次加載偏慢較快快但結(jié)果不可交互上手成本中功能多所以菜單深低高要熟 IL 指令典型場景斷點(diǎn)復(fù)現(xiàn)、臨時改 IL 驗(yàn)證猜想快速翻代碼、搜方法名、對照兩個版本差異腳本化批處理、把 IL 導(dǎo)出成文本做 diff結(jié)論不用繞日常翻代碼、搜關(guān)鍵字我用 ILSpy啟動快、搜索跟手一旦問題從「這段代碼寫了什么」變成「運(yùn)行時到底發(fā)生了什么」立刻換 dnSpy需要把幾十個程序集的 IL 全導(dǎo)出成文本做版本間比對才動命令行那套。三者不是替代關(guān)系是分工。有一點(diǎn)得提前說清楚dnSpy 上游的更新節(jié)奏早就慢下來了社區(qū)有維護(hù)分支在接。這不影響它作為調(diào)試器的價值但意味著你偶爾會遇到新版本 .NET 運(yùn)行時的元數(shù)據(jù)讀不全。真碰上了別在工具上死磕換條路驗(yàn)證更快。2.3 裝環(huán)境前的三個前置判斷后端、構(gòu)建類型、裁剪等級很多人第一次用 dnSpy 翻車不是操作錯是打開方式從一開始就不對。動手前先確認(rèn)三件事能省掉大半無效勞動。第一腳本后端是 Mono 還是 IL2CPP。這是分水嶺。Mono 后端下C# 代碼編譯成托管 IL落在 Assembly-CSharp.dll 里dnSpy 能完整反編譯、能下斷點(diǎn)。IL2CPP 后端會把 IL 轉(zhuǎn)成 C 再編譯成原生機(jī)器碼托管程序集里只剩一層轉(zhuǎn)發(fā)樁你打開看到的全是空方法體或者一句「該方法由運(yùn)行時提供」。這時候 dnSpy 的價值就退化成「看編輯器下編譯出的程序集」和「讀第三方托管 DLL」想看游戲邏輯本身得換思路。判斷方法很簡單看 Player Settings 里的 Scripting Backend或者看包里有沒有 libil2cpp.so 這類原生庫。第二是不是 Development Build。Unity 的腳本調(diào)試依賴構(gòu)建時打開的調(diào)試符號和調(diào)試端口。Release 包默認(rèn)關(guān)掉這些你附加進(jìn)程時會發(fā)現(xiàn)根本連不上。要斷點(diǎn)復(fù)現(xiàn)必須出一版勾了 Development Build 和 Script Debugging 的包這一點(diǎn)沒有捷徑。第三Managed Stripping Level 開到哪一檔。裁剪等級直接決定你反編譯出來還剩多少方法體。裁剪等級反編譯看到的樣子對調(diào)試的影響Disabled方法體完整類名和字段名保留最適合斷點(diǎn)復(fù)現(xiàn)符號最全Low大部分保留未引用的類型可能被裁一般夠用個別方法會消失Medium反射用到的類型開始出現(xiàn)缺口斷點(diǎn)可能打在你找不到的方法上High大量方法體被替換成空實(shí)現(xiàn)或拋異?;緹o法用于邏輯復(fù)現(xiàn)這三條判斷做完你才知道手里的包到底值不值得用 dnSpy 打開。我的習(xí)慣是只要今天的目標(biāo)是「復(fù)現(xiàn)某個運(yùn)行時問題」就先出一版 Disabled 裁剪加 Development Build 的包專門用來調(diào)調(diào)完再回到正式配置驗(yàn)證。這個習(xí)慣救過我很多次因?yàn)槟谜桨舱{(diào)你會把大量時間花在「這個方法為什么是空的」上而不是問題本身。3. 從包體到可讀 IL定位程序集與讀懂結(jié)構(gòu)的完整鏈路3.1 定位程序集托管 DLL 到底躺在哪Unity 打出來的包本質(zhì)是個壓縮容器托管程序集的位置是固定的。安卓側(cè)在 assets/bin/Data/Managed/ 下iOS 側(cè)在 Payload 里那個 .app 的 Data/Managed/ 下編輯器下則直接在你工程的 Library/ScriptAssemblies/ 或者打包輸出的 Data/Managed/ 里。先說明邊界下面這些操作我只在自己構(gòu)建的調(diào)試包、或者明確拿到授權(quán)的包體上做。第三方商業(yè)包不在討論范圍內(nèi)那不是技術(shù)問題是底線問題。# 先看包里有什么確認(rèn) Managed 目錄存在 unzip -l app-debug.apk | grep -i Managed # 只解出托管程序集目錄避免把整個包攤開 unzip app-debug.apk assets/bin/Data/Managed/* -d ./extract # 看清單Assembly-CSharp.dll 是項(xiàng)目腳本其余是引擎模塊和第三方庫 ls -lh ./extract/assets/bin/Data/Managed/ | head -30第一行只是列目錄不改動任何東西用來確認(rèn)這個包里確實(shí)有托管程序集。第二行用通配符只解壓 Managed 目錄好處是不用把貼圖音頻全解出來占空間。第三行看清單重點(diǎn)是分清三類文件Assembly-CSharp.dll 是你自己寫的游戲邏輯UnityEngine.*.Module.dll 是引擎模塊剩下名字帶廠商前綴的是接入的第三方庫。這里有個容易忽略的點(diǎn)Assembly-CSharp.dll 只裝默認(rèn)程序集里的腳本。如果工程里配了 Assembly Definition腳本會分散到多個自定義 DLL 里名字就是你當(dāng)初起的程序集名。所以翻不到目標(biāo)類時先看清單里有沒有其他可疑的 DLL別急著懷疑工具。3.2 在 dnSpy 里讀懂結(jié)構(gòu)類名、字段與 Inspector 的對應(yīng)關(guān)系把 Assembly-CSharp.dll 拖進(jìn) dnSpy左側(cè)是程序集樹展開后按命名空間分層。第一次打開最常見的困惑是代碼能看但不知道從哪看起。我的入手順序是固定的。先找入口類通常是名字里帶 Game、Manager、Bootstrap 的那個看它的 Awake 和 Start 里初始化了哪些子系統(tǒng)。然后按調(diào)用鏈往外擴(kuò)而不是漫無目的地翻類列表。dnSpy 的「分析」功能可以列出某個方法被誰調(diào)用反過來也能列出它調(diào)了誰順著這個圖走比按字母序翻快得多。第二個要理解的是序列化字段和 Inspector 的對應(yīng)關(guān)系。Unity 的序列化靠字段名匹配所以哪怕代碼被混淆過MonoBehaviour 上那些需要在 Inspector 里拖引用的字段名通常還是原樣保留的——混淆器一般不敢動它們動了場景和預(yù)制體就全丟引用。這意味著你可以靠一個 public 字段名反推出它屬于哪個類這是很實(shí)用的定位技巧。第三個是引擎模塊的引用解析。打開 Assembly-CSharp.dll 時如果右側(cè)代碼里 UnityEngine 的類型全是紅的說明 dnSpy 沒找到引擎模塊。解決辦法是把解壓出來的整個 Managed 目錄設(shè)為程序集搜索路徑讓它自己去同目錄找依賴。這一步做完代碼里的類型、方法簽名、繼承關(guān)系才會完整顯示。// 反編譯出來的樣子來自自研包體用于說明讀法 public class UnitController : MonoBehaviour { public float maxHp 100f; // Inspector 里可拖混淆器通常保留此名 private float currentHp; private void Awake() { this.currentHp this.maxHp; } public void ApplyDamage(float amount) { this.currentHp - amount; if (this.currentHp 0f) { GameManager.Instance.OnUnitDead(this.gameObject); } } }這段代碼本身沒什么特別的重點(diǎn)在怎么讀它。maxHp 是 public 且有初值說明它大概率在 Inspector 里暴露可以拿這個字段名去場景文件里搜反查這個腳本掛在哪些預(yù)制體上。currentHp 是 private 且沒有序列化標(biāo)記運(yùn)行時狀態(tài)只能靠斷點(diǎn)看。ApplyDamage 里那句 GameManager.Instance 是典型的單例訪問如果崩潰堆棧指向這里八成是 Instance 還沒初始化而不是傷害計算本身有問題。讀代碼的價值就在這——它能把「哪里可能出錯」縮小到兩三個點(diǎn)。3.3 反編譯結(jié)果落盤導(dǎo)出成工程后怎么用單個文件看不夠的時候我會把整個程序集導(dǎo)出成一份可編譯的工程。dnSpy 的導(dǎo)出功能會把每個類型拆成一個 .cs 文件按命名空間建目錄同時生成工程文件。這么做的目的不是重新編譯而是拿到一份可以用編輯器全局搜索、可以跨文件跳轉(zhuǎn)、可以丟進(jìn)版本控制做 diff 的代碼。導(dǎo)出后一般會得到這么一堆東西產(chǎn)物用途按命名空間分層的 .cs 文件全局搜方法名、字段名、字符串常量工程文件用 IDE 打開享受跳轉(zhuǎn)和引用查找資源與元數(shù)據(jù)查特性標(biāo)注、查常量表、查嵌入資源這份導(dǎo)出代碼有兩個明確用途。一是做版本對比把改動前后兩個包各導(dǎo)一份用 diff 工具比能精確看到這次改動動了哪些方法比看提交記錄可靠。二是做全局檢索想找「哪個地方把音量寫死成 0.3」直接全目錄搜 0.3f比在 dnSpy 里逐個類翻快得多。要注意的是導(dǎo)出的代碼不一定能編譯通過混淆過的名字、被裁剪的方法體、缺失的依賴都會報錯。別在這上面浪費(fèi)時間把它當(dāng)只讀的檢索素材就行。4. 斷點(diǎn)復(fù)現(xiàn)給自研包體下斷點(diǎn)、改 IL、看真實(shí)入?yún)?.1 附加進(jìn)程與斷點(diǎn)設(shè)置從連不上到停得住斷點(diǎn)復(fù)現(xiàn)的前提是能附加。編輯器下最簡單出一版 Development BuildPlay 起來dnSpy 里選調(diào)試、附加到進(jìn)程找到編輯器進(jìn)程附加即可。真機(jī)要麻煩一層需要把設(shè)備上的調(diào)試端口轉(zhuǎn)發(fā)到本機(jī)。# 確認(rèn)設(shè)備已連接且裝的是帶腳本調(diào)試的 Development Build adb devices # 把設(shè)備上的調(diào)試端口轉(zhuǎn)發(fā)到本機(jī)同端口端口號以構(gòu)建日志里打印的為準(zhǔn) adb forward tcp:55555 tcp:55555 # 啟動應(yīng)用讓它開始監(jiān)聽調(diào)試端口 adb shell am start -n com.example.demo/.MainActivity第一條確認(rèn)設(shè)備在線。第二條把設(shè)備端口映射到本機(jī)dnSpy 附加時就連本機(jī)的這個端口。第三條拉起應(yīng)用注意包名要換成你自己的這里只是占位示例。端口號不要照抄Unity 在構(gòu)建日志里會打印實(shí)際監(jiān)聽端口以那個為準(zhǔn)。連上之后是打斷點(diǎn)。這里有個經(jīng)驗(yàn)不要一上來就在可疑方法的第一行下斷那樣你會被高頻調(diào)用淹掉。先在有明確觸發(fā)條件的路徑上下比如「點(diǎn)擊某個按鈕之后的處理函數(shù)」「某次網(wǎng)絡(luò)回包之后的解析函數(shù)」命中一次看一眼局部變量確認(rèn)參數(shù)和預(yù)期是否一致再決定要不要往上游追。命中斷點(diǎn)后重點(diǎn)看三樣入?yún)⒌膶?shí)際值、this 指向的實(shí)例字段、調(diào)用棧上每一層的參數(shù)。很多時候 bug 的根因不在當(dāng)前方法而在上游某個調(diào)用方傳了個意料之外的 null 或者 0。調(diào)用棧往下翻兩層往往就水落石出了。4.2 改 IL 驗(yàn)證猜想只在自研包體上做的快速實(shí)驗(yàn)有些猜想讀代碼驗(yàn)證不了必須跑一次才知道。比如「這個閾值如果設(shè)成 0分支會不會走到另一條」重新改代碼打包要幾分鐘改 IL 只要幾十秒。dnSpy 支持在方法體上右鍵編輯改完點(diǎn)編譯再保存模塊替換回包里重啟即可。// 原始邏輯傷害小于 1 時直接忽略 public void ApplyDamage(float amount) { if (amount 1f) return; this.currentHp - amount; } // 驗(yàn)證用改法把閾值臨時改成 0觀察后續(xù)分支 public void ApplyDamage(float amount) { if (amount 0f) return; this.currentHp - amount; }這么改的目的是驗(yàn)證「邊界值被吃掉」這個猜想是否成立跑一次看表現(xiàn)就能確認(rèn)確認(rèn)完必須改回來。這里有個硬性紀(jì)律改 IL 只在本地自研調(diào)試包上做改完的包不對外分發(fā)驗(yàn)證結(jié)束立刻用原始產(chǎn)物覆蓋回去。因?yàn)楦牧?IL 就等于繞開了源碼后續(xù)再出問題你沒法靠代碼回溯這是給自己埋雷。還有一個技術(shù)限制要知道如果程序集帶強(qiáng)名稱簽名改完保存會導(dǎo)致簽名失效加載時直接報類型加載異常。碰到這種情況別想著去重簽名直接換一條驗(yàn)證路徑——要么回源碼改要么用斷點(diǎn)看運(yùn)行時值代替修改。4.3 混淆與反混淆的邊界靠什么線索把名字還原回來自研項(xiàng)目上了混淆之后dnSpy 里看到的類名可能是 a、b、c方法名可能是 m1、m2。這時候別急著找反混淆工具先想想有哪些名字是混淆器動不了的靠這些錨點(diǎn)往往能把結(jié)構(gòu)還原個七七八八。第一類是 Unity 的消息方法。Awake、Start、Update、OnEnable、OnTriggerEnter 這些是引擎按名字反射調(diào)用的混淆器改了引擎就找不到所以它們一定是原名。找到 Update 就能定位到每幀邏輯的入口順著往下讀邏輯主線就出來了。第二類是序列化字段。前面提過Inspector 拖引用靠字段名匹配混淆器一般保留??恳粋€ public 字段名去場景和預(yù)制體里反查能直接鎖定類的用途。第三類是字符串常量。日志文案、報錯信息、界面文本通常不會混淆。在 dnSpy 里搜一句日志原文能直接跳到打印它的方法再從方法反推類。第四類是第三方庫的調(diào)用點(diǎn)。接入的 SDK 方法名是外部程序集定義的混淆器改不了調(diào)用簽名所以看到某個類在調(diào)支付、統(tǒng)計、推送的 API基本能判斷這個類的職責(zé)。這四條線索疊起來即使名字全亂了也能在半小時內(nèi)把關(guān)鍵路徑摸清。反混淆工具當(dāng)然也有但它給你的是一堆看起來正常但可能語義錯亂的名字反而容易誤導(dǎo)判斷。我的偏好是靠錨點(diǎn)自己推推出來的結(jié)構(gòu)圖和真實(shí)邏輯對得上比工具輸出可信。5. 避坑記錄五個讓調(diào)試當(dāng)場翻車的操作5.1 打開就一片紅依賴沒補(bǔ)齊的典型表現(xiàn)現(xiàn)象把 Assembly-CSharp.dll 拖進(jìn) dnSpy代碼能顯示但 UnityEngine 相關(guān)的類型、基類、方法簽名全是紅色波浪線點(diǎn)跳轉(zhuǎn)跳不過去甚至有些類展開后成員列表是空的。原因dnSpy 加載程序集時只認(rèn)當(dāng)前文件和它已配置的搜索路徑。你只拖了一個 dll 進(jìn)去引擎模塊和第三方庫都不在它的視野里引用解析自然失敗。這不是文件損壞也不是版本不匹配。解決把解壓出來的整個 Managed 目錄加進(jìn)程序集搜索路徑或者在打開前先把同目錄所有 dll 一起選中拖進(jìn)去。補(bǔ)齊之后重新加載紅色會消失繼承鏈和方法簽名也會完整顯示。這個坑我見過太多人第一次就踩然后誤以為是工具不支持新版本。5.2 附加不上進(jìn)程調(diào)試符號和構(gòu)建類型沒對上現(xiàn)象dnSpy 的附加進(jìn)程列表里根本看不到目標(biāo)進(jìn)程或者看到了但點(diǎn)附加之后報連接失敗、超時。原因三種可能。一是包不是 Development Build構(gòu)建時沒生成調(diào)試符號二是 Script Debugging 沒勾三是真機(jī)場景下端口沒轉(zhuǎn)發(fā)或者轉(zhuǎn)發(fā)的端口號和實(shí)際監(jiān)聽的不一致。解決按順序排查。先看構(gòu)建配置里 Development Build 和 Script Debugging 是否都勾上再看構(gòu)建日志里打印的調(diào)試端口號把 adb forward 的端口改成一致的最后確認(rèn)設(shè)備上的應(yīng)用是由調(diào)試器拉起或已主動開始監(jiān)聽。三步都對了還連不上就換編輯器環(huán)境先驗(yàn)證一遍排除是設(shè)備側(cè)的問題。5.3 改完 IL 保存后加載報錯強(qiáng)名稱簽名在攔你現(xiàn)象在 dnSpy 里改了一個方法體保存模塊替換回包里應(yīng)用啟動直接拋類型加載異常或者程序集加載失敗。原因程序集帶強(qiáng)名稱簽名。你改了 IL簽名就和內(nèi)容對不上了運(yùn)行時校驗(yàn)直接拒絕加載。這不是你改錯了代碼是簽名機(jī)制在起作用。解決別去重簽名那條路會把你帶進(jìn)更深的坑而且改出來的產(chǎn)物不可信。正確做法是放棄改 IL 這條驗(yàn)證路徑回到源碼改、重新打包。如果只是想在運(yùn)行時看值用斷點(diǎn)代替修改一樣能達(dá)到目的。5.4 方法體是空的裁剪等級和 IL2CPP 兩個常見誤判現(xiàn)象反編譯出來的方法體只有一行 throw null或者干脆是空的什么都不做。原因兩種情況最常見。一是 Managed Stripping Level 開得高未被引用的方法體被裁掉了二是這個包是 IL2CPP 后端托管程序集里只剩轉(zhuǎn)發(fā)樁真實(shí)邏輯在原生庫里。解決先確認(rèn)后端??窗镉袥]有 libil2cpp.so 之類原生庫有就是 IL2CPPdnSpy 在這條路上到此為止別再往下折騰。確認(rèn)是 Mono 后端后檢查裁剪等級出一版 Disabled 的調(diào)試包再看。這一步最好在動手之前就做能省掉大量「為什么代碼是空的」的困惑。5.5 斷點(diǎn)命中了但值不對符號和實(shí)際產(chǎn)物不同步現(xiàn)象斷點(diǎn)能停住但看到的局部變量值明顯和實(shí)際行為對不上或者行號錯位單步跳轉(zhuǎn)的位置很詭異。原因調(diào)試符號和當(dāng)前運(yùn)行的二進(jìn)制不是同一次構(gòu)建的產(chǎn)物。常見于你改了代碼重新打包但 dnSpy 還加載著舊版本的程序集或者設(shè)備上裝的是舊包電腦上打開的是新 dll。解決每次調(diào)試前把設(shè)備上的應(yīng)用卸載重裝確認(rèn) dll 是從當(dāng)前這個包里解出來的再重新加載。養(yǎng)成「包和符號同源」的習(xí)慣別在兩個版本之間來回切。這個坑最隱蔽因?yàn)閿帱c(diǎn)確實(shí)停了你會誤以為看到的是真相實(shí)際是被過期符號騙了。6. 一套能復(fù)用的排查習(xí)慣從復(fù)現(xiàn)到驗(yàn)證的閉環(huán)把上面這些串起來我現(xiàn)在處理一個運(yùn)行時問題的固定動作是這樣的五步順序不亂。第一步先判斷能不能用這條路???Scripting Backend 和 Managed Stripping LevelIL2CPP 或者高裁剪的包直接換思路不在工具上浪費(fèi)時間。第二步出一版專用調(diào)試包Development Build 加 Script Debugging裁剪等級臨時關(guān)掉。這一步多花兩分鐘構(gòu)建后面能省兩小時。第三步定位程序集把 Managed 目錄整體作為搜索路徑加載確認(rèn)依賴解析完整再開始讀代碼。第四步讀代碼縮小范圍選定兩三個可疑方法下斷點(diǎn)命中后看入?yún)?、?this、看調(diào)用棧上下游兩層。第五步如果讀代碼驗(yàn)證不了改 IL 跑一次快速實(shí)驗(yàn)驗(yàn)證完立刻還原絕不讓改過的產(chǎn)物留在手里。這五步里第三步和第五步是最容易偷懶的。第三步偷懶的后果是看一片紅代碼然后懷疑工具第五步偷懶的后果是某天你發(fā)現(xiàn)本地跑得好好的打包出去行為不對回頭查半天才發(fā)現(xiàn)是那次實(shí)驗(yàn)的改動沒還原。血淚經(jīng)驗(yàn)別問我是怎么知道的。再給一個具體技巧收尾。判斷一個方法在運(yùn)行時到底被調(diào)了多少次、入?yún)⒎植际裁礃硬挥孟聰帱c(diǎn)也能做在 dnSpy 里找到方法看它的 IL 里有沒有被內(nèi)聯(lián)的跡象如果方法體很短又被高頻調(diào)用JIT 可能已經(jīng)把它內(nèi)聯(lián)斷點(diǎn)會打不中。這時候把斷點(diǎn)下在它的調(diào)用方或者臨時給方法加個不內(nèi)聯(lián)的特性再重打包命中率立刻就上來了。這個細(xì)節(jié)很多人卡很久其實(shí)原因就一句話?;剡^頭看我在這類工具上踩過的坑絕大多數(shù)不是工具本身的問題是「包、符號、配置三者不同源」造成的假象。從那以后我每次動手前都強(qiáng)制走一遍同源檢查設(shè)備上的包、解出來的 dll、dnSpy 加載的模塊三者必須是同一次構(gòu)建的產(chǎn)物確認(rèn)完再往下做。這個習(xí)慣讓我少走了很多彎路。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取