Not
Bu sayfaya erişim yetkilendirme gerektiriyor. Oturum açmayı veya dizinleri değiştirmeyi deneyebilirsiniz.
Bu sayfaya erişim yetkilendirme gerektiriyor. Dizinleri değiştirmeyi deneyebilirsiniz.
Bu makale belirli güvenli olmayan desenler, içerdikleri riskler ve bu riskleri nasıl azaltacakları hakkında ayrıntılı öneriler içerir. Bu yönergeler, C# dilinde güvenli olmayan kod yazan veya gözden geçiren tüm geliştiricileri hedefler. F# ve Visual Basic gibi diğer .NET dilleri bu makalenin kapsamı dışındadır, ancak bazı öneriler bu diller için de geçerli olabilir.
Sözlük
- AVE - Erişim ihlali özel durumu.
- Byref - Yönetilmeyen işaretçiye benzeyen ancak GC tarafından izlenen yönetilen işaretçi (
ref T t). Genellikle nesnelerin veya yığının rastgele bölümlerine işaret eder. Referans, +0 offset ile yönetilen bir işaretçidir. - CVE - Genel olarak açıklanan siber güvenlik açıkları.
- JIT - Tam zamanında derleme yapan derleyici (CoreCLR ve NativeAOT için kullanılan RyuJIT).
- PGO - Profil temelli iyileştirme.
- Yönetilmeyen işaretçi (veya ham işaretçi) - Rastgele bellek konumuna işaret eden ve GC tarafından yönetilmeyen veya izlenmeyen bir işaretçi (
T* p).
Diğer terimler için bkz. .NET Çalışma Zamanı Sözlüğü.
Yaygın güvenilir olmayan desenler
C#, geliştiricilerin çalışma zamanının ve GC'nin iç çalışmalarıyla ilgilenmesi gerekmeyen güvenli bir ortam sağlar. Güvenli olmayan kod, bu güvenlik denetimlerini atlayarak bellek bozulmasına yol açabilecek güvenilir olmayan desenler sunmanızı sağlar. Bu tür desenler bazı senaryolarda yararlı olabilir ancak bunları dikkatli ve yalnızca kesinlikle gerekli olduğunda kullanmanız gerekir. C# ve .NET yalnızca güvenli olmayan kodun sesini doğrulamak için araçlar sağlamaz (çeşitli C/C++ dezenfektanlarının sağlayabilecekleri gibi), GC'ye özgü davranışlar, geleneksel C/C++ geliştiricilerinin aşina olabileceği risklerin ötesinde güvenli olmayan C# içinde ek risklere neden olabilir.
Yönetilen referansların etrafındaki emniyetsiz kod, aşağıdaki muhafazakar varsayımlar göz önünde bulundurularak yazılmalıdır.
- GC, herhangi bir yönergede herhangi bir zamanda herhangi bir yöntemin yürütülmesini kesintiye uğratabilir.
- GC, bellekteki nesneleri taşıyabilir ve izlenen tüm başvuruları güncelleştirebilir.
- GC, referansların artık ne zaman gerekli olmadığını tam olarak bilir.
Klasik yığın bozulması örneği, GC bir nesne başvurusunun izini kaybettiğinde veya geçersiz işaretçileri yığın başvuruları olarak değerlendirdiğinde oluşur. Bu durum genellikle deterministik olmayan kilitlenmelere veya bellek bozulmasına neden olur. Yığın bozulması hatalarının tanılanması ve yeniden üretilmesi özellikle zordur çünkü:
- Bu sorunlar uzun süre gizli kalabilir ve yalnızca ilgisiz bir kod değişikliği veya çalışma zamanı güncelleştirmesinin ardından bildirimde bulunabilir.
- Genellikle, yeniden üretmek için kesin zamanlama gerektirir, bu da GC'nin belirli bir konumda yürütmeyi kesintiye uğratması ve yığın sıkıştırmayı başlatması gibi nadir ve öngörülemeyen bir olaydır.
Sonraki bölümlerde ✔️ DO ve ❌ DON'T önerileriyle yaygın olarak kullanılan güvensiz desenler tanımlanmaktadır.
1. İzlenmeyen yönetilen işaretçiler (Unsafe.AsPointer ve benzerleri)
Yönetilen (izlenen) işaretçiyi güvenli C# içinde yönetilmeyen (izlenmeyen) bir işaretçiye dönüştürmek mümkün değildir. Böyle bir gereksinim ortaya çıktığında, bir Unsafe.AsPointer<T>(T) ifadesinin ek yükünü önlemek için fixed kullanmak cazip olabilir. Bunun için geçerli kullanım örnekleri olsa da, taşınabilen nesneler için izlenmeyen işaretçiler oluşturma riskine neden olur.
Örnek:
unsafe void UnreliableCode(ref int x)
{
int* nativePointer = (int*)Unsafe.AsPointer(ref x);
nativePointer[0] = 42;
}
GC, işaretçi okunduktan hemen sonra yöntemin UnreliableCode yürütülmesini keserse (tarafından xbaşvurulan adres) ve başvurulan nesneyi yeniden konumlandırırsa, GC içinde x depolanan konumu doğru bir şekilde güncelleştirir, ancak hakkında nativePointer hiçbir şey bilmez ve içerdiği değeri güncelleştirmez. Bu noktada, nativePointer'a yazmak, rastgele belleğe yazmak demektir.
unsafe void UnreliableCode(ref int x)
{
int* nativePointer = (int*)Unsafe.AsPointer(ref x);
// <-- GC happens here between the two lines of code and updates `x` to point to a new location.
// However, `nativePointer` still points to the old location as it's not reported to the GC
nativePointer[0] = 42; // Potentially corrupting write, access violation, or other issue.
}
GC yönteminin yürütülmesine devam ettiğinde, x eski konumuna 42 yazar ve bu durum beklenmedik bir özel duruma, genel durum bozulmasına veya erişim ihlali yoluyla işlemin sonlandırılmasına neden olabilir.
Gereken çözüm, GC'nin işlem süresi boyunca hedef başvuruyu yer değiştirmeye karşı korumak için fixed anahtar sözcüğünü ve & adres işlecini kullanmaktır.
unsafe void ReliableCode(ref int x)
{
fixed (int* nativePointer = &x) // `x` cannot be relocated for the duration of this block.
{
nativePointer[0] = 42;
}
}
Recommendations
-
❌Bağımsız değişkenleri her zaman yığınla ayrılmış, sabitlenmiş veya GC tarafından başka bir şekilde yeniden konumlandırılmayan örtük bir anlaşmayla
ref XkullanmayınX. Aynı durum, düz nesneler ve Span'lar için de geçerlidir. Yöntem imzalarında, yaşam süreleri belirgin olmayan arayan tabanlı sözleşmeler tanıtmayın. Bunun yerine ref struct bağımsız değişkeni almayı veya bağımsız değişkeni ham işaretçi türüne (X*) dönüştürmeyi düşünebilirsiniz. - ❌ İşaretçiyi, işaret ettiğiniz özgün nesneden Unsafe.AsPointer<T>(T) daha uzun yaşayabiliyorsa kullanmayın. API'nin belgelerine göre, başvurunun GC tarafından taşınamayacağından emin olmak, Unsafe.AsPointer<T>(T) öğesini çağıran kişiye bağlıdır. Çağıranın bu önkoşulu yerine getirdiğinden, kod gözden geçirenlere açıkça görünür olduğundan emin olun.
- ✔️ Açık kapsamları yönetilmeyen işaretçiler için tanımlamak ve nesnenin her zaman sabitlendiğinden emin olmak üzere yerine
fixedveya Unsafe.AsPointer<T>(T) kapsamlarını kullanın. - ✔️ DO, bir diziyi belirli bir sınıra hizalamanız gerektiğinde byrefs yerine yönetilmeyen işaretçileri (ile
fixed) kullanın. Bu, GC'nin nesnenin yerini değiştirmemesini ve mantığınızın dayanabileceği hizalama varsayımlarını geçersiz kılmasını sağlar.
2. İşaretçileri fixed kapsamı dışına ifşa etme
Sabit anahtar sözcük sabitlenmiş nesneden alınan işaretçi için bir kapsam tanımlasa da, C# bu nesne için sahiplik/yaşam döngüsü korumaları sağlamadığından, bu işaretçinin kapsamdan fixed kaçması ve hatalar vermesi yine de mümkündür.
Tipik bir örnek aşağıdaki kod parçacığıdır:
unsafe int* GetPointerToArray(int[] array)
{
fixed (int* pArray = array)
{
_ptrField = pArray; // Bug!
Method(pArray); // Bug if `Method` allows `pArray` to escape,
// perhaps by assigning it to a field.
return pArray; // Bug!
// And other ways to escape the scope.
}
}
Bu örnekte, diziyi fixed anahtar sözcüğü ile doğru bir şekilde sabitleyerek GC'nin onu fixed bloğu içinde yeniden konumlandıramamasını sağlıyoruz, ancak işaretçi fixed bloğunun dışında açığa çıkıyor. Bu, başvuruyu kaldırmanın tanımsız davranışa neden olacağı bir sarkma işaretçisi oluşturur.
Recommendations
- ✔️ BLOKLARDAKI
fixedişaretçilerin tanımlı kapsamdan çıkmadığından emin olun. - ✔️ DO, C# başvuru yapısı gibi yerleşik kaçış analizi ile güvenli düşük düzey ilkelleri tercih eder. Daha fazla bilgi için bkz . Güvenli bağlam kısıtlaması.
3. Çalışma zamanı ve kitaplıkların iç uygulama ayrıntıları
İç uygulama ayrıntılarına erişmek veya bunlara güvenmek genel olarak kötü bir uygulama olsa da (ve .NET tarafından desteklenmese de), yaygın olarak gözlemlenen belirli durumlara dikkat edilmesi faydalı olabilir. Bu, kod bir iç uygulama ayrıntısına uygun olmayan bir şekilde bağlı olduğunda ters gidebilecek tüm olası şeylerin kapsamlı bir listesi olarak tasarlanmamıştır.
Recommendations
❌ Nesne üst bilgisinin herhangi bir bölümünü değiştirmeyin veya okumayın.
- Nesne üst bilgileri çalışma zamanları arasında farklılık gösterebilir.
- CoreCLR'de nesne başlığına erişebilmek için önce nesnenin sabitlenmesi gerekir; aksi takdirde güvenli erişim sağlanamaz.
- MethodTable işaretçisini değiştirerek hiçbir zaman nesnenin türünü değiştirmeyin.
❌ Nesnenin dolgu alanında hiçbir veri depolamayın. Doldurma içeriğinin daima korunacağını veya doldurmanın varsayılan olarak her zaman sıfırlanacağını varsaymayın.
❌ Sıralı veya açık düzende ilkel öğeler ve yapılar dışında herhangi bir şeyin boyutları ve uzaklıkları hakkında varsayımlarda BULUNMAYIN. İstisnalar bulunabilir, örneğin, GC tanıtıcılarının dahil olduğu özel durumlar gibi.
❌ BCL türlerindeki erişilemez yöntemleri çağırmayın, erişilemez alanlarına erişmeyin veya salt okunur alanları yansıma veya güvensiz kodla değiştirmeyin.
❌ BCL'deki belirli bir nonpublic üyesinin her zaman mevcut olacağını veya belirli bir şekle sahip olacağını VARSAYMAYIN. .NET ekibi, hizmet yayınlarındaki nonpublic API'leri zaman zaman değiştirir veya kaldırır.
❌ Alanları, sabit olduğu varsayıldığı için yansıma veya güvenli olmayan kod kullanarak değiştirmeyin
static readonly. Örneğin, RyuJIT genellikle bunları açık sabitler olarak satır içi olarak sıralar.❌ Bir referansın yeniden konumlandırılamaz olduğunu varsaymayın. Bu kılavuz dize ve UTF-8 (
"..."u8) değişmez değerleri, statik alanlar, RVA alanları, LOH nesneleri vb. için geçerlidir.- Bunlar, bazı çalışma zamanlarında geçerli olabilecek ancak diğerlerinde olmayabilecek çalışma zamanı uygulama ayrıntılarıdır.
- Bu tür nesnelere yönelik yönetilmeyen işaretçiler, derlemelerin kaldırılmasını durdurmayabilir ve işaretçilerin sarkmasına neden olabilir. Doğruluğu sağlamak için
fixedkapsamlarını kullanın.
ReadOnlySpan<int> rva = [1, 2, 4, 4]; int* p = (int*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(rva)); // Bug! The assembly containing the RVA field might be unloaded at this point // and `p` becomes a dangling pointer. int value = p[0]; // Access violation or other issue.❌ Belirli bir çalışma zamanının uygulama ayrıntılarına dayalı kod yazmayın.
4. Geçersiz yönetilen işaretçiler (hiçbir zaman başvurulmasalar bile)
Belirli kod kategorileri işaretçi işlemeye ve aritmetike dayalı hale gelir ve bu tür kodlar genellikle yönetilmeyen işaretçiler () ve yönetilen işaretçiler (T* pref T p) kullanma arasında bir seçime sahiptir.
Bu işaretçiler, yönetilmeyen işaretçiler üzerindeki işleçler p++ ve yönetilen işaretçiler üzerindeki Unsafe yöntemler p = ref Unsafe.Add(ref p, 1) aracılığıyla keyfi olarak işlenebilir. Her ikisi de "güvenli olmayan kod" olarak kabul edilir ve her ikisiyle de güvenilir olmayan desenler oluşturmak mümkündür. Ancak, belirli algoritmalar için yönetilen işaretçileri düzenlerken yanlışlıkla GC güvenli olmayan desenler oluşturmak daha kolay olabilir. Yönetilmeyen işaretçiler GC tarafından izlenmediğinden, içerdikleri değer yalnızca geliştiricinin kodu tarafından başvurulduğunda geçerlidir. Buna karşılık, yönetilen bir işaretçinin değeri, geliştiricinin kodu tarafından başvurulduğunda olduğu kadar, GC tarafından incelendiğinde de geçerlidir. Bu nedenle, geliştirici, derecelendirilmediği sürece sonuç doğurmadan geçersiz yönetilmeyen işaretçiler oluşturabilir. Ancak, geçersiz yönetilen işaretçiler oluşturmak bir hatadır. Örnek:
unsafe void UnmanagedPointers(int[] array)
{
fixed (int* p = array)
{
int* invalidPtr = p - 1000;
// invalidPtr is pointing to an undefined location in memory
// it's ok as long as it's not dereferenced.
int* validPtr = invalidPtr + 1000; // Returning back to the original location
*validPtr = 42; // OK
}
}
Ancak, byrefs (yönetilen işaretçiler) kullanan benzer kod geçersizdir.
void ManagedPointers_Incorrect(int[] array)
{
ref int invalidPtr = ref Unsafe.Add(ref array[0], -1000); // Already a bug!
ref int validPtr = ref Unsafe.Add(ref invalidPtr, 1000);
validPtr = 42; // possibly corrupting write
}
Burada yönetilen uygulama küçük sabitleme ek yükünden kurtulsa da, geçersizdir çünkü invalidPtrişaretçi, array[0] adresini GC güncellerken harici bir işaretçiye dönüşebilir.
Bu tür hatalar incedir ve hatta .NET bunlarla geliştirme sırasında sorun yaşamıştır.
Recommendations
-
❌ Başvurulmasalar veya hiç yürütülmemiş kod yollarının içinde bulunsalar bile geçersiz yönetilen işaretçiler oluşturmayın.
- Geçerli bir yönetilen işaretçinin ne olduğu hakkında daha fazla bilgi için bkz. ECMA-335, Sn. II.14.4.2 Yönetilen işaretçiler; ve ECMA-335 CLI Belirtimi Eki, Sn. II.14.4.2.
- ✔️ Algoritma bu tür işlemeler gerektiriyorsa, sabitlenmiş yönetilmeyen işaretçileri doğru kullanın.
5. Yeniden yorum benzeri tür atamaları
Her türlü yapıdan sınıfa veya sınıftan yapıya dönüştürme tanımsız bir davranış olsa da, yapıdan yapıya veya sınıftan sınıfa dönüştürmelerle güvenilir olmayan desenlerle karşılaşmak da mümkündür. Güvenilir olmayan bir desenin tipik bir örneği aşağıdaki koddur:
struct S1
{
string a;
nint b;
}
struct S2
{
string a;
string b;
}
S1 s1 = ...
S2 s2 = Unsafe.As<S1, S2>(ref s1); // Bug! A random nint value becomes a reference reported to the GC.
Düzen benzer olsa bile GC başvuruları (alanlar) söz konusu olduğunda dikkatli olmanız gerekir.
Recommendations
- ❌ Yapıları sınıflara dönüştürmeyin veya tam tersi.
-
❌ Yapıdan yapıya veya sınıftan sınıfa dönüştürme işlemleri için
Unsafe.Askullanmayın, atamanın yasal olduğundan kesinlikle emin değilseniz. Daha fazla bilgi için bakın.Unsafe.As - ✔️ DO, bu tür dönüştürmeler için alan bazında daha güvenli kopyalamayı, AutoMapper gibi dış kitaplıkları veya Kaynak Oluşturucuları tercih eder.
- ✔️ DO,
Unsafe.BitCastbazı temel kullanım denetimleri sağladığı içinUnsafe.AsyerineBitCasttercih eder. Bu denetimlerin tam doğruluk garantileri sağlamadığını, yaniBitCasthala güvenli olmayan bir API olarak kabul edildiğini unutmayın.
6. GC referanslarında Yazma Engellemesini atlamak ve atomik olmayan operasyonlar
Normalde, GC referanslarının her türlü yazma veya okuma işlemi her zaman atomiktir. Ayrıca, potansiyel bir yığın konumuna GC referansı (veya GC alanları içeren bir yapıya byref) atama girişimleri, GC'nin nesneler arasındaki yeni bağlantıları fark etmesini sağlayan Yazma Engellemesi'ni geçer. Ancak, güvenli olmayan kod bu garantileri atlamamıza ve güvenilir olmayan desenler sunmamıza olanak tanır. Örnek:
unsafe void InvalidCode1(object[] arr1, object[] arr2)
{
fixed (object* p1 = arr1)
fixed (object* p2 = arr2)
{
nint* ptr1 = (nint*)p1;
nint* ptr2 = (nint*)p2;
// Bug! We're assigning a GC pointer to a heap location
// without going through the Write Barrier.
// Moreover, we also bypass array covariance checks.
*ptr1 = *ptr2;
}
}
Benzer şekilde, yönetilen işaretçilerle aşağıdaki kod da güvenilir değildir:
struct StructWithGcFields
{
object a;
int b;
}
void InvalidCode2(ref StructWithGcFields dst, ref StructWithGcFields src)
{
// It's already a bad idea to cast a struct with GC fields to `ref byte`, etc.
ref byte dstBytes = ref Unsafe.As<StructWithGcFields, byte>(ref dst);
ref byte srcBytes = ref Unsafe.As<StructWithGcFields, byte>(ref src);
// Bug! Bypasses the Write Barrier. Also, non-atomic writes/reads for GC references.
Unsafe.CopyBlockUnaligned(
ref dstBytes, ref srcBytes, (uint)Unsafe.SizeOf<StructWithGcFields>());
// Bug! Same as above.
Vector128.LoadUnsafe(ref srcBytes).StoreUnsafe(ref dstBytes);
}
Recommendations
- ❌ GC başvurularında atomik olmayan işlemler kullanmayın (örneğin, SIMD işlemleri genellikle bunları sağlamaz).
- ❌ GC başvurularını yığın konumlarında (Yazma Engeli'ni atarak) depolamak için yönetilmeyen işaretçiler kullanmayın.
7. Nesne yaşam süreleri hakkındaki varsayımlar (sonlandırıcılar, GC.KeepAlive)
GC'nin perspektifinden nesnelerin ömrü hakkında varsayımlarda bulunmaktan kaçının. Özellikle, bir nesne olmayabilirken hala hayatta olduğunu varsaymayın. Nesne yaşam süreleri farklı çalışma zamanlarında ve hatta aynı yöntemin farklı Katmanları arasında (RyuJIT'de Katman0 ve Katman1) farklılık gösterebilir. Sonlandırıcılar, bu tür varsayımların yanlış olabileceği yaygın bir senaryodur.
public class MyClassWithBadCode
{
public IntPtr _handle;
public void DoWork() => DoSomeWork(_handle); // A use-after-free bug!
~MyClassWithBadCode() => DestroyHandle(_handle);
}
// Example usage:
var obj = new MyClassWithBadCode()
obj.DoWork();
Bu örnekte, DestroyHandle tamamlanmadan önce DoWork veya başlamadan önce çağrılabilir.
Bu nedenle, gibi thisnesnelerin yöntemin sonuna kadar canlı kalacağını varsaymamak önemlidir.
void DoWork()
{
// A pseudo-code of what might happen under the hood:
IntPtr reg = this._handle;
// 'this' object is no longer alive at this point.
// <-- GC interrupts here, collects the 'this' object, and triggers its finalizer.
// DestroyHandle(_handle) is called.
// Bug! 'reg' is now a dangling pointer.
DoSomeWork(reg);
// You can resolve the issue and force 'this' to be kept alive (thus ensuring the
// finalizer will not run) by uncommenting the line below:
// GC.KeepAlive(this);
}
Bu nedenle, GC.KeepAlive(Object) veya SafeHandle kullanarak nesnelerin ömrünü açıkça uzatmanız önerilir.
Bu sorunun bir diğer klasik örneği de API'dir Marshal.GetFunctionPointerForDelegate<TDelegate>(TDelegate) :
var callback = new NativeCallback(OnCallback);
// Convert delegate to function pointer
IntPtr fnPtr = Marshal.GetFunctionPointerForDelegate(callback);
// Bug! The delegate might be collected by the GC here.
// It should be kept alive until the native code is done with it.
RegisterCallback(fnPtr);
Recommendations
-
❌ Nesne yaşam süreleri hakkında varsayımlarda bulunmayın. Örneğin,
this'ün her zaman yöntemin sonuna kadar canlı olduğunu asla varsaymayın. - ✔️ DO SafeHandle ifadesini yerel kaynakları yönetmek için kullanın.
- ✔️ GEREKTIĞINDE nesnelerin ömrünü uzatmak için DO kullanın GC.KeepAlive(Object) .
8. Yerel değişkenlere çapraz iş parçacığı erişimi
Yerel değişkenlere farklı bir iş parçacığından erişmek genellikle kötü bir uygulama olarak kabul edilir. Ancak, .NET Bellek Modeli'nde açıklandığı gibi yönetilen başvurular söz konusu olduğunda açıkça tanımsız bir davranış haline gelir.
Örnek: GC başvurularını içeren bir yapı, no-GC bölgesinde, iş parçacığına güvenli olmayan bir şekilde sıfırlanabilir veya üzerine yazılabilir. Bu sırada başka bir iş parçacığı bunu okurken tanımsız davranış ortaya çıkabilir.
Recommendations
- ❌ İş parçacıkları arasında yerel ayarlara erişmeyin (özellikle GC başvuruları içeriyorsa).
- ✔️ BUNUN yerine yığın veya yönetilmeyen bellek (örneğin, NativeMemory.Alloc) kullanın.
9. Güvensiz sınır kontrolünün kaldırılması
C# dilinde tüm idiomatic bellek erişimleri varsayılan olarak sınır denetimlerini içerir. JIT derleyicisi, aşağıdaki örnekte olduğu gibi gereksiz olduğunu kanıtlayabilirse bu denetimleri kaldırabilir.
int SumAllElements(int[] array)
{
int sum = 0;
for (int i = 0; i < array.Length; i++)
{
// The JIT knows that within this loop body, i >= 0 and i < array.Length.
// The JIT can reason that its own bounds check would be duplicative and
// unnecessary, so it opts not to emit the bounds check into the final
// generated code.
sum += array[i];
}
}
JIT bu tür desenleri tanıma konusunda sürekli geliştiriliyor olsa da, denetimleri yerinde bıraktığı ve sık erişimli koddaki performansı etkilediği senaryolar da vardır. Böyle durumlarda, riskleri tam olarak anlamadan veya performans avantajlarını doğru bir şekilde değerlendirmeden bu denetimleri el ile kaldırmak için güvenli olmayan kod kullanmak isteyebilirsiniz.
Örneğin aşağıdaki yöntemi göz önünde bulundurun.
int FetchAnElement(int[] array, int index)
{
return array[index];
}
JIT, index her zaman array sınırları içinde yasal olarak bulunduğunu kanıtlayamazsa, yöntemi aşağıdakine benzer bir görünüme sahip olacak şekilde yeniden yazar.
int FetchAnElement_AsJitted(int[] array, int index)
{
if (index < 0 || index >= array.Length)
throw new IndexOutOfBoundsException();
return array.GetElementAt(index);
}
Bu kontrolün sık erişimli kodda neden olabileceği ek yükü azaltmak için, güvensiz eşdeğer API'leri (Unsafe ve MemoryMarshal) kullanmayı düşünebilirsiniz.
int FetchAnElement_Unsafe1(int[] array, int index)
{
// DANGER: The access below is not bounds-checked and could cause an access violation.
return Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(array), index);
}
Veya sabitleme ve ham işaretçileri kullanın:
unsafe int FetchAnElement_Unsafe2(int[] array, int index)
{
fixed (int* pArray = array)
{
// DANGER: The access below is not bounds-checked and could cause an access violation.
return pArray[index];
}
}
index
array sınırlarının dışındaysa, rastgele kilitlenmelere veya durum bozulmasına neden olabilir.
Bu tür güvenli olmayan dönüşümler çok sık erişimli yollarda performans avantajlarına sahip olabilir, ancak bu avantajlar genellikle geçicidir çünkü her .NET sürümü, güvenli olduğunda JIT'nin gereksiz sınır denetimlerini ortadan kaldırma yeteneğini geliştirir.
Recommendations
- ✔️ DO, .NET'in en son sürümünün sınır denetimini yine de ortadan kaldıramadığını doğrulayın. Bunu yapabilirse, güvenli kodu kullanarak yeniden yazın. Aksi takdirde RyuJIT'e bir sorun bildirin. Bu izleme sorununu iyi bir başlangıç noktası olarak kullanın.
- ✔️ Gerçek dünya performans etkisini ölçün. Performans kazancı önemsizse veya kodun önemsiz bir mikrobenchmark dışında etkin olduğu kanıtlanmamışsa, güvenli kodu kullanarak yeniden yazın.
- ✔️ JIT'ye döngülerden önce el ile sınır denetimleri yapmak ve alanları yerel değişkenlere kaydetmek gibi ek ipucu verin, çünkü .NET Bellek Modeli bazı senaryolarda JIT'nin sınır denetimlerini kaldırmasını ihtiyatlı bir şekilde engelleyebilir.
- ✔️ Güvenli olmayan kod hala gerekliyse, kodu
Debug.Assertsınır kontrolleri ile koruyun. Aşağıdaki örneği göz önünde bulundurun.
Debug.Assert(array is not null);
Debug.Assert((index >= 0) && (index < array.Length));
// Unsafe code here
Hatta bu denetimleri yeniden kullanılabilir yardımcı yöntemler halinde yeniden düzenleyebilirsiniz.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static T UnsafeGetElementAt<T>(this T[] array, int index)
{
Debug.Assert(array is not null);
Debug.Assert((index >= 0) && (index < array.Length));
return Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(array), index);
}
öğesinin Debug.Assert eklenmesi Yayın derlemeleri için herhangi bir ses denetimi sağlamaz, ancak Hata ayıklama derlemelerindeki olası hataları algılamaya yardımcı olabilir.
10. Bellek erişimi birleştirme
Performansı artırmak için bellek erişimlerini birleştirme amacıyla güvenli olmayan kod kullanmak isteyebilirsiniz.
Klasik bir örnek, bir char dizisine yazmak "False" için aşağıdaki koddur:
// Naive implementation
static void WriteToDestination_Safe(char[] dst)
{
if (dst.Length < 5) { throw new ArgumentException(); }
dst[0] = 'F';
dst[1] = 'a';
dst[2] = 'l';
dst[3] = 's';
dst[4] = 'e';
}
// Unsafe coalesced implementation
static void WriteToDestination_Unsafe(char[] destination)
{
Span<char> dstSpan = destination;
if (dstSpan.Length < 5) { throw new ArgumentException(); }
ulong fals_val = BitConverter.IsLittleEndian ? 0x0073006C00610046ul : 0x00460061006C0073ul;
MemoryMarshal.Write(MemoryMarshal.AsBytes(dstSpan.Slice(0, 4)), in fals_val); // Write "Fals" (4 chars)
dstSpan[4] = 'e'; // Write "e" (1 char)
}
.NET'in önceki sürümlerinde, MemoryMarshal kullanılan güvensiz sürüm, basit güvenli sürümden ölçülebilir derecede daha hızlıydı. Ancak, .NET'in modern sürümleri, her iki durumda da eşdeğer kodgen üreten çok geliştirilmiş bir JIT içerir. .NET 10 itibarıyla x64 codegen şöyledir:
; WriteToDestination_Safe
cmp eax, 5
jl THROW_NEW_ARGUMENTEXCEPTION
mov rax, 0x73006C00610046
mov qword ptr [rdi+0x10], rax
mov word ptr [rdi+0x18], 101
; WriteToDestination_Unsafe
cmp edi, 5
jl THROW_NEW_ARGUMENTEXCEPTION
mov rdi, 0x73006C00610046
mov qword ptr [rax], rdi
mov word ptr [rax+0x08], 101
Kodun daha basit ve daha okunabilir bir sürümü vardır:
"False".CopyTo(dst);
.NET 10 itibarıyla, bu çağrı yukarıdaki gibi aynı kodgeni üretir. Hatta ek bir avantajı vardır: JIT'ye, katı öğe başına yazmaların atomik olması gerekmediğine dair ipuçları sağlar. JIT, bu ipucunu diğer bağlamsal bilgilerle birleştirerek burada anlatılanların ötesinde daha da fazla iyileştirme sağlayabilir.
Recommendations
- ✔️ Bellek erişimi birleştirilmesi için güvenli olmayan kod yerine ilkesel olarak güvenli kodu tercih edin.
- Verileri kopyalamak için
Span<T>.CopyToveSpan<T>.TryCopyTotercih edin. - Verileri karşılaştırmak için
String.EqualsveSpan<T>.SequenceEqual'yi tercih edin (hattaStringComparer.OrdinalIgnoreCasekullanırken bile). - Veri doldurmak için
Span<T>.Fillve verileri temizlemek içinSpan<T>.Cleartercih edin. - Öğe başına veya alan başına yazma/okuma işlemleri JIT tarafından otomatik olarak bir araya getirilebilir.
- Verileri kopyalamak için
- ✔️ Idiomatic kod yazarsanız ve beklendiği gibi iyileştirilmediğini gözlemlerseniz dotnet/runtime ile ilgili bir sorun oluşturun.
- ❌ Yanlış hizalanmış bellek erişimi risklerinden, bölünmezlik garantilerinden veya ilişkili performans avantajlarından emin değilseniz bellek erişimlerini el ile birleştirmeyin.
11. Hizalanmamış bellek erişimi
Bellek erişimi birleştirme bölümünde açıklandığı üzere bu işlemler, genellikle açık veya örtük olarak yanlış hizalanmış okuma/yazma işlemleriyle sonuçlanır. Bu genellikle ciddi sorunlara neden olmasa da (önbellek ve sayfa sınırlarının aşması nedeniyle olası performans cezaları dışında), yine de bazı gerçek riskler oluşturur.
Örneğin, bir dizinin iki öğesini aynı anda temizlediğiniz senaryoyu göz önünde bulundurun:
uint[] arr = _arr;
arr[i + 0] = 0;
arr[i + 1] = 0;
Bu konumlardaki önceki değerlerin her ikisi de uint.MaxValue (0xFFFFFFFF) olduğunu düşünelim.
.NET Bellek Modeli, her iki yazma işleminin atomik olduğunu garanti eder, bu yüzden işlemdeki diğer tüm iş parçacıkları sadece yeni değeri 0 veya eski değeri 0xFFFFFFFF gözlemler, hiçbir zaman 0xFFFF0000 şeklinde "yırtık" değerler gözlemez.
Ancak, sınır denetimini atlamak için aşağıdaki güvenli olmayan kodun kullanıldığını ve tek bir 64 bit deposu olan her iki öğenin de sıfır olduğunu varsayalım:
ref uint p = ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(arr), i);
Unsafe.WriteUnaligned<ulong>(ref Unsafe.As<uint, byte>(ref p), 0UL);
Bu kod, bölünmezlik garantisini kaldırmanın yan etkisine sahiptir. Yırtılmış değerler, diğer iş parçacıkları tarafından görülebilir ve bu da tanımsız davranışa yol açabilir. Böyle bir birleşimli yazmanın atomik olması için, belleğin yazma boyutuna (bu örnekte 8 bayt) hizalanması gerekir. İşlemden önce belleği el ile hizalamayı denerseniz, GC'nin sabitlenmemişse diziyi istediğiniz zaman yeniden yer değiştirebileceğini (ve hizalamasını etkili bir şekilde değiştirebileceğini) göz önünde bulundurmanız gerekir. Diğer ayrıntılar için .NET Bellek Modeli belgelerine bakın.
Hizalanmamış bellek erişiminin bir diğer riski de belirli senaryolarda uygulama kilitlenme olasılığıdır. Bazı .NET çalışma zamanları yanlış hizalanmış erişimleri düzeltmek için işletim sistemine bağlı olsa da, bazı platformlarda yanlış hizalanmış erişim DataMisalignedException (veya SEHException) ile sonuçlanabilir. Örneklerden bazıları şunlardır:
-
Interlockedbazı platformlarda yanlış hizalanmış bellek üzerinde işlemler (örnek). - ARM'de yanlış hizalanmış kayan nokta işlemleri.
- Belirli hizalama gereksinimleriyle özel cihaz belleğine erişme (.NET tarafından gerçekten desteklenmez).
Recommendations
- ❌ Kilitlenmeyen algoritmalarda ve bölünmezliğin önemli olduğu diğer senaryolarda hizalanmamış bellek erişimlerini kullanmayın.
- ✔️ DO, gerekirse verileri el ile hizalayın, ancak GC nesneleri herhangi bir zamanda yeniden yerleştirebilir ve hizalamayı dinamik olarak değiştirebilir. Bu özellikle SIMD'deki çeşitli
StoreAligned/LoadAlignedAPI'ler için önemlidir. - ✔️ Verilerin yanlış hizalanmış olabileceği durumlarda, hizalanmış olanlar yerine Unsafe.ReadUnaligned/Unsafe.WriteUnaligned gibi açık bir şekilde hizalanmamış Okuma/Yazma API'lerini kullanın.
- ✔️ Çeşitli bellek işleme API'lerinin, Span<T>.CopyTo(Span<T>) gibi, atomiklik garantisi sağlamadığını unutmayın.
- ✔️ Bölünmezlik garantileri hakkında daha fazla bilgi için .NET Bellek Modeli belgelerine (referanslara bakın) başvurun.
- ✔️ Do, bazı platformlar hizalanmamış bellek erişimleri için önemli bir performans cezası uyguladığı için tüm hedef platformlarınızda performansı ölçer. Bu platformlarda saf kodun akıllı koddan daha iyi performans sergilediğini görebilirsiniz.
- ✔️ DO, hizalanmamış bellek erişiminin özel duruma yol açabileceği senaryolar ve platformlar bulunabileceğini unutmayın.
12. Doldurmalar veya bölünebilir olmayan üyelerle yapıların ikili (de)seri hale getirilmesi
Bayt dizilerine veya bayt dizilerinden yapıları kopyalamak veya okumak için serileştirme benzeri çeşitli API'ler kullanırken dikkatli olun.
Yapı doldurmalar veya kesilebilir olmayan üyeler (örneğin, bool veya GC alanları) içeriyorsa, Fill, CopyTo ve SequenceEqual gibi klasik güvensiz bellek işlemleri hassas verileri yanlışlıkla yığından doldurmalara kopyalayabilir veya karşılaştırmalar sırasında atık verileri önemli olarak değerlendirebilir, bu da nadiren yeniden üretilebilir hatalara neden olabilir. Yaygın bir anti-desen şöyle görünebilir:
T UnreliableDeserialization<TObject>(ReadOnlySpan<byte> data) where TObject : unmanaged
{
return MemoryMarshal.Read<TObject>(data); // or Unsafe.ReadUnaligned
// BUG! TObject : unmanaged doesn't guarantee that TObject is blittable and contains no paddings.
}
Tek doğru yaklaşım, her bir TObject girişi için özelleştirilmiş veya Yansıma, Kod Üreticiler veya serileştirme kitaplıkları ile genelleştirilmiş alan bazlı yüklemeler/depolar kullanmaktır.
Recommendations
-
❌ Yapıları doldurmalarla veya bölünemez üyelerle kopyalamak/yüklemek/karşılaştırmak için güvenli olmayan kod kullanmayın. Güvenilmeyen girişlerden gelen yükler,
boolveyadecimalgibi temel türler için bile sorunludur. Aynı zamanda mağazalar, bir veri yapısının boşluklarında veya dolgu alanlarında bulunan yığınlardan hassas bilgileri yanlışlıkla seri hale getirebilir. -
❌Genel bir türün üzerinde bit düzeyinde işlemler gerçekleştirmenin güvenli olduğundan emin olmak için kısıtlama,
T : unmanagedveya benzer API'lere güvenmeyinRuntimeHelpers.IsReferenceOrContainsReferences. Bu yönergeleri yazarken, belirli bir tür üzerinde rastgele bit düzeyinde işlemler gerçekleştirmenin yasal olup olmadığını belirlemenin güvenilir bir programlı yolu yoktur.- Böyle bir bit düzeyinde işleme gerçekleştirmeniz gerekiyorsa, bunu yalnızca bu önceden tanımlanmış sabit tür listesi üzerinde yapın ve geçerli makinenin bayt sıralamasına dikkat edin.
- İlkel tam sayı türleri
Byte,SByte,Int16,UInt16,Int32,UInt32,Int64veUInt64; - Yukarıdaki ilkel integral türlerinden biri tarafından desteklenen bir
Enum; -
Char,Int128,UInt128,Half, ,Single,Double,IntPtr.UIntPtr
- İlkel tam sayı türleri
- Böyle bir bit düzeyinde işleme gerçekleştirmeniz gerekiyorsa, bunu yalnızca bu önceden tanımlanmış sabit tür listesi üzerinde yapın ve geçerli makinenin bayt sıralamasına dikkat edin.
- ✔️ Bunun yerine, alana göre yükleme ve depolama (de)serileştirme kullanın. Serileştirme ve ters serileştirme için popüler ve güvenli kitaplıkları kullanmayı göz önünde bulundurun.
13. Null yönetilen işaretçiler
Genellikle byrefs (yönetilen işaretçiler) nadiren null olur ve bugünden itibaren null byref oluşturmanın tek güvenli yolu ref struct ile başlatmaktır default. Ardından tüm ref alanları null yönetilen işaretçilerdir:
RefStructWithRefField s = default;
ref byte nullRef = ref s.refFld;
Ancak null byrefs oluşturmanın birkaç güvenli olmayan yolu vardır. Bazı örnekler şunlardır:
// Null byref by calling Unsafe.NullRef directly:
ref object obj = ref Unsafe.NullRef<object>();
// Null byref by turning a null unmanaged pointer into a null managed pointer:
ref object obj = ref Unsafe.AsRef<object>((void*)0);
Bellek güvenliği sorunlarına giriş riski düşüktür ve null byref'i kaldırma girişimleri iyi tanımlanmış bir NullReferenceException'a yol açar. Ancak, C # derleyicisi bir byref başvurusunun dereferans edilmesinin her zaman başarılı olacağını varsayar ve gözlemlenebilir bir yan etki oluşturmadığını kabul eder. Bu nedenle, sonuç olarak elde edilen değer hemen yok ediliyorsa herhangi bir indirekt erişimi ortadan kaldırmak yasal bir iyileştirmedir. .NET'te kitaplık kodunun yan etkiyi tetikleyen başvurunun hatalı şekilde kullanıldığı ve C# derleyicisinin amaçlanan mantığı etkili bir şekilde kısa devreden çıkardığının farkında olmadığı bir hata örneği için dotnet/runtime#98681 (ve bu ilgili açıklama) konusuna bakın.
Recommendations
- ❌ Gerekli değilse C# içinde null byrefs oluşturmayın. Bunun yerine normal yönetilen referansları, Null Nesne Deseni veya boş sahaları kullanmayı göz önünde bulundurun.
- ❌ Optimizasyon nedeniyle kaldırılabileceğinden ve potansiyel hatalara yol açabileceğinden, byref dereference sonucunu atmayın.
14. stackalloc
stackalloc geçmişe dönük olarak yığında küçük, kaçışsız diziler oluşturmak ve GC basıncını azaltmak için kullanılmıştır. Gelecekte JIT'nin Kaçış Analizi, kaçışsız GC ayırmalarının yığın nesnelerine dönüşmesini optimize etmeye başlayabilir ve bu da stackalloc 'yi gereksiz hale getirebilir. O zamana kadar stackalloc, yığında küçük arabellekler ayırmada yararlıdır. Daha büyük veya kaçış arabellekleri için genellikle ArrayPool<T> ile birleştirilir.
Recommendations
✔️ DO her zaman
stackallocifadenin sol tarafındaki içineReadOnlySpan<T>/Span<T>kullanarak sınır denetimleri sağlar:// Good: Span<int> s = stackalloc int[10]; s[2] = 0; // Bounds check is eliminated by JIT for this write. s[42] = 0; // IndexOutOfRangeException is thrown // Bad: int* s = stackalloc int[10]; s[2] = 0; s[42] = 0; // Out of bounds write, undefined behavior.❌ İç döngüleri KULLANMAYIN
stackalloc. Yöntem geri dönene kadar yığın alanı geri kazanılmaz, bu nedenle bir döngünün içinestackallocdahil etmek yığın taşması nedeniyle işlemin sonlanmasına yol açabilir.❌ için
stackallocbüyük uzunluklar kullanmayın. Örneğin, 1024 bayt makul bir üst sınır olarak kabul edilebilir.✔️ Değişkenlerin
stackallocuzunluk olarak kullanım aralığını kontrol edin.void ProblematicCode(int length) { Span<int> s = stackalloc int[length]; // Bad practice: check the range of `length`! Consume(s); }Sabit sürüm:
void BetterCode(int length) { // The "throw if length < 0" check below is important, as attempting to stackalloc a negative // length will result in process termination. ArgumentOutOfRangeException.ThrowIfLessThan(length, 0, nameof(length)); Span<int> s = length <= 256 ? stackalloc int[length] : new int[length]; // Or: // Span<int> s = length <= 256 ? stackalloc int[256] : new int[length]; // Which performs a faster zeroing of the stackalloc, but potentially consumes more stack space. Consume(s); }✔️ DO, mümkün olduğunda el ile bellek yönetimini önlemek için koleksiyon sabit değerleri (
Span<int> s = [1, 2, 3];),params Span<T>, ve satır içi diziler gibi modern C# özelliklerini kullanın.
15. Sabit boyutlu arabellekler
Sabit boyutlu arabellekler, diğer dillerden veya platformlardan veri kaynaklarıyla birlikte çalışma senaryoları için kullanışlıdır. Daha sonra daha güvenli ve daha kullanışlı satır içi dizilerle değiştirildiler.
Sabit boyutlu bir arabellek örneği (unsafe bağlam gerektiren) aşağıdaki kod parçacığıdır:
public struct MyStruct
{
public unsafe fixed byte data[8];
// Some other fields
}
MyStruct m = new();
ms.data[10] = 0; // Out-of-bounds write, undefined behavior.
Modern ve daha güvenli bir alternatif satır içi dizilerdir:
[System.Runtime.CompilerServices.InlineArray(8)]
public struct Buffer
{
private int _element0; // can be generic
}
public struct MyStruct
{
public Buffer buffer;
// Some other fields
}
MyStruct ms = new();
ms.buffer[i] = 0; // Runtime performs a bounds check on index 'i'; could throw IndexOutOfRangeException.
ms.buffer[7] = 0; // Bounds check elided; index is known to be in range.
ms.buffer[10] = 0; // Compiler knows this is out of range and produces compiler error CS9166.
Her zaman varsayılan olarak sıfırdan başlatılan satır içi diziler yerine sabit boyutlu arabelleklerden kaçınmanın bir diğer nedeni de, sabit boyutlu arabelleklerin belirli senaryolarda sıfırlanmamış içeriklere sahip olmasıdır.
Recommendations
- ✔️ MÜMKÜN olduğunda sabit boyutlu arabellekleri satır içi dizilerle veya IL marshalling öznitelikleriyle değiştirmeyi tercih edin.
16. Bitişik verileri işaretçiler ve uzunluklar olarak geçirme (veya sıfır sonlandırmaya bağlı kalma)
Bitişik verilere yönelik yönetilmeyen veya yönetilen işaretçileri kabul eden API'ler tanımlamaktan kaçının. Bunun yerine Span<T> veya ReadOnlySpan<T> kullanın.
// Poor API designs:
void Consume(ref byte data, int length);
void Consume(byte* data, int length);
void Consume(byte* data); // zero-terminated
void Consume(ref byte data); // zero-terminated
// Better API designs:
void Consume(Span<byte> data);
void Consume(Memory<byte> data);
void Consume(byte[] data);
void Consume(byte[] data, int offset, int length);
Sıfır sonlandırma özellikle risklidir çünkü tüm arabellekler sıfırla sonlandırılmaz ve herhangi bir sıfır sonlandırıcıyı aşarak okumak, bilgilerin açığa çıkmasına, veri bozulmasına veya erişim ihlali yoluyla işlem sonlandırmasına neden olabilir.
Recommendations
❌ Bağımsız değişkenler arabellekleri temsil etmek üzere tasarlandığında, bağımsız değişkenleri işaretçi türleri (yönetilmeyen işaretçiler
T*veya yönetilen işaretçilerref T) olan yöntemleri kullanıma sunmayın.Span<T>veyaReadOnlySpan<T>gibi güvenli arabellek türlerini kullanın.❌ Tüm çağıranların girişleri yığın üzerinde ayırmasını gerektiren byref bağımsız değişkenleri için örtük sözleşmeler yapmaktan kaçının. Böyle bir sözleşme gerekiyorsa, bunun yerine başvuru yapısını kullanmayı göz önünde bulundurun.
❌ Senaryo bunun geçerli bir varsayım olduğunu açıkça belgelemedikçe arabelleklerin sıfır sonlandırıldığını varsaymayın. Örneğin, .NET,
stringörneklerinin ve"..."u8sabitlerinin null ile sonlandırıldığını garanti eder; ancak, aynı durumReadOnlySpan<char>ya dachar[]gibi diğer arabellek türleri için geçerli değildir.unsafe void NullTerminationExamples(string str, ReadOnlySpan<char> span, char[] array) { Debug.Assert(str is not null); Debug.Assert(array is not null); fixed (char* pStr = str) { // OK: Strings are always guaranteed to have a null terminator. // This will assign the value '\0' to the variable 'ch'. char ch = pStr[str.Length]; } fixed (char* pSpan = span) { // INCORRECT: Spans aren't guaranteed to be null-terminated. // This could throw, assign garbage data to 'ch', or cause an AV and crash. char ch = pSpan[span.Length]; } fixed (char* pArray = array) { // INCORRECT: Arrays aren't guaranteed to be null-terminated. // This could throw, assign garbage data to 'ch', or cause an AV and crash. char ch = pArray[array.Length]; } }❌ Sabitlenmiş
Span<char>veyaReadOnlySpan<char>öğelerini, belirgin bir uzunluk bağımsız değişkeni geçirmeden p/invoke sınırının ötesine geçirmeyin. Aksi takdirde, p/invoke sınırının diğer tarafındaki kod, arabelleğin null olarak sonlandırıldığına hatalı şekilde inanabilir.
unsafe static extern void SomePInvokeMethod(char* pwszData);
unsafe void IncorrectPInvokeExample(ReadOnlySpan<char> data)
{
fixed (char* pData = data)
{
// INCORRECT: Since 'data' is a span and is not guaranteed to be null-terminated,
// the receiver might attempt to keep reading beyond the end of the buffer,
// resulting in undefined behavior.
SomePInvokeMethod(pData);
}
}
Bu sorunu çözmek için hem veri işaretçisinihem de mümkünse uzunluğu kabul eden alternatif bir p/invoke imzası kullanın. Aksi takdirde, alıcının ayrı bir uzunluk argümanı kabul etmenin bir yolu yoksa, orijinal verilerin string'e dönüştürüldüğünden ve sabitlemeden önce p/invoke sınırına geçirildiğinden emin olun.
unsafe static extern void SomePInvokeMethod(char* pwszData);
unsafe static extern void SomePInvokeMethodWhichTakesLength(char* pwszData, uint cchData);
unsafe void CorrectPInvokeExample(ReadOnlySpan<char> data)
{
fixed (char* pData = data)
{
// OK: Since the receiver accepts an explicit length argument, they're signaling
// to us that they don't expect the pointer to point to a null-terminated buffer.
SomePInvokeMethodWhichTakesLength(pData, (uint)data.Length);
}
// Alternatively, if the receiver doesn't accept an explicit length argument, use
// ReadOnlySpan<T>.ToString to convert the data to a null-terminated string before
// pinning it and sending it across the p/invoke boundary.
fixed (char* pStr = data.ToString())
{
// OK: Strings are guaranteed to be null-terminated.
SomePInvokeMethod(pStr);
}
}
17. Dize mutasyonları
C# dilindeki string'ler tasarım gereği değişmezdir ve güvenli olmayan kod kullanarak onları değiştirme girişimleri tanımsız davranışa yol açabilir. Örnek:
string s = "Hello";
fixed (char* p = s)
{
p[0] = '_';
}
Console.WriteLine("Hello"); // prints "_ello" instead of "Hello"
Bir intern edilen dizeyi değiştirmek (çoğu dize değişmez olmakla birlikte), diğer tüm kullanım değerlerini değiştirir. Dize araya alma olmadan bile, yeni oluşturulan bir dizeye yazmak yerine daha güvenli String.Create API kullanılmalıdır.
// Bad:
string s = new string('\n', 4); // non-interned string
fixed (char* p = s)
{
// Copy data into the newly created string
}
// Good:
string s = string.Create(4, state, (chr, state) =>
{
// Copy data into the newly created string
});
Recommendations
-
❌ Dizeleri değiştirmeyin.
String.CreateKarmaşık kopyalama mantığı gerekiyorsa yeni bir dize oluşturmak için API'yi kullanın. Aksi takdirde.ToString(),StringBuilder,new string(...)veya string interpolasyon söz dizimini kullanın.
18. Ham IL kodu (örneğin, System.Reflection.Emit ve Mono.Cecil)
Tanım gereği ham IL (gibi System.Reflection.Emitüçüncü taraf kitaplıkları aracılığıyla Mono.Cecilveya doğrudan IL kodu yazarak) yaymak, C# tarafından sunulan tüm bellek güvenliği garantilerini atlar.
Kesinlikle gerekli olmadıkça bu tür teknikleri kullanmaktan kaçının.
Recommendations
- ❌ Kılavuzlardan yoksun olarak gelen ham IL kodunu yaymayın, çünkü bu, tür güvenliği ve diğer sorunlar için risk oluşturmayı kolaylaştırır. Diğer dinamik kod oluşturma tekniklerinde olduğu gibi, derleme zamanında yapılmazsa ham IL yayma da AOT kullanımına uygun değildir.
- ✔️ MÜMKÜNse Bunun yerine Kaynak Oluşturucuları kullanın.
- ✔️ DO, gereksinim ortaya çıktığında özel üyeler için düşük ek yük serileştirme kodu yazmak için ham IL yayma yerine [UnsafeAccessor] tercih eder.
- ✔️ Bazı API'ler eksikse ve bunun yerine ham IL kodunu kullanmaya zorlanıyorsanız dotnet/runtime için bir API teklifi oluşturun.
- ✔️ Ham IL kullanmanız gerekiyorsa yayılan IL kodunu doğrulamak için
ilverifyveya benzer araçlar kullanın.
19. Başlatılmamış yereller [SkipLocalsInit] ve Unsafe.SkipInit
[SkipLocalsInit] ,NET 5.0'da JIT'nin yöntem başına veya modül genelinde yöntemlerdeki yerel değişkenlerin sıfırlanması işlemini atmasına olanak sağlamak için kullanıma sunulmuştur. Bu özellik, stackalloc gibi gereksiz sıfır başlatmalarını ortadan kaldırmak için JIT'ye sıklıkla yardımcı olmak amacıyla kullanılır. Ancak, yerel öğeler kullanımdan önce açıkça başlatılmazsa tanımsız davranışa yol açabilir. JIT'in sıfır başlatmaları ortadan kaldırma ve vektörleştirme yapma yeteneğinde yapılan son geliştirmelerle, [SkipLocalsInit] ve Unsafe.SkipInit gereksinimi önemli ölçüde azaldı.
Recommendations
-
❌ ve
[SkipLocalsInit]KULLANMAYINUnsafe.SkipInit; sık erişimli kodda herhangi bir performans avantajı gözlemlenmiyorsa veya bunların neden olduğu risklerden emin değilseniz. - ✔️
GC.AllocateUninitializedArrayveArrayPool<T>.Shared.Rentgibi API'leri kullanırken savunmacı programlama yapın, çünkü bu API'ler benzer şekilde başlatılmamış arabellekler döndürebilir.
20. ArrayPool<T>.Shared ve benzer havuz API'leri
ArrayPool<T>.Shared , sık erişimli koddaki GC basıncını azaltmak için kullanılan paylaşılan bir dizi havuzudur. Genellikle G/Ç işlemleri veya diğer kısa süreli senaryolar için geçici arabellekler ayırma amacıyla kullanılır. API basit olsa da ve doğası gereği güvenli olmayan özellikler içermese de, C# dilinde kullanım sonrası hatalara yol açabilir. Örnek:
var buffer = ArrayPool<byte>.Shared.Rent(1024);
_buffer = buffer; // buffer object escapes the scope
Use(buffer);
ArrayPool<byte>.Shared.Return(buffer);
_buffer çağrısından sonra Return her kullanımı, serbest bırakıldıktan sonra kullanım hatasıdır. Bu minimal örnek kolayca tespit edilir, ancak Rent ve Return, farklı kapsam veya yöntemlerde olduğunda hatayı tespit etmek zorlaşır.
Recommendations
- ✔️ DO, olası hataların kapsamını daraltmak için mümkünse ve ile eşleşen çağrıları
RentReturnaynı yöntem içinde tutun. -
❌ Hatalı mantığın arabelleği kullanmayı bitirdiğinden emin değilseniz, bloğu çağırmak için bir desen
try-finallykullanmayın. Beklenmedik erken birReturnnedeniyle kullanımdan sonra hatası riski almak yerine arabelleği terk etmek daha iyidir. - ✔️ gibi ObjectPool<T>diğer havuz api'leri veya desenleriyle ilgili benzer sorunların ortaya çıkabileceğini unutmayın.
21. bool<->int dönüşümler
ECMA-335 standardı, boole değerini 0-255 aralığında tanımlarken true sıfır olmayan herhangi bir değerdir, ancak 0 veya 1 haricindeki "denormalleştirilmiş" değerlerin neden olabileceği güvenilmez davranışlardan kaçınmak için tamsayılar ile Boole değerleri arasında açık dönüşümlerden kaçınmak daha iyidir.
// Bad:
bool b = Unsafe.As<int, bool>(ref someInteger);
int i = Unsafe.As<bool, int>(ref someBool);
// Good:
bool b = (byte)someInteger != 0;
int i = someBool ? 1 : 0;
Önceki .NET çalışma zamanlarında mevcut olan JIT, bu mantığın güvenli sürümünü tam olarak iyileştirmediğinden geliştiricilerin performansa duyarlı kod yollarında ve bool arasında int dönüştürme yapmak için güvenli olmayan yapılar kullanmasına neden oldu. Bu artık geçerli değildir ve modern .NET JIT'leri güvenli sürümü etkili bir şekilde iyileştirir.
Recommendations
- ❌ Güvenli olmayan kod kullanarak tamsayılar ve Boole'lar arasında "dalsız" dönüştürmeler yazmayın.
- ✔️ BUNUN yerine üçüncül işleçleri (veya diğer dallanma mantığını) kullanın. Modern .NET JIT'leri bunları etkili bir şekilde iyileştirir.
-
❌ GÜVENLİ OLMAYAN API'LERİ, örneğin
boolveyaUnsafe.ReadUnaligned, kullanarak okumayın eğer girişe güvenmiyorsanız. Bunun yerine üçüncül işleçler veya eşitlik karşılaştırmaları kullanmayı göz önünde bulundurun:
// Bad:
bool b = Unsafe.ReadUnaligned<bool>(ref byteData);
// Good:
bool b = byteData[0] != 0;
// Bad:
ReadOnlySpan<byte> byteSpan = ReadDataFromNetwork();
bool[] boolArray = MemoryMarshal.Cast<byte, bool>(byteSpan).ToArray();
// Good:
ReadOnlySpan<byte> byteSpan = ReadDataFromNetwork();
bool[] boolArray = new bool[byteSpan];
for (int i = 0; i < byteSpan.Length; i++) { boolArray[i] = byteSpan[i] != 0; }
Daha fazla bilgi için bkz. Doldurmalar veya bölümlenebilir olmayan üyeler içeren yapıların ikili (de)seri hale getirilmesi.
22. Birlikte Çalışma
Bu belgedeki önerilerin çoğu birlikte çalışma senaryoları için de geçerli olsa da Yerel birlikte çalışabilirlik en iyi yöntemleri kılavuzunu izlemeniz önerilir. Ayrıca CsWin32 ve CsWinRT gibi otomatik olarak oluşturulan birlikte çalışma sarmalayıcılarını da kullanmayı göz önünde bulundurun. Elle birlikte çalışma kodu yazma gereksinimini en aza indirir ve bellek güvenliği sorunlarına neden olma riskini azaltır.
23. İş parçacığı güvenliği
Bellek güvenliği ve iş parçacığı güvenliği ortogonal kavramlardır. Kod bellek açısından güvenli olabilir ancak yine de veri yarışları, yırtık okumalar veya görünürlük hataları içerebilir; buna karşılık, kod güvenli olmayan bellek işlemleri yoluyla tanımsız davranışı tetikleyebilse bile iş parçacığı açısından güvenli olabilir. Daha kapsamlı yönergeler için bkz. Yönetilen iş parçacığı için en iyi uygulamalar ve .NET Bellek Modeli.
24. SIMD/Vektörleştirme etrafında güvenli olmayan kod
Diğer ayrıntılar için bkz . Vektörleştirme yönergeleri . Güvenli olmayan kod bağlamında aşağıdakileri göz önünde bulundurmanız önemlidir:
- SIMD işlemleri, bölünmezlik garantileri sağlamak için karmaşık gereksinimlere sahiptir (bazen bunları hiç sağlamaz).
- SIMD Load/Store API'lerinin çoğu sınır denetimi sağlamaz.
25. Fuzz testi
Fuzz testi (veya "fuzzing"), bir bilgisayar programına giriş olarak geçersiz, beklenmeyen veya rastgele veriler sağlamayı içeren otomatik bir yazılım testi tekniğidir. Test kapsamında boşluklar olabilecek kodda bellek güvenliği sorunlarını algılamanın bir yolunu sağlar. .NET kodu için fuzz testi ayarlamak için SharpFuzz gibi araçları kullanabilirsiniz.
26. Derleyici uyarıları
C# derleyicisi genellikle yanlış güvenli olmayan kod kullanımıyla ilgili uyarılar ve çözümleyiciler gibi kapsamlı destek sağlamaz. Ancak, olası sorunları algılamaya yardımcı olabilecek bazı mevcut uyarılar vardır ve dikkatli bir şekilde dikkate alınmadan göz ardı edilmemeli veya gizlenmemelidir. Bazı örnekler şunlardır:
nint ptr = 0;
unsafe
{
int local = 0;
ptr = (nint)(&local);
}
await Task.Delay(100);
// ptr is used here
Bu kod CS9123 uyarısını ("'&' işleci zaman uyumsuz yöntemlerdeki parametrelerde veya yerel değişkenlerde kullanılmamalıdır") üretir ve bu da kodun büyük olasılıkla yanlış olduğunu gösterir.
Recommendations
- ✔️ Derleyici uyarılarına özen gösterin ve onları engellemek yerine temel sorunları düzeltin.
- ❌ Derleyici uyarılarının olmamasının kodun doğru olduğu anlamına geldiğini varsaymayın. C# derleyicisinin, yanlış güvensiz kod kullanımını algılama konusunda sınırlı desteği vardır.
References
- Güvenli olmayan kod, işaretçi türleri ve işlev işaretçileri.
- Güvenli olmayan kod, dil belirtimi.
- CoreCLR ve GC iç bileşenleriyle ilgili gelişmiş konular için Her CLR Geliştiricisinin Kod Yazmadan Önce Bilmesi Gerekenler.
- Yerel birlikte çalışabilirlik en iyi yöntemleri.
- Yönetilen iş parçacığı oluşturma en iyi yöntemleri.
- Özel durumlar için en iyi yöntemler.
- Vektörleştirme yönergeleri
- .NET Bellek Modeli
- ECMA-335
- ECMA-335 artırılmış özellikleri