Cara menggunakan dan men-debug unloadability assembly di .NET
.NET (Core) memperkenalkan kemampuan untuk memuat dan kemudian membongkar satu set rakitan. Di .NET Framework, domain aplikasi kustom digunakan untuk tujuan ini, tetapi .NET (Core) hanya mendukung satu domain aplikasi default.
Unloadability didukung melalui AssemblyLoadContext. Anda dapat memuat satu set assembly ke dalam metode yang dapat AssemblyLoadContext
dikumpulkan, menjalankan metode di dalamnya atau hanya memeriksanya menggunakan refleksi, dan akhirnya membongkar AssemblyLoadContext
. Yang membongkar assembly yang dimuat ke dalam AssemblyLoadContext
.
Ada satu perbedaan penting antara pembongkaran menggunakan AssemblyLoadContext
dan menggunakan AppDomains. Dengan AppDomains, pembongkaran dipaksa. Pada waktu unload, semua utas yang berjalan di AppDomain target dibatalkan, objek COM terkelola yang dibuat di AppDomain target dihancurkan, dan sebagainya. Dengan AssemblyLoadContext
, bongkar adalah "kooperatif". Memanggil metode AssemblyLoadContext.Unload hanya memulai pembongkaran. Pembongkaran selesai setelah:
- Tidak ada utas yang memiliki metode dari assembly yang dimuat ke
AssemblyLoadContext
dalam tumpukan panggilan mereka. - Tidak ada jenis dari rakitan yang dimuat ke dalam , instans jenis tersebut
AssemblyLoadContext
, dan rakitan itu sendiri direferensikan oleh:- Referensi di luar
AssemblyLoadContext
, kecuali untuk referensi yang lemah (WeakReference atau WeakReference<T>). - Handel pengumpul sampah (GC) yang kuat (GCHandleType.Normal atau GCHandleType.Pinned) dari dalam dan di luar
AssemblyLoadContext
.
- Referensi di luar
Menggunakan AssemblyLoadContext yang dapat dikumpulkan
Bagian ini berisi tutorial langkah demi langkah terperinci yang menunjukkan cara sederhana untuk memuat aplikasi .NET (Core) ke dalam collectible AssemblyLoadContext
, jalankan titik masuknya, lalu bongkar. Anda dapat menemukan sampel lengkap di https://github.com/dotnet/samples/tree/main/core/tutorials/Unloading.
Membuat AssemblyLoadContext yang dapat dikumpulkan
Dapatkan kelas Anda dari AssemblyLoadContext dan ambil alih metodenya AssemblyLoadContext.Load . Metode itu menyelesaikan referensi ke semua assembly yang merupakan dependensi assembly yang dimuat ke dalamnya AssemblyLoadContext
.
Kode berikut adalah contoh kustom AssemblyLoadContext
paling sederhana :
class TestAssemblyLoadContext : AssemblyLoadContext
{
public TestAssemblyLoadContext() : base(isCollectible: true)
{
}
protected override Assembly? Load(AssemblyName name)
{
return null;
}
}
Seperti yang dapat Anda lihat, metode Load
mengembalikan null
. Itu berarti bahwa semua assembly dependensi dimuat ke dalam konteks default, dan konteks baru hanya berisi assembly yang dimuat secara eksplisit ke dalamnya.
Jika Anda ingin memuat beberapa atau semua dependensi ke AssemblyLoadContext
dalamnya juga, Anda dapat menggunakan AssemblyDependencyResolver
dalam metode .Load
AssemblyDependencyResolver
menyelesaikan nama assembly ke jalur file assembly absolut. Resolver menggunakan file .deps.json dan file assembly di direktori assembly utama yang dimuat ke dalam konteks.
using System.Reflection;
using System.Runtime.Loader;
namespace complex
{
class TestAssemblyLoadContext : AssemblyLoadContext
{
private AssemblyDependencyResolver _resolver;
public TestAssemblyLoadContext(string mainAssemblyToLoadPath) : base(isCollectible: true)
{
_resolver = new AssemblyDependencyResolver(mainAssemblyToLoadPath);
}
protected override Assembly? Load(AssemblyName name)
{
string? assemblyPath = _resolver.ResolveAssemblyToPath(name);
if (assemblyPath != null)
{
return LoadFromAssemblyPath(assemblyPath);
}
return null;
}
}
}
Menggunakan AssemblyLoadContext yang dapat dikumpulkan kustom
Bagian ini mengasumsikan versi yang lebih sederhana dari TestAssemblyLoadContext
sedang digunakan.
Anda dapat membuat instans kustom AssemblyLoadContext
dan memuat assembly ke dalamnya sebagai berikut:
var alc = new TestAssemblyLoadContext();
Assembly a = alc.LoadFromAssemblyPath(assemblyPath);
Untuk setiap assembly yang direferensikan oleh assembly yang dimuat, TestAssemblyLoadContext.Load
metode dipanggil sehingga TestAssemblyLoadContext
dapat memutuskan dari mana assembly mendapatkan assembly. Dalam hal ini, ia kembali null
untuk menunjukkan bahwa itu harus dimuat ke dalam konteks default dari lokasi yang digunakan runtime untuk memuat rakitan secara default.
Sekarang setelah assembly dimuat, Anda dapat menjalankan metode darinya. Menjalankan metode Main
:
var args = new object[1] {new string[] {"Hello"}};
_ = a.EntryPoint?.Invoke(null, args);
Main
Setelah metode kembali, Anda dapat memulai pembongkaran dengan memanggil Unload
metode pada kustom AssemblyLoadContext
atau menghapus referensi yang Anda miliki ke AssemblyLoadContext
:
alc.Unload();
Ini cukup untuk membongkar assembly pengujian. Selanjutnya, Anda akan memasukkan semua ini ke dalam metode terpisah yang tidak dapat dibariskan untuk memastikan bahwa TestAssemblyLoadContext
, , Assembly
dan MethodInfo
( Assembly.EntryPoint
) tidak dapat tetap hidup dengan referensi slot tumpukan (lokal yang diperkenalkan nyata atau JIT). Itu bisa membuat TestAssemblyLoadContext
tetap hidup dan mencegah pembongkaran.
Selain itu, kembalikan referensi yang lemah ke AssemblyLoadContext
sehingga Anda dapat menggunakannya nanti untuk mendeteksi penyelesaian pembongkaran.
[MethodImpl(MethodImplOptions.NoInlining)]
static void ExecuteAndUnload(string assemblyPath, out WeakReference alcWeakRef)
{
var alc = new TestAssemblyLoadContext();
Assembly a = alc.LoadFromAssemblyPath(assemblyPath);
alcWeakRef = new WeakReference(alc, trackResurrection: true);
var args = new object[1] {new string[] {"Hello"}};
_ = a.EntryPoint?.Invoke(null, args);
alc.Unload();
}
Sekarang Anda dapat menjalankan fungsi ini untuk memuat, menjalankan, dan membongkar assembly.
WeakReference testAlcWeakRef;
ExecuteAndUnload("absolute/path/to/your/assembly", out testAlcWeakRef);
Namun, pembongkaran tidak segera selesai. Seperti disebutkan sebelumnya, mengandalkan pengumpul sampah untuk mengumpulkan semua objek dari assembly pengujian. Dalam banyak kasus, tidak perlu menunggu penyelesaian bongkar muatan. Namun, ada kasus di mana berguna untuk mengetahui bahwa bongkar telah selesai. Misalnya, Anda mungkin ingin menghapus file rakitan yang dimuat ke dalam kustom AssemblyLoadContext
dari disk. Dalam kasus seperti itu, cuplikan kode berikut dapat digunakan. Ini memicu pengumpulan sampah dan menunggu finalizer yang tertunda dalam perulangan sampai referensi lemah ke kustom AssemblyLoadContext
diatur ke null
, menunjukkan objek target dikumpulkan. Dalam kebanyakan kasus, hanya satu lewati perulangan yang diperlukan. Namun, untuk kasus yang lebih kompleks di mana objek yang dibuat oleh kode yang AssemblyLoadContext
berjalan di memiliki finalizer, lebih banyak pass mungkin diperlukan.
for (int i = 0; testAlcWeakRef.IsAlive && (i < 10); i++)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
Batasan
Rakitan yang dimuat dalam koleksi AssemblyLoadContext
harus mematuhi pembatasan umum pada rakitan yang dapat dikumpulkan. Batasan berikut juga berlaku:
- Rakitan yang ditulis dalam C++/CLI tidak didukung.
- Kode yang dihasilkan ReadyToRun akan diabaikan.
Kejadian Pembongkaran
Dalam beberapa kasus, mungkin perlu bagi kode yang dimuat ke dalam kustom AssemblyLoadContext
untuk melakukan pembersihan saat pembongkaran dimulai. Misalnya, mungkin perlu menghentikan utas atau membersihkan handel GC yang kuat. Kejadian Unloading
dapat digunakan dalam kasus semacam itu. Anda dapat menghubungkan handler yang melakukan pembersihan yang diperlukan untuk kejadian ini.
Memecahkan masalah pembongkaran
Karena sifat kooperatif dari pembongkaran, mudah untuk melupakan referensi yang mungkin menjaga barang-barang dalam kolektif AssemblyLoadContext
hidup dan mencegah pembongkaran. Berikut adalah ringkasan entitas (beberapa di antaranya tidak berbahaya) yang dapat menyimpan referensi:
- Referensi reguler disimpan dari luar koleksi
AssemblyLoadContext
yang disimpan dalam slot tumpukan atau daftar prosesor (metode lokal, baik yang dibuat secara eksplisit oleh kode pengguna atau secara implisit oleh pengkompilasi just-in-time (JIT), variabel statis, atau handel GC yang kuat (menyematkan), dan secara transitif menunjuk ke:- Assembly yang dimuat ke dalam
AssemblyLoadContext
yang dapat dikumpulkan. - Jenis dari assembly seperti itu.
- Instans jenis dari assembly tersebut.
- Assembly yang dimuat ke dalam
- Utas yang menjalankan kode dari assembly yang dimuat ke dalam
AssemblyLoadContext
yang dapat dikumpulkan. - Instans jenis kustom dan tidak dapat diungkapkan
AssemblyLoadContext
yang dibuat di dalam koleksiAssemblyLoadContext
. - Instans tertunda RegisteredWaitHandle dengan panggilan balik diatur ke metode dalam kustom
AssemblyLoadContext
.
Tip
Referensi objek yang disimpan dalam slot tumpukan atau register prosesor dan yang dapat mencegah pembongkaran AssemblyLoadContext
dapat terjadi dalam situasi berikut:
- Saat hasil panggilan fungsi diteruskan langsung ke fungsi lain, meski tidak ada variabel lokal yang dibuat pengguna.
- Saat pengompilasi JIT menyimpan referensi ke objek yang tersedia di beberapa titik dalam metode.
Masalah pembongkaran debug
Masalah penelusuran kesalahan dengan pembongkaran bisa membosankan. Anda bisa masuk ke situasi di mana Anda tidak tahu apa yang bisa menahan AssemblyLoadContext
hidup, tetapi pembongkaran gagal. Alat terbaik untuk membantu dengan itu adalah WinDbg (atau LLDB di Unix) dengan plugin SOS. Anda perlu menemukan apa yang menjaga LoaderAllocator
yang milik spesifik AssemblyLoadContext
hidup. Plugin SOS memungkinkan Anda melihat objek tumpukan GC, hierarkinya, dan akarnya.
Untuk memuat plugin SOS ke debugger, masukkan salah satu perintah berikut di baris perintah debugger.
Di WinDbg (jika belum dimuat):
.loadby sos coreclr
Dalam LLDB:
plugin load /path/to/libsosplugin.so
Sekarang Anda akan men-debug contoh program yang memiliki masalah dengan pembongkaran. Kode sumber tersedia di bagian Contoh kode sumber. Saat Anda menjalankannya di bawah WinDbg, program memecah ke debugger tepat setelah mencoba memeriksa keberhasilan pembongkaran. Anda kemudian dapat mulai mencari pelakunya.
Tip
Jika Anda men-debug menggunakan LLDB di Unix, perintah SOS dalam contoh berikut tidak memiliki !
di depannya.
!dumpheap -type LoaderAllocator
Perintah ini mencadangkan semua objek dengan nama jenis yang berisi LoaderAllocator
yang ada di tumpukan GC. Berikut contohnya:
Address MT Size
000002b78000ce40 00007ffadc93a288 48
000002b78000ceb0 00007ffadc93a218 24
Statistics:
MT Count TotalSize Class Name
00007ffadc93a218 1 24 System.Reflection.LoaderAllocatorScout
00007ffadc93a288 1 48 System.Reflection.LoaderAllocator
Total 2 objects
Di bagian "Statistik:", periksa MT
(MethodTable
) milik System.Reflection.LoaderAllocator
, yang merupakan objek yang Anda pedulikan. Kemudian, dalam daftar di awal, temukan entri dengan yang cocok dengan MT
yang satu itu, dan dapatkan alamat objek itu sendiri. Dalam hal ini, itu adalah "000002b78000ce40".
Sekarang setelah Anda mengetahui alamat LoaderAllocator
objek, Anda dapat menggunakan perintah lain untuk menemukan akar GC-nya:
!gcroot 0x000002b78000ce40
Perintah ini mencadangkan rantai referensi objek yang mengarah ke instans LoaderAllocator
. Daftar dimulai dengan akar, yang merupakan entitas yang menjaga LoaderAllocator
tetap hidup dan dengan demikian adalah inti dari masalah. Akar dapat berupa slot tumpukan, register prosesor, handel GC, atau variabel statis.
Berikut adalah contoh output gcroot
perintah:
Thread 4ac:
000000cf9499dd20 00007ffa7d0236bc example.Program.Main(System.String[]) [E:\unloadability\example\Program.cs @ 70]
rbp-20: 000000cf9499dd90
-> 000002b78000d328 System.Reflection.RuntimeMethodInfo
-> 000002b78000d1f8 System.RuntimeType+RuntimeTypeCache
-> 000002b78000d1d0 System.RuntimeType
-> 000002b78000ce40 System.Reflection.LoaderAllocator
HandleTable:
000002b7f8a81198 (strong handle)
-> 000002b78000d948 test.Test
-> 000002b78000ce40 System.Reflection.LoaderAllocator
000002b7f8a815f8 (pinned handle)
-> 000002b790001038 System.Object[]
-> 000002b78000d390 example.TestInfo
-> 000002b78000d328 System.Reflection.RuntimeMethodInfo
-> 000002b78000d1f8 System.RuntimeType+RuntimeTypeCache
-> 000002b78000d1d0 System.RuntimeType
-> 000002b78000ce40 System.Reflection.LoaderAllocator
Found 3 roots.
Langkah selanjutnya adalah mencari tahu di mana akar berada sehingga Anda dapat memperbaikinya. Kasus termudah adalah saat akar adalah slot tumpukan atau register prosesor. Dalam hal ini, gcroot
menunjukkan nama fungsi yang bingkainya berisi akar dan utas yang menjalankan fungsi tersebut. Kasus yang sulit adalah saat akar adalah variabel statis atau handel GC.
Dalam contoh sebelumnya, akar pertama adalah lokal dari jenis System.Reflection.RuntimeMethodInfo
yang disimpan dalam bingkai fungsi example.Program.Main(System.String[])
di alamat rbp-20
(rbp
adalah register rbp
prosesor dan -20 adalah offset heksadesimal dari register tersebut).
Akar kedua adalah normal (kuat) GCHandle
yang menyimpan referensi ke instans kelas test.Test
.
Akar ketiga GCHandle
disematkan. Yang satu ini sebenarnya adalah variabel statis, tetapi sayangnya, tidak ada cara untuk mengetahuinya. Statis untuk jenis referensi disimpan dalam array objek terkelola dalam struktur runtime bahasa umum internal.
Kasus lain yang dapat mencegah pembongkaran AssemblyLoadContext
adalah saat utas memiliki bingkai metode dari assembly yang dimuat ke dalam AssemblyLoadContext
tumpukannya. Anda dapat memeriksanya dengan mencadangkan tumpukan panggilan terkelola dari semua utas:
~*e !clrstack
Perintah berarti "berlaku untuk semua utas perintah !clrstack
". Berikut ini adalah output dari perintah tersebut untuk contoh. Sayangnya, LLDB di Unix tidak memiliki cara apa pun untuk menerapkan perintah ke semua utas, jadi Anda harus beralih utas secara manual dan mengulangi clrstack
perintah. Abaikan semua utas di mana debugger mengatakan "Tidak dapat berjalan di tumpukan terkelola".
OS Thread Id: 0x6ba8 (0)
Child SP IP Call Site
0000001fc697d5c8 00007ffb50d9de12 [HelperMethodFrame: 0000001fc697d5c8] System.Diagnostics.Debugger.BreakInternal()
0000001fc697d6d0 00007ffa864765fa System.Diagnostics.Debugger.Break()
0000001fc697d700 00007ffa864736bc example.Program.Main(System.String[]) [E:\unloadability\example\Program.cs @ 70]
0000001fc697d998 00007ffae5fdc1e3 [GCFrame: 0000001fc697d998]
0000001fc697df28 00007ffae5fdc1e3 [GCFrame: 0000001fc697df28]
OS Thread Id: 0x2ae4 (1)
Unable to walk the managed stack. The current thread is likely not a
managed thread. You can run !threads to get a list of managed threads in
the process
Failed to start stack walk: 80070057
OS Thread Id: 0x61a4 (2)
Unable to walk the managed stack. The current thread is likely not a
managed thread. You can run !threads to get a list of managed threads in
the process
Failed to start stack walk: 80070057
OS Thread Id: 0x7fdc (3)
Unable to walk the managed stack. The current thread is likely not a
managed thread. You can run !threads to get a list of managed threads in
the process
Failed to start stack walk: 80070057
OS Thread Id: 0x5390 (4)
Unable to walk the managed stack. The current thread is likely not a
managed thread. You can run !threads to get a list of managed threads in
the process
Failed to start stack walk: 80070057
OS Thread Id: 0x5ec8 (5)
Child SP IP Call Site
0000001fc70ff6e0 00007ffb5437f6e4 [DebuggerU2MCatchHandlerFrame: 0000001fc70ff6e0]
OS Thread Id: 0x4624 (6)
Child SP IP Call Site
GetFrameContext failed: 1
0000000000000000 0000000000000000
OS Thread Id: 0x60bc (7)
Child SP IP Call Site
0000001fc727f158 00007ffb5437fce4 [HelperMethodFrame: 0000001fc727f158] System.Threading.Thread.SleepInternal(Int32)
0000001fc727f260 00007ffb37ea7c2b System.Threading.Thread.Sleep(Int32)
0000001fc727f290 00007ffa865005b3 test.Program.ThreadProc() [E:\unloadability\test\Program.cs @ 17]
0000001fc727f2c0 00007ffb37ea6a5b System.Threading.Thread.ThreadMain_ThreadStart()
0000001fc727f2f0 00007ffadbc4cbe3 System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
0000001fc727f568 00007ffae5fdc1e3 [GCFrame: 0000001fc727f568]
0000001fc727f7f0 00007ffae5fdc1e3 [DebuggerU2MCatchHandlerFrame: 0000001fc727f7f0]
Seperti yang Anda lihat, utas terakhir memiliki test.Program.ThreadProc()
. Ini adalah fungsi dari assembly yang dimuat ke dalam AssemblyLoadContext
, dan sehingga menjaga AssemblyLoadContext
tetap hidup.
Contoh kode sumber
Kode berikut yang berisi masalah unloadability digunakan dalam contoh penelusuran kesalahan sebelumnya.
Program pengujian utama
using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Loader;
namespace example
{
class TestAssemblyLoadContext : AssemblyLoadContext
{
public TestAssemblyLoadContext() : base(true)
{
}
protected override Assembly? Load(AssemblyName name)
{
return null;
}
}
class TestInfo
{
public TestInfo(MethodInfo? mi)
{
_entryPoint = mi;
}
MethodInfo? _entryPoint;
}
class Program
{
static TestInfo? entryPoint;
[MethodImpl(MethodImplOptions.NoInlining)]
static int ExecuteAndUnload(string assemblyPath, out WeakReference testAlcWeakRef, out MethodInfo? testEntryPoint)
{
var alc = new TestAssemblyLoadContext();
testAlcWeakRef = new WeakReference(alc);
Assembly a = alc.LoadFromAssemblyPath(assemblyPath);
if (a == null)
{
testEntryPoint = null;
Console.WriteLine("Loading the test assembly failed");
return -1;
}
var args = new object[1] {new string[] {"Hello"}};
// Issue preventing unloading #1 - we keep MethodInfo of a method
// for an assembly loaded into the TestAssemblyLoadContext in a static variable.
entryPoint = new TestInfo(a.EntryPoint);
testEntryPoint = a.EntryPoint;
var oResult = a.EntryPoint?.Invoke(null, args);
alc.Unload();
return (oResult is int result) ? result : -1;
}
static void Main(string[] args)
{
WeakReference testAlcWeakRef;
// Issue preventing unloading #2 - we keep MethodInfo of a method for an assembly loaded into the TestAssemblyLoadContext in a local variable
MethodInfo? testEntryPoint;
int result = ExecuteAndUnload(@"absolute/path/to/test.dll", out testAlcWeakRef, out testEntryPoint);
for (int i = 0; testAlcWeakRef.IsAlive && (i < 10); i++)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
System.Diagnostics.Debugger.Break();
Console.WriteLine($"Test completed, result={result}, entryPoint: {testEntryPoint} unload success: {!testAlcWeakRef.IsAlive}");
}
}
}
Program yang dimuat ke dalam TestAssemblyLoadContext
Kode berikut mewakili test.dll yang diteruskan ke metode ExecuteAndUnload
dalam program pengujian utama.
using System;
using System.Runtime.InteropServices;
using System.Threading;
namespace test
{
class Test
{
}
class Program
{
public static void ThreadProc()
{
// Issue preventing unloading #4 - a thread running method inside of the TestAssemblyLoadContext at the unload time
Thread.Sleep(Timeout.Infinite);
}
static GCHandle handle;
static int Main(string[] args)
{
// Issue preventing unloading #3 - normal GC handle
handle = GCHandle.Alloc(new Test());
Thread t = new Thread(new ThreadStart(ThreadProc));
t.IsBackground = true;
t.Start();
Console.WriteLine($"Hello from the test: args[0] = {args[0]}");
return 1;
}
}
}