SAL'ı Anlama
Microsoft kaynak kodu ek açıklama dili (SAL), bir işlevin parametrelerini nasıl kullandığını, bunlar hakkında yaptığı varsayımları ve tamamlandığında yaptığı garantileri açıklamak için kullanabileceğiniz bir dizi ek açıklama sağlar. Ek açıklamalar üst bilgi dosyasında <sal.h>
tanımlanır. C++ için Visual Studio kod analizi, işlevlerin analizini değiştirmek için SAL ek açıklamalarını kullanır. Windows sürücü geliştirmesi için SAL 2.0 hakkında daha fazla bilgi için bkz . Windows Sürücüleri için SAL 2.0 Ek Açıklamaları.
Yerel olarak, C ve C++ geliştiricilerin amaç ve sabitliği tutarlı bir şekilde ifade etmek için yalnızca sınırlı yollar sağlar. SAL ek açıklamalarını kullanarak işlevlerinizi daha ayrıntılı bir şekilde açıklayabilirsiniz, böylece bunları kullanan geliştiriciler bunları nasıl kullanacaklarını daha iyi anlayabilir.
SAL Nedir ve Neden Kullanmalısınız?
Basitçe belirtmek gerekirse, SAL derleyicinin kodunuzu sizin yerinize denetlemesine izin vermenin ucuz bir yoludur.
SAL Kodu Daha Değerli Hale Getirir
SAL, hem insanlar hem de kod analizi araçları için kod tasarımınızı daha anlaşılır hale getirmenize yardımcı olabilir. C çalışma zamanı işlevini memcpy
gösteren bu örneği göz önünde bulundurun:
void * memcpy(
void *dest,
const void *src,
size_t count
);
Bu işlevin ne yaptığını söyleyebilir misiniz? Bir işlev uygulandığında veya çağrıldığında, program doğruluğunu sağlamak için belirli özelliklerin korunması gerekir. Örnekteki gibi bir bildirime bakarak bunların ne olduğunu bilmezsiniz. SAL ek açıklamaları olmadan belgelere veya kod açıklamalarına güvenmeniz gerekir. Belgelerde şu ifadeler yer aldı memcpy
:
"
memcpy
src'den dest'e bayt sayısını kopyalar;wmemcpy
kopyalar, geniş karakterleri (iki bayt) sayar. Kaynak ve hedef çakışıyorsa, öğesininmemcpy
davranışı tanımsız olur. Çakışan bölgeleri işlemek için kullanınmemmove
.
Önemli: Hedef arabelleğin kaynak arabellekten aynı boyutta veya daha büyük olduğundan emin olun. Daha fazla bilgi için bkz. Arabellek Taşmalarından Kaçınma."
Belgelerde, program doğruluğundan emin olmak için kodunuzun belirli özellikleri tutması gereken birkaç bilgi bulunur:
memcpy
bayt sayısınıcount
kaynak arabellekten hedef arabelleğe kopyalar.Hedef arabellek en az kaynak arabellek kadar büyük olmalıdır.
Ancak, derleyici belgeleri veya resmi olmayan açıklamaları okuyamaz. İki arabellek ile count
arasında bir ilişki olduğunu bilmez ve bir ilişkiyi etkili bir şekilde tahmin etmez. SAL, burada gösterildiği gibi işlevin özellikleri ve uygulaması hakkında daha fazla netlik sağlayabilir:
void * memcpy(
_Out_writes_bytes_all_(count) void *dest,
_In_reads_bytes_(count) const void *src,
size_t count
);
Bu ek açıklamaların belgelerdeki bilgilere benzediklerine dikkat edin, ancak daha kısadır ve anlamsal bir desen izlerler. Bu kodu okurken, bu işlevin özelliklerini ve arabellek taşması güvenlik sorunlarını nasıl önleyebileceğinizi hızlı bir şekilde anlayabilirsiniz. Daha da iyisi, SAL'nin sağladığı anlamsal desenler, olası hataların erken keşfedilmesinde otomatik kod analizi araçlarının verimliliğini ve verimliliğini artırabilir. Birinin bu buggy uygulamasını wmemcpy
yazdığını düşünün:
wchar_t * wmemcpy(
_Out_writes_all_(count) wchar_t *dest,
_In_reads_(count) const wchar_t *src,
size_t count)
{
size_t i;
for (i = 0; i <= count; i++) { // BUG: off-by-one error
dest[i] = src[i];
}
return dest;
}
Bu uygulama ortak bir hata içeriyor. Neyse ki, kod yazarı SAL arabellek boyutu ek açıklamasını ekledi; bir kod çözümleme aracı bu işlevi tek başına analiz ederek hatayı yakalayabilir.
SAL Temel Bilgileri
SAL, kullanım düzenine göre kategorilere ayrılmış dört temel parametre türünü tanımlar.
Kategori | Parametre Ek Açıklaması | Açıklama |
---|---|---|
Çağrılan işleve giriş | _In_ |
Veriler çağrılan işleve geçirilir ve salt okunur olarak kabul edilir. |
Çağrılan işleve giriş ve çağıranın çıkışı | _Inout_ |
Kullanılabilir veriler işleve geçirilir ve potansiyel olarak değiştirilir. |
Çağıranın çıktısı | _Out_ |
Çağıran yalnızca çağrılan işlevin yazılabilmesi için alan sağlar. Çağrılan işlev bu alana veri yazar. |
Çağıranın işaretçisinin çıkışı | _Outptr_ |
Çağıranın çıktısı gibi. Çağrılan işlev tarafından döndürülen değer bir işaretçidir. |
Bu dört temel ek açıklama çeşitli yollarla daha açık hale getirilebilir. Varsayılan olarak, açıklamalı işaretçi parametrelerinin gerekli olduğu varsayılır; işlevin başarılı olması için NULL olmayan olması gerekir. Temel ek açıklamaların en yaygın olarak kullanılan varyasyonu, işaretçi parametresinin isteğe bağlı olduğunu gösterir; NULL ise, işlev çalışmasını yaparken yine de başarılı olabilir.
Bu tabloda, gerekli ve isteğe bağlı parametreler arasında nasıl ayrım yapılır gösterilmektedir:
Parametreler gereklidir | Parametreler isteğe bağlıdır | |
---|---|---|
Çağrılan işleve giriş | _In_ |
_In_opt_ |
Çağrılan işleve giriş ve çağıranın çıkışı | _Inout_ |
_Inout_opt_ |
Çağıranın çıktısı | _Out_ |
_Out_opt_ |
Çağıranın işaretçisinin çıkışı | _Outptr_ |
_Outptr_opt_ |
Bu ek açıklamalar, olası başlatılmamış değerleri ve geçersiz null işaretçi kullanımlarını resmi ve doğru bir şekilde tanımlamaya yardımcı olur. Null değerinin gerekli bir parametreye geçirilmesi kilitlenmeye veya "başarısız" hata kodunun döndürülmesine neden olabilir. Her iki durumda da işlev işini yaparken başarılı olamaz.
SAL Örnekleri
Bu bölümde, temel SAL ek açıklamaları için kod örnekleri gösterilmektedir.
Hataları Bulmak için Visual Studio Code Çözümleme Aracı'nı kullanma
Örneklerde Visual Studio Code Analysis aracı, kod hatalarını bulmak için SAL ek açıklamaları ile birlikte kullanılır. Bunu şu şekilde yapabilirsiniz.
Visual Studio kod çözümleme araçlarını ve SAL'yi kullanmak için
Visual Studio'da, SAL ek açıklamalarını içeren bir C++ projesi açın.
Menü çubuğunda Derle, Çözümde Kod Analizini Çalıştır'ı seçin.
Bu bölümdeki _In_ örneğini göz önünde bulundurun. Üzerinde kod analizi çalıştırırsanız, bu uyarı görüntülenir:
C6387 Geçersiz Parametre Değeri 'pInt' '0' olabilir: Bu, 'InCallee' işlevinin belirtimine uymaz.
Örnek: _In_ Ek Açıklaması
Ek _In_
açıklama aşağıdakileri gösterir:
Parametre geçerli olmalıdır ve değiştirilmez.
İşlev yalnızca tek öğeli arabellekten okur.
Çağıranın arabelleği sağlaması ve başlatması gerekir.
_In_
"salt okunur" ifadesini belirtir. Bunun yerine ek açıklamaya sahip olması gereken bir parametreye_Inout_
uygulamak_In_
yaygın bir hatadır._In_
izin verilir ancak işaretçi olmayan skalerlerde çözümleyici tarafından yoksayılır.
void InCallee(_In_ int *pInt)
{
int i = *pInt;
}
void GoodInCaller()
{
int *pInt = new int;
*pInt = 5;
InCallee(pInt);
delete pInt;
}
void BadInCaller()
{
int *pInt = NULL;
InCallee(pInt); // pInt should not be NULL
}
Bu örnekte Visual Studio Code Analysis kullanıyorsanız, çağıranların için pInt
başlatılan arabelleğe Null olmayan bir işaretçi geçirdiğini doğrular. Bu durumda, pInt
işaretçi NULL olamaz.
Örnek: _In_opt_ Ek Açıklaması
_In_opt_
ile aynıdır _In_
, ancak giriş parametresinin NULL olması ve bu nedenle işlevin bunu denetlemesi gerekir.
void GoodInOptCallee(_In_opt_ int *pInt)
{
if(pInt != NULL) {
int i = *pInt;
}
}
void BadInOptCallee(_In_opt_ int *pInt)
{
int i = *pInt; // Dereferencing NULL pointer 'pInt'
}
void InOptCaller()
{
int *pInt = NULL;
GoodInOptCallee(pInt);
BadInOptCallee(pInt);
}
Visual Studio Code Analizi, işlevin arabelleğe erişmeden önce NULL değerini denetlediğini doğrular.
Örnek: _Out_ Ek Açıklaması
_Out_
, bir öğe arabelleğine işaret eden NULL olmayan bir işaretçinin geçirildiği ve işlevin öğeyi başlatdığı yaygın bir senaryoyu destekler. Çağıranın çağrıdan önce arabelleği başlatması gerekmez; çağrılan işlev, döndürmeden önce başlatmayı vaat eder.
void GoodOutCallee(_Out_ int *pInt)
{
*pInt = 5;
}
void BadOutCallee(_Out_ int *pInt)
{
// Did not initialize pInt buffer before returning!
}
void OutCaller()
{
int *pInt = new int;
GoodOutCallee(pInt);
BadOutCallee(pInt);
delete pInt;
}
Visual Studio Code Çözümleme Aracı, çağıranın bir arabelleğe pInt
NULL olmayan bir işaretçi geçirip döndürmeden önce arabelleğin işlev tarafından başlatıldığını doğrular.
Örnek: _Out_opt_ Ek Açıklaması
_Out_opt_
, parametresinin NULL olması için izin verilmesi dışında ile aynıdır _Out_
ve bu nedenle işlevin bunu denetlemesi gerekir.
void GoodOutOptCallee(_Out_opt_ int *pInt)
{
if (pInt != NULL) {
*pInt = 5;
}
}
void BadOutOptCallee(_Out_opt_ int *pInt)
{
*pInt = 5; // Dereferencing NULL pointer 'pInt'
}
void OutOptCaller()
{
int *pInt = NULL;
GoodOutOptCallee(pInt);
BadOutOptCallee(pInt);
}
Visual Studio Code Çözümlemesi, bu işlevin başvurulmadan önce pInt
NULL olup olmadığını denetlediğini ve NULL değilse pInt
, arabelleğin işlev tarafından döndürülmeden önce başlatıldığını doğrular.
Örnek: _Inout_ Ek Açıklaması
_Inout_
işlevi tarafından değiştirilebilen bir işaretçi parametresine açıklama eklemek için kullanılır. İşaretçi, çağrıdan önce geçerli başlatılan verilere işaret etmelidir ve değişse bile, döndürülen geçerli bir değere sahip olmalıdır. Ek açıklama işlevin tek öğeli arabelleğe serbestçe okunabileceğini ve yazabileceğini belirtir. Çağıranın arabelleği sağlaması ve başlatması gerekir.
Not
gibi _Out_
, _Inout_
değiştirilebilir bir değere uygulanmalıdır.
void InOutCallee(_Inout_ int *pInt)
{
int i = *pInt;
*pInt = 6;
}
void InOutCaller()
{
int *pInt = new int;
*pInt = 5;
InOutCallee(pInt);
delete pInt;
}
void BadInOutCaller()
{
int *pInt = NULL;
InOutCallee(pInt); // 'pInt' should not be NULL
}
Visual Studio Code Çözümlemesi, çağıranların için pInt
başlatılan bir arabelleğe NULL olmayan bir işaretçi geçirdiğini ve dönüşten pInt
önce hala NULL olmayan bir işaretçi olduğunu doğrular ve arabellek başlatılır.
Örnek: _Inout_opt_ Ek Açıklaması
_Inout_opt_
ile aynıdır _Inout_
, ancak giriş parametresinin NULL olması ve bu nedenle işlevin bunu denetlemesi gerekir.
void GoodInOutOptCallee(_Inout_opt_ int *pInt)
{
if(pInt != NULL) {
int i = *pInt;
*pInt = 6;
}
}
void BadInOutOptCallee(_Inout_opt_ int *pInt)
{
int i = *pInt; // Dereferencing NULL pointer 'pInt'
*pInt = 6;
}
void InOutOptCaller()
{
int *pInt = NULL;
GoodInOutOptCallee(pInt);
BadInOutOptCallee(pInt);
}
Visual Studio Code Analysis, bu işlevin arabelleğe erişmeden önce NULL olup olmadığını denetlediğini ve NULL değilse pInt
, arabelleğin işlev tarafından döndürülmeden önce başlatıldığını doğrular.
Örnek: _Outptr_ Ek Açıklaması
_Outptr_
, işaretçi döndürmek için amaçlanan bir parametreye açıklama eklemek için kullanılır. Parametrenin kendisi NULL olmamalıdır ve çağrılan işlev içinde NULL olmayan bir işaretçi döndürür ve bu işaretçi başlatılan verileri gösterir.
void GoodOutPtrCallee(_Outptr_ int **pInt)
{
int *pInt2 = new int;
*pInt2 = 5;
*pInt = pInt2;
}
void BadOutPtrCallee(_Outptr_ int **pInt)
{
int *pInt2 = new int;
// Did not initialize pInt buffer before returning!
*pInt = pInt2;
}
void OutPtrCaller()
{
int *pInt = NULL;
GoodOutPtrCallee(&pInt);
BadOutPtrCallee(&pInt);
}
Visual Studio Code Çözümlemesi, çağıranın için *pInt
NULL olmayan bir işaretçi geçtiğini ve arabelleğin işlev tarafından döndürülmeden önce başlatıldığını doğrular.
Örnek: _Outptr_opt_ Ek Açıklaması
_Outptr_opt_
, parametresinin isteğe bağlı olması dışında ile aynıdır _Outptr_
; çağıran parametre için NULL işaretçisi geçirebilir.
void GoodOutPtrOptCallee(_Outptr_opt_ int **pInt)
{
int *pInt2 = new int;
*pInt2 = 6;
if(pInt != NULL) {
*pInt = pInt2;
}
}
void BadOutPtrOptCallee(_Outptr_opt_ int **pInt)
{
int *pInt2 = new int;
*pInt2 = 6;
*pInt = pInt2; // Dereferencing NULL pointer 'pInt'
}
void OutPtrOptCaller()
{
int **ppInt = NULL;
GoodOutPtrOptCallee(ppInt);
BadOutPtrOptCallee(ppInt);
}
Visual Studio Code Analysis, bu işlevin başvurulmadan önce *pInt
NULL olup olmadığını denetlediğini ve arabelleğin işlev tarafından döndürülmeden önce başlatıldığını doğrular.
Örnek: _Out_ ile Birlikte _Success_ Ek Açıklaması
Ek açıklamalar çoğu nesneye uygulanabilir. Özellikle bir işlevin tamamına açıklama ekleyebilirsiniz. Bir işlevin en belirgin özelliklerinden biri, başarılı veya başarısız olmasıdır. Ancak bir arabellek ile boyutu arasındaki ilişki gibi, C/C++ işlevin başarısını veya başarısızlığını ifade edemez. Ek açıklamayı _Success_
kullanarak, bir işlev için başarının nasıl göründüğünü söyleyebilirsiniz. Ek açıklama parametresi _Success_
, true olduğunda işlevin başarılı olduğunu gösteren bir ifadedir. İfade, ek açıklama ayrıştırıcısının işleyebileceği herhangi bir şey olabilir. ek açıklamaların işlev döndürdüğünden sonraki etkileri yalnızca işlev başarılı olduğunda geçerlidir. Bu örnekte, doğru şeyi yapmak için nasıl etkileşime _Out_
geçtiğini gösterir_Success_
. Dönüş değerini temsil etmek için anahtar sözcüğünü return
kullanabilirsiniz.
_Success_(return != false) // Can also be stated as _Success_(return)
bool GetValue(_Out_ int *pInt, bool flag)
{
if(flag) {
*pInt = 5;
return true;
} else {
return false;
}
}
Ek _Out_
açıklama, Visual Studio Code Analysis'in çağıranın için pInt
bir arabelleğe NULL olmayan bir işaretçi geçirip geçirmediğini ve arabelleğin işlev tarafından döndürülmeden önce başlatıldığını doğrulamasına neden olur.
SAL En İyi Uygulaması
Mevcut Koda Ek Açıklamalar Ekleme
SAL, kodunuzun güvenliğini ve güvenilirliğini artırmanıza yardımcı olabilecek güçlü bir teknolojidir. SAL'yi öğrendikte yeni beceriyi günlük çalışmanıza uygulayabilirsiniz. Yeni kodda, tasarım gereği SAL tabanlı belirtimleri kullanabilirsiniz; eski kodda artımlı olarak ek açıklamalar ekleyebilir ve böylece her güncelleştirdiğinizde avantajları artırabilirsiniz.
Microsoft genel üst bilgilerine zaten açıklama ekleniyor. Bu nedenle, projelerinizde en iyi şekilde yararlanmak için önce Win32 API'lerini çağıran yaprak düğüm işlevlerine ve işlevlerine açıklama eklemenizi öneririz.
Ne Zaman Açıklama Ekleyebilirim?
Bazı yönergeler şunlardır:
Tüm işaretçi parametrelerine açıklama ekleme.
Kod Analizi'nin arabellek ve işaretçi güvenliğini sağlayabilmesi için değer aralığı ek açıklamalarına açıklama ekleyin.
Kilitleme kurallarına ve kilitleme yan efektlerine açıklama ekleme. Daha fazla bilgi için bkz . Kilitleme Davranışına Açıklama Ekleme.
Sürücü özelliklerine ve etki alanına özgü diğer özelliklere açıklama ekleyin.
İsterseniz amacınızı net bir şekilde ifade etmek ve ek açıklamaların yapıldığını denetlemeyi kolaylaştırmak için tüm parametrelere açıklama ekleyebilirsiniz.