一、簡述
本文簡要的介紹.NET Framework中System.AppDomain.AssemblyResolve事件的用法、使用注意事項,以及複雜場景下AssemblyResolve事件的汙染問題和解決辦法。
System.AppDomain.AssemblyResolve事件可以理解為「找程式集事件「,假設程式集A依賴了程式集B、C,而B、C不在與A相同的目錄下也不在常規的常規探測路徑之內,此時相應AppDomain(一般指AppDomain.Current)的AssemblyResolve事件會被觸發,程式集A中可儘早響應此事件,實現載入程式集B和C到對應的AppDomain。可參考微軟官方檔案:Resolve assembly loads https://learn.microsoft.com/en-us/dotnet/standard/assembly/resolve-loads#how-the-assemblyresolve-event-works,該文比較詳細的描述了AssemblyResolve的原理、用法和注意事項。不過該文中雖多次提及注意事項,但給出的例程中並沒有很好的體現注意事項,這是官方給馬虎的小夥子埋下的坑之一。
二、典型用法
場景1:讓.NET軟體的安裝目錄更整潔
將所有程式集都放到與主程式集(指.exe程式集也可能是一個.dll)相同的目錄下,在軟體規模稍大時顯得雜亂無章。即使認定使用者不會檢視軟體的安裝目錄,對於愛乾淨的開發者來說也不能忍。於是按功能模組建子資料夾,將相應的dll放入子資料夾內,通過在主程式集入口處立即響應AssemblyResolve事件已實現載入子資料夾中的程式集。PS:主程式集是自己的exe檔案時也可通過應用程式設定解決,如有興趣可百度關鍵字」assemblyBinding「。
場景2:有序的組織軟體內共用程式集
有的時候我們編寫的軟體是大型軟體的外掛,不巧的是,該大型軟體每年升級版本,外掛適配該宿主的多個版本。外掛軟體有較多的程式集,其中少數依賴於宿主的API,多數與宿主無關能不妨稱為軟體內共用程式集,此時可將軟體內共用程式集放安裝目錄,與宿主版本相關的程式集則放到安裝目錄下的子目錄。讓宿主啟動後載入對應子目錄下的"主程式集",該程式集中儘快註冊AssemblyResolve事件以載入軟體內共用程式集。
值得咱們這樣追隨的宿主平臺,通常也有別的追求者,大家都用AssemblyResolve事件,林子大了,就可能出現本文主描述的AssemblyResolve汙染問題。
場景3:從位元組陣列載入程式集
從位元組陣列載入程式集…貌似可以做程式集加密!?不過不用期待太多特別是看完本文之後。此處用一段摘自官方檔案中的文字來描述:如果處理程式有權存取以位元組陣列形式儲存的程式集的資料庫,則它可以通過使用可採用位元組陣列的一種 Assembly.Load 方法過載來載入位元組陣列。
更多用法,歡迎評論討論。
三、AssemblyResolve汙染問題
這裡解釋何為AssemblyResolve汙染,以及問題起因。
AssemblyResolve汙染指AppDomain.AssemblyResolve事件的一個或多個響應函數沒有按該事件的響應規範正確處理,影響所有依賴於該事件的功能模組執行穩定性。通常問題現象是,執行到某個具體功能時提示找不到*.Resources.dll,或*.XmlSerializers .dll,或提未能從程式集xxx中載入型別yyy。
咱們僅討論無意中未按AssemblyResolve響應規範正確處理的情況。此時的不規範響應通常是:遇到不能處理的程式集時應簡單的返回null,但自.NET Framework 4.0開始,事件引數ResolveEventArgs增加了RequestingAssembly屬性,該屬性剛好是一個程式集且名字看著挺像是」正要找的程式集「,於是有些開發者在遇到不能處理的程式集時返回ResolveEventArgs.RequestingAssembly。一旦返回了一個不為null的程式集物件,AssemblyResolve事件的其它響應函數變不會再被呼叫(本文在」探討「一節中證實這個情況),從而引起本節描述的AssemblyResolve汙染問題。
問題1:提未能從程式集xxx中載入型別yyy
如果有問題的響應函數先於咱們的程式響應了AppDomain.AssemblyResolve事件,則一直輪不到咱們程式集中的AppDomain.AssemblyResolve事件,於是出現這個問題。
問題2:提示找不到*.Resources.dll,或*.XmlSerializers .dll
這類衛星程式集或附屬程式集到上下文的載入,從.NET Framework 4.0開始也會觸發事件,要求統一簡單的返回null。摘一段官網說明:
!Important
Beginning with the .NET Framework 4, the AssemblyResolve event is raised for satellite assemblies. This change affects an event handler that was written for an earlier version of the .NET Framework, if the handler tries to resolve all assembly load requests. Event handlers that ignore assemblies they do not recognize are not affected by this change: They return null, and normal fallback mechanisms are followed.
四、解決辦法
一種解決辦法是喊話對應的軟體開發商讓修改。不過也許對應的開發商已經跑路,或的確做了修改,但使用者側仍然用了未修改前的舊版本,這種方式是不可靠的。
問題解決思路:
通過反射拿到承載AppDomain.AssemblyResolve事件的Delegate,逐一檢查Delegate中各ResolveEventHandler是否正常,不正常者關小黑屋後改造後再置入AppDomain.AssemblyResolve事件的Delegate。
附例程:
1 class AssemblyResolveHook 2 { 3 ResolveEventHandler _handler; 4 const string c_no_this_assembly = "NoThisAssembly, Version=1.0.0.0, Culture=zh-CN, PublicKeyToken=null"; 5 6 AssemblyResolveHook(ResolveEventHandler handler) 7 { 8 _handler= handler; 9 } 10 11 Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) 12 { 13 if (_handler== null) { 14 return null; 15 } 16 17 // 這裡是「小黑屋」改造過程 18 // 此處是對症下藥的較溫和的無害的處理方式 19 var asm = args.RequestingAssembly; 20 var asm2 = _handler(sender, args); 21 if (asm2 != null && asm != null && asm.FullName == asm2.FullName) { 22 asm2 = null; 23 } 24 25 return asm2; 26 } 27 28 /// <summary> 29 /// 使呼叫本方法之前的所有CurrentDomain_AssemblyResolve事件響應無害化 30 /// </summary> 31 public static void Handsup() 32 { 33 try { 34 var domain = AppDomain.CurrentDomain; 35 var far = domain.GetFieldValue("_AssemblyResolve") as ResolveEventHandler; 36 if (far != null) { 37 var invocationList = far.GetInvocationList(); 38 var num = invocationList.Length; 39 for (int i = 0; i < num; i++) { 40 var handler = (ResolveEventHandler)invocationList[i]; 41 42 // 測試一下這個handler有沒有問題? 43 // 方法是給一個不可能找到的程式集名稱,看是否返回了程式集,如果是,給關小黑屋 44 var asm = handler(domain, new ResolveEventArgs(c_no_this_assembly, System.Reflection.Assembly.GetExecutingAssembly())); 45 if (asm != null) { 46 handler = new ResolveEventHandler(new AssemblyResolveHook(handler).CurrentDomain_AssemblyResolve); 47 } 48 49 far = i == 0 ? handler : (ResolveEventHandler)Delegate.Combine(far, handler); 50 } 51 52 domain.SetFieldValue("_AssemblyResolve", far); 53 } 54 } 55 catch (System.Exception ex) { 56 System.Diagnostics.Debug.WriteLine(ex.ToString()); 57 } 58 } 59 }
幾點解釋:
問:例程中的GetFieldValue/SetFieldValue,沒有這樣的方法?
答:這是擴充套件方法,能看到這裡的你,也不會在乎反射的這幾行程式碼怎麼寫
問:AssemblyResolveHook.Handsup 呼叫之後,新新增的 AssemblyResolve 事件響應有問題怎麼辦?
答:一般AssemblyResolve事件會在第一時間響應,故可延遲呼叫AssemblyResolveHook.Handsup
問:怎麼知道欄位名是 _AssemblyResolve?
答:反正用VS2022社群版,遊標放程式碼的AppDomain上按F12,就能看到答案。其它版本VS應該也行
問:被關小黑屋了,如果對方要登出 AssemblyResolve 事件的響應咋辦?
答:太多問題了…
五、探討
本問題提及AssemblyResolve事件的某響應函數一旦返回了非null程式集,就不會再呼叫後續的響應函數,可以遊標放程式碼的AppDomain上按F12,找到對應的 .NET Framwork 程式碼證實。
如下:
1 [SecurityCritical] 2 private RuntimeAssembly OnAssemblyResolveEvent(RuntimeAssembly assembly, string assemblyFullName) 3 { 4 ResolveEventHandler assemblyResolve = _AssemblyResolve; 5 if (assemblyResolve == null) { 6 return null; 7 } 8 9 Delegate[] invocationList = assemblyResolve.GetInvocationList(); 10 int num = invocationList.Length; 11 for (int i = 0; i < num; i++) { 12 Assembly asm = ((ResolveEventHandler)invocationList[i])(this, new ResolveEventArgs(assemblyFullName, assembly)); 13 RuntimeAssembly runtimeAssembly = GetRuntimeAssembly(asm); 14 if (runtimeAssembly != null) { 15 return runtimeAssembly; 16 } 17 } 18 19 return null; 20 }
如果哪天微軟對上述程式碼稍加修改,本關小黑屋方法就可以退休了。
(全文完,本文最早由yangzhj發表於部落格園,轉載需註明出處)