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