Porozumění SAL
Jazyk Microsoft Source-Code Annotation (SAL) poskytuje sadu poznámek, které můžete použít k popsání toho, jak funkce používá své parametry, předpoklady, které se o nich týkají, a záruky, které při jejím dokončení vytvoří. Poznámky jsou definovány v hlavičkovém souboru <sal.h> . Visual Studio analýza kódu pro jazyk C++ používá poznámky SAL k úpravě analýzy funkcí. další informace o sal 2,0 pro vývoj ovladačů Windows naleznete v tématu poznámky SAL 2,0 pro ovladače Windows.
Nativně, C a C++ poskytují jenom omezené možnosti, jak vývojářům konzistentně vyjádřit a nerovnost. Pomocí poznámek SAL můžete své funkce popsat podrobněji, aby vývojáři, kteří je používají, lépe pochopili, jak je používat.
Co je SAL a proč byste ji měli používat?
SAL je pouze nenákladný způsob, jak nechat kompilátor kontrolovat váš kód.
SAL dělá kód užitečnější
SAL vám může usnadnit návrh kódu pro lidi i pro nástroje pro analýzu kódu. Vezměte v úvahu tento příklad, který ukazuje běhovou funkci jazyka C memcpy :
void * memcpy(
void *dest,
const void *src,
size_t count
);
Můžete zjistit, co tato funkce dělá? Pokud je funkce implementována nebo volána, je nutné zachovat určité vlastnosti, aby bylo zajištěno správnost programu. Pouhým zobrazením deklarace, jako je například v příkladu, neznáte, co jsou. Bez poznámek SAL byste se museli spoléhat na dokumentaci nebo komentáře ke kódu. V dokumentaci k tomuto memcpy říkáme:
"
memcpykopírujememcpybajtů ze Src na cíl; zkopíruje počet znaků v šířce (dva bajty). Pokud se zdrojový a cílový překrývají, chovánímemcpynení definováno. Sloužímemmoveke zpracování překrývajících se oblastí.
Důležité informace: Ujistěte se, že cílová vyrovnávací paměť má stejnou velikost nebo je větší než zdrojová vyrovnávací paměť. Další informace najdete v tématu předcházení přetečení vyrovnávací paměti.
Dokumentace obsahuje několik bitů informací, které naznačují, že váš kód musí udržovat určité vlastnosti, aby bylo zajištěno správné fungování programu:
memcpykopírujecountbajty ze zdrojové vyrovnávací paměti do cílové vyrovnávací paměti.Cílová vyrovnávací paměť musí být alespoň stejně velká jako zdrojová vyrovnávací paměť.
Kompilátor ale nemůže přečíst dokumentaci nebo neformální komentáře. Neví, že mezi těmito dvěma vyrovnávacími paměťmi a a count zároveň nedokáže efektivně odhadnout relaci. SAL může poskytnout přehlednější informace o vlastnostech a implementaci funkce, jak je znázorněno zde:
void * memcpy(
_Out_writes_bytes_all_(count) void *dest,
_In_reads_bytes_(count) const void *src,
size_t count
);
Všimněte si, že tyto poznámky se podobají informacím v dokumentaci, ale jsou stručnější a sledují sémantický vzor. Při čtení tohoto kódu můžete rychle porozumět vlastnostem této funkce a vyhnout se problémům se zabezpečením při přetečení vyrovnávací paměti. Ještě lepší, sémantické vzory, které SAL poskytují, můžou zlepšit efektivitu a efektivitu automatizovaných nástrojů pro analýzu kódu v brzkém zjišťování možných chyb. Imagine, že někdo zapisuje tuto implementaci ladění wmemcpy :
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;
}
Tato implementace obsahuje běžnou chybu mimo jednu. Naštěstí autor kódu zahrnoval anotaci velikosti vyrovnávací paměti SAL – Nástroj pro analýzu kódu by mohl zachytit chybu tím, že analyzuje tuto funkci samostatně.
Základy SAL
SAL definuje čtyři základní druhy parametrů, které jsou zařazeny do kategorií podle vzorce použití.
| Kategorie | Anotace parametru | Popis |
|---|---|---|
| Vstup do volané funkce | _In_ |
Data se předávají volané funkci a jsou považována za jen pro čtení. |
| Vstup na volanou funkci a výstup do volajícího | _Inout_ |
Použitelná data se předávají do funkce a můžou se změnit. |
| Výstup do volajícího | _Out_ |
Volající poskytuje prostor pro volanou funkci, do které se zapisuje. Volaná funkce zapisuje data do tohoto prostoru. |
| Výstup ukazatele na volajícího | _Outptr_ |
Jako výstup do volajícího. Hodnota vrácená volanou funkcí je ukazatel. |
Tyto čtyři základní poznámky je možné v různých způsobech považovat za explicitní. Ve výchozím nastavení se předpokládá, že jsou vyžadovány parametry ukazatele s poznámkami, musí být pro funkci nenulové, aby byla funkce úspěšná. Nejčastěji používaná variace základních poznámek znamená, že parametr ukazatele je nepovinný – Pokud má hodnotu NULL, funkce může i nadále úspěšně fungovat.
Tato tabulka ukazuje, jak rozlišovat mezi požadovanými a nepovinnými parametry:
| Parametry jsou povinné. | Parametry jsou volitelné. | |
|---|---|---|
| Vstup do volané funkce | _In_ |
_In_opt_ |
| Vstup na volanou funkci a výstup do volajícího | _Inout_ |
_Inout_opt_ |
| Výstup do volajícího | _Out_ |
_Out_opt_ |
| Výstup ukazatele na volajícího | _Outptr_ |
_Outptr_opt_ |
Tyto poznámky vám pomůžou identifikovat možné neinicializované hodnoty a neplatný ukazatel s hodnotou null používá formálním a přesným způsobem. Předání hodnoty NULL požadovanému parametru může způsobit chybu nebo může způsobit vrácení kódu chyby "neúspěšné". V obou případech nemůže funkce při provádění své úlohy úspěšně fungovat.
Příklady SAL
V této části jsou uvedeny příklady kódu pro základní poznámky SAL.
hledání vad pomocí nástroje Visual Studio Code Analysis Tool
v příkladech se nástroj Visual Studio Code Analysis používá společně s poznámkami SAL k nalezení vad kódu. Tady je postup.
použití nástrojů pro analýzu kódu Visual Studio a SAL
v Visual Studio otevřete projekt C++, který obsahuje poznámky SAL.
na panelu nabídek vyberte možnost sestavit, spustit Code Analysis v řešení.
Podívejte se na příklad _In_ v této části. Pokud na něm spustíte analýzu kódu, zobrazí se toto upozornění:
C6387 neplatná hodnota parametru ' pInt ' může být ' 0 ': to nedodržuje specifikace pro funkci ' InCallee '.
Příklad: _In_ anotace
_In_Poznámka znamená, že:
Parametr musí být platný a nebude změněn.
Funkce bude načtena pouze z vyrovnávací paměti s jedním prvkem.
Volající musí poskytnout vyrovnávací paměť a inicializovat ji.
_In_Určuje "jen pro čtení". Běžnou_In_chybou je použít na parametr, který má_Inout_anotaci místo toho._In_je povoleno, ale analyzátor ignoruje na skalárních skalárních modulech.
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
}
použijete-li analýzu Visual Studio Code v tomto příkladu, ověří, že volající přecházejí ukazatel, který není Null, na inicializovaný buffer pro pInt . V takovém případě pInt ukazatel nemůže mít hodnotu null.
Příklad: anotace _In_opt_
_In_opt_ je stejný jako _In_ s tím rozdílem, že vstupní parametr může mít hodnotu null a proto by funkce měla tuto funkci kontrolovat.
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);
}
analýza Visual Studio Code ověří, že funkce před přístupem k vyrovnávací paměti kontroluje hodnotu NULL.
Příklad: _Out_ anotace
_Out_ podporuje běžný scénář, ve kterém je předán ukazatel bez hodnoty NULL, který odkazuje na vyrovnávací paměť elementu, a funkce inicializuje element. Volající nemusí před voláním inicializovat vyrovnávací paměť; volaná funkce příslibů k jejímu inicializaci, než se vrátí.
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;
}
nástroj pro analýzu Visual Studio Code ověřuje, že volající předává ukazatel bez hodnoty NULL do vyrovnávací paměti pro pInt a zda je vyrovnávací paměť inicializována funkcí před tím, než se vrátí.
Příklad: anotace _Out_opt_
_Out_opt_ je stejný jako _Out_ s tím rozdílem, že parametr může mít hodnotu null a proto by měla funkce tuto funkci kontrolovat.
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);
}
analýza Visual Studio Code ověřuje, že tato funkce před tím, než se vrátí zpět na odkaz, kontroluje, jestli je hodnota null, pInt a pokud pInt není NULL, tato vyrovnávací paměť je inicializována funkcí.
Příklad: _Inout_ anotace
_Inout_ slouží k přidání poznámky k parametru ukazatele, který může být změněn funkcí. Ukazatel musí před voláním ukazovat na platná inicializovaná data a i když se změní, musí mít při návratu stále platnou hodnotu. Poznámka určuje, že funkce může volně číst a zapisovat do vyrovnávací paměti s jedním prvkem. Volající musí poskytnout vyrovnávací paměť a inicializovat ji.
Poznámka
Podobně _Out_ jako musí být na _Inout_ upravitelnou hodnotu aplikována hodnota .
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 Analysis ověří, že volající předá ukazatel, který není null, inicializované vyrovnávací paměti pro , a že před vrácením je stále bez hodnoty NULL a vyrovnávací paměť pIntpInt je inicializována.
Příklad: _Inout_opt_ anotace
_Inout_opt_ je stejný jako s tím rozdílem, že vstupní parametr může mít hodnotu NULL, a proto by to funkce _Inout_ měla zkontrolovat.
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 ověří, že tato funkce kontroluje hodnotu NULL před tím, než přistupuje k vyrovnávací paměti, a pokud není NULL, inicializuje vyrovnávací paměť funkcí předtím, než se pInt vrátí.
Příklad: Annotace _Outptr_
_Outptr_ slouží k anotace parametru, který má vrátit ukazatel. Samotný parametr by neměl mít hodnotu NULL a v ní volána funkce vrací ukazatel, který není NULL, a tento ukazatel odkazuje na inicializované data.
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 Analysis ověří, že volající předá ukazatel, který nemá hodnotu NULL, a že funkce před vrácením inicializuje vyrovnávací *pInt paměť.
Příklad: _Outptr_opt_ anotace
_Outptr_opt_ je stejný jako s tím rozdílem, že parametr je volitelný – volající může _Outptr_ předat ukazatel NULL pro parametr.
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 ověří, že tato funkce před dereferenced kontroluje hodnotu NULL a že funkce před vrácením inicializuje vyrovnávací *pInt paměť.
Příklad: Anotace _Success_ v kombinaci s _Out_
Poznámky lze použít u většiny objektů. Konkrétně můžete anotovat celou funkci. Jednou z nejběžnějších vlastností funkce je, že může být úspěšná nebo neúspěchná. Stejně jako přidružení mezi vyrovnávací pamětí a její velikostí ale C/C++ nedokáže vyjádřit úspěch nebo neúspěch funkce. Pomocí poznámky můžete říct, jak vypadá úspěch _Success_ funkce. Parametr poznámky je pouze výraz, který v případě hodnoty true označuje, _Success_ že funkce byla úspěšná. Výrazem může být cokoli, co dokáže analyzátor poznámek zpracovat. Účinky poznámek po vrácení funkce jsou použitelné pouze v případě, že je funkce úspěšná. Tento příklad ukazuje, _Success_ jak komunikuje se _Out_ správným způsobem. K reprezentaci návratové return hodnoty můžete použít klíčové slovo .
_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;
}
}
Poznámka způsobí, že Visual Studio Code Analysis ověří, že volající předá ukazatel, který není null, do vyrovnávací paměti pro a že vyrovnávací paměť je inicializována funkcí před _Out_pInt vrácením.
Sal – osvědčený postup
Přidání poznámek do existujícího kódu
SAL je výkonná technologie, která vám pomůže zlepšit zabezpečení a spolehlivost kódu. Až se naučíte SAL, můžete tuto novou dovednost použít pro každodenní práci. V novém kódu můžete specifikace založené na SAL používat v celém návrhu. Ve starším kódu můžete postupně přidávat poznámky a zvyšovat tak výhody při každé aktualizaci.
Veřejné hlavičky Microsoftu jsou už anotované. Proto doporučujeme, abyste ve svých projektech nejprve anotace funkcí a funkcí listového uzlu, které volají rozhraní API Win32, získali největší výhody.
Kdy můžu anotovat?
Tady je několik pokynů:
Anotace všech parametrů ukazatele
Anotace rozsahu hodnot anotací, aby Code Analysis zajistily bezpečnost vyrovnávací paměti a ukazatele.
Anotace pravidel uzamykání a zamykání vedlejších efektů Další informace najdete v tématu Popis chování při uzamykání.
Anotace vlastností ovladačů a dalších vlastností specifických pro doménu
Nebo můžete anotovat všechny parametry, aby byl záměr jasný a aby bylo možné snadno zkontrolovat, že byly poznámky provedeny.
Viz také
- Použití poznámek SAL k snížení míry výskytu závad kódu C/C++
- Zadávání poznámek k parametrům funkcí a návratovým hodnotám
- Zadávání poznámek k chování funkcí
- Zadávání poznámek ke strukturám a třídám
- Zadávání poznámek o chování při zamykání
- Určení, kdy a kde se má poznámka použít
- Doporučené postupy a příklady