Porozumění SAL
Jazyk SAL (Source Code Annotation Language) společnosti Microsoft poskytuje sadu poznámek, které můžete použít k popisu, jak funkce používá své parametry, předpoklady, které o nich dělá, a záruky, které provede po dokončení. Poznámky jsou definovány v souboru <sal.h>
záhlaví . Analýza kódu sady Visual Studio pro C++ používá k úpravě analýzy funkcí poznámky SAL. Další informace o sal 2.0 pro vývoj ovladačů systému Windows naleznete v tématu POZNÁMKY SAL 2.0 pro ovladače systému Windows.
Nativně poskytuje jazyk C a C++ pouze omezené způsoby, jak vývojáři konzistentně vyjádřit záměr a invarianci. Pomocí poznámek SAL můžete podrobněji popsat své funkce, aby vývojáři, kteří je využívají, lépe pochopili, jak je používat.
Co je SAL a proč byste ji měli používat?
Jednoduše řečeno, SAL je levný způsob, jak kompilátoru nechat zkontrolovat váš kód za vás.
SAL zvyšuje hodnotu kódu
SAL vám pomůže lépe pochopitelný návrh kódu pro lidi i nástroje pro analýzu kódu. Podívejte se na tento příklad, který ukazuje funkci memcpy
modulu runtime jazyka C:
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, musí být zachovány určité vlastnosti, aby byla zajištěna správnost programu. Stačí se podívat na deklaraci, jako je například deklarace v příkladu, nevíte, co jsou. Bez poznámek SAL byste se museli spoléhat na dokumentaci nebo komentáře ke kódu. Tady je informace o tom, co říká dokumentace memcpy
:
"
memcpy
počet bajtů z src do dest;wmemcpy
počet širokých znaků (dva bajty). Pokud se zdroj a cíl překrývají, chovánímemcpy
není definováno. Sloužímemmove
ke zpracování překrývajících se oblastí.
Důležité: Ujistěte se, že cílová vyrovnávací paměť je stejná nebo větší než zdrojová vyrovnávací paměť. Další informace naleznete v tématu Zabránění 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 se zajistila správnost programu:
memcpy
count
zkopíruje bajty ze zdrojové vyrovnávací paměti do cílové vyrovnávací paměti.Cílová vyrovnávací paměť musí být alespoň tak velká jako zdrojová vyrovnávací paměť.
Kompilátor ale nemůže přečíst dokumentaci ani neformální komentáře. Neví, že mezi těmito dvěma vyrovnávacími paměťmi existuje vztah a count
že také nedokáže efektivně odhadnout vztah. Sal by mohl poskytnout větší přehled 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 používají sémantický vzor. Při čtení tohoto kódu můžete rychle pochopit vlastnosti této funkce a jak se vyhnout problémům se zabezpečením přetečení vyrovnávací paměti. Sémantické vzory, které sal poskytuje, můžou ještě lépe zlepšit efektivitu a efektivitu automatizovaných nástrojů pro analýzu kódu v raném zjišťování potenciálních chyb. Představte si, že někdo napíše tuto implementaci wmemcpy
chyby:
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 typu off-by-one. Autor kódu naštěstí zahrnul poznámku k velikosti vyrovnávací paměti SAL – nástroj pro analýzu kódu by chybu mohl zachytit analýzou samotné funkce.
Základy SAL
SAL definuje čtyři základní typy parametrů, které jsou kategorizovány podle vzoru použití.
Kategorie | Poznámka k parametru | Popis |
---|---|---|
Vstup do volané funkce | _In_ |
Data se předávají do volané funkce a považují se za jen pro čtení. |
Vstup do volané funkce a výstup volajícího | _Inout_ |
Použitelná data se předávají do funkce a potenciálně se mění. |
Výstup volajícího | _Out_ |
Volající poskytuje místo jen pro zavolánou funkci, do které se má zapisovat. Volaná funkce zapisuje data do daného prostoru. |
Výstup ukazatele na volajícího | _Outptr_ |
To se mi líbí Výstup volajícímu. Hodnota vrácená volanou funkcí je ukazatel. |
Tyto čtyři základní poznámky mohou být explicitnější různými způsoby. Ve výchozím nastavení se předpokládá, že se vyžadují parametry ukazatele s poznámkami – aby funkce byla úspěšná, musí mít hodnotu non-NULL. Nejčastěji používané varianty základních poznámek značí, že parametr ukazatele je nepovinný – pokud má hodnotu NULL, může funkce stále úspěšně fungovat.
Tato tabulka ukazuje, jak rozlišovat mezi požadovanými a volitelnými parametry:
Parametry jsou povinné. | Parametry jsou volitelné. | |
---|---|---|
Vstup do volané funkce | _In_ |
_In_opt_ |
Vstup do volané funkce a výstup volajícího | _Inout_ |
_Inout_opt_ |
Výstup volajícího | _Out_ |
_Out_opt_ |
Výstup ukazatele na volajícího | _Outptr_ |
_Outptr_opt_ |
Tyto poznámky pomáhají identifikovat možné neinicializované hodnoty a neplatný ukazatel 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 chybové ukončení nebo může způsobit vrácení kódu chyby "selhání". V obou směrech nemůže funkce úspěšně provést svou úlohu.
Příklady SAL
Tato část ukazuje příklady kódu pro základní poznámky SAL.
Použití nástroje Visual Studio Code Analysis Tool k vyhledání vad
V příkladech se nástroj pro analýzu kódu sady Visual Studio používá společně s poznámkami SAL k vyhledání vad kódu. Tady je postup, jak to udělat.
Použití nástrojů pro analýzu kódu sady Visual Studio a sal
V sadě Visual Studio otevřete projekt C++, který obsahuje poznámky SAL.
Na řádku nabídek zvolte Sestavit, spustit analýzu kódu v řešení.
Podívejte se na příklad _In_ v této části. Pokud spustíte analýzu kódu, zobrazí se toto upozornění:
C6387 Neplatná hodnota parametru pInt může být 0: toto není v souladu se specifikací funkce InCallee.
Příklad: Poznámka _In_
Poznámka _In_
označuje, že:
Parametr musí být platný a nebude změněn.
Funkce bude číst 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 chybou je použití_In_
parametru, který by místo toho měl mít poznámku_Inout_
._In_
je povolen, ale ignoruje ho analyzátor na skalárech bez ukazatele.
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
}
Pokud v tomto příkladu použijete analýzu editoru Visual Studio Code, ověří se, že volající předávají nenulový ukazatel na inicializovanou vyrovnávací paměť pro pInt
. V tomto případě pInt
nemůže být ukazatel NULL.
Příklad: Poznámka _In_opt_
_In_opt_
je stejná jako _In_
, s výjimkou toho, že vstupní parametr může mít hodnotu NULL, a proto by funkce měla tuto funkci zkontrolovat.
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 kódu v sadě Visual Studio ověří, že funkce před přístupem k vyrovnávací paměti kontroluje hodnotu NULL.
Příklad: Poznámka _Out_
_Out_
podporuje běžný scénář, ve kterém se předá ukazatel bez hodnoty NULL, který odkazuje na vyrovnávací paměť elementu, a funkce inicializuje prvek. Volající nemusí inicializovat vyrovnávací paměť před voláním; volaná funkce slibuje, že ji inicializuje předtím, 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 kódu sady Visual Studio ověří, že volající předá do vyrovnávací paměti pInt
ukazatel bez hodnoty NULL a že vyrovnávací paměť inicializuje funkce před jejím vrácením.
Příklad: Poznámka _Out_opt_
_Out_opt_
je stejný jako _Out_
parametr s tím rozdílem, že parametr může mít hodnotu NULL, a proto by funkce měla tuto funkci zkontrolovat.
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 kódu sady Visual Studio ověří, zda tato funkce kontroluje hodnotu NULL před pInt
zrušením odvození, a pokud pInt
není null, že vyrovnávací paměť je inicializována funkcí před jejím vrácením.
Příklad: Poznámka _Inout_
_Inout_
slouží k přidání poznámek k parametru ukazatele, který může funkce změnit. Ukazatel musí před voláním odkazovat na platná inicializovaná data, a to i v případě, že se změní, musí mít při vrácení platnou hodnotu. Poznámka určuje, že funkce může volně číst a zapisovat do vyrovnávací paměti jednoho prvku. Volající musí poskytnout vyrovnávací paměť a inicializovat ji.
Poznámka:
_Inout_
Podobně jako _Out_
se musí použít u upravitelné hodnoty.
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
}
Analýza kódu v sadě Visual Studio ověří, že volající předají inicializovaný vyrovnávací paměť pro pInt
inicializovanou vyrovnávací paměť a že před vrácením pInt
je stále non-NULL a vyrovnávací paměť je inicializována.
Příklad: Poznámka _Inout_opt_
_Inout_opt_
je stejná jako _Inout_
, s výjimkou toho, že vstupní parametr může mít hodnotu NULL, a proto by funkce měla tuto funkci 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);
}
Analýza kódu sady Visual Studio ověří, že tato funkce před přístupem k vyrovnávací paměti kontroluje hodnotu NULL, a pokud pInt
není NULL, že vyrovnávací paměť inicializuje funkce před jejím vrácením.
Příklad: Poznámka _Outptr_
_Outptr_
slouží k přidání poznámek k parametru, který má vrátit ukazatel. Samotný parametr by neměl být NULL a volaná funkce vrátí 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);
}
Analýza kódu sady Visual Studio ověří, že volající předá ukazatel bez hodnoty NULL a *pInt
že vyrovnávací paměť inicializuje funkce před jejím vrácením.
Příklad: Poznámka _Outptr_opt_
_Outptr_opt_
je stejný jako _Outptr_
parametr s tím rozdílem, že parametr je nepovinný – volající může předat ukazatel NULL parametru.
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);
}
Analýza editoru Visual Studio Code ověří, že tato funkce kontroluje hodnotu NULL před *pInt
dereferencedou a že vyrovnávací paměť inicializuje funkce před jejím vrácením.
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 nejobraznějších charakteristik funkce je, že může být úspěšná nebo neúspěšná. Podobně jako přidružení mezi vyrovnávací pamětí a její velikostí nemůže C/C++ vyjádřit úspěch nebo selhání funkce. Pomocí poznámky _Success_
můžete říct, jak vypadá úspěch funkce. Parametr poznámky _Success_
je jen výraz, který když je pravdivý, znamená to, že funkce byla úspěšná. Výraz může být cokoli, co může analyzátor poznámek zpracovat. Efekty poznámek po vrácení funkce se použijí pouze v případě, že je funkce úspěšná. Tento příklad ukazuje, jak _Success_
interaguje s _Out_
tou správnou věcí. Klíčové slovo return
můžete použít k reprezentaci návratové hodnoty.
_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 _Out_
způsobí, že analýza editoru Visual Studio Code ověří, že volající předá do vyrovnávací paměti pInt
jiný ukazatel než NULL a že vyrovnávací paměť inicializuje funkce před jejím vrácením.
Osvědčený postup sal
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. Jakmile se naučíte SAL, můžete novou dovednost uplatnit na svou každodenní práci. V novém kódu můžete použít specifikace založené na SAL návrhu v celém rozsahu; ve starším kódu můžete přidávat poznámky postupně a tím zvýšit výhody při každé aktualizaci.
Veřejné hlavičky Microsoftu jsou již opatřeny poznámkami. Proto doporučujeme, abyste ve svých projektech nejprve přidali poznámky k funkcím a funkcím uzlu typu list, které volají rozhraní API Win32, abyste získali největší výhodu.
Kdy poznámek?
Tady je několik pokynů:
Označte všechny parametry ukazatele poznámkami.
Přidávání poznámek k rozsahu hodnot, aby analýza kódu zajistila bezpečnost vyrovnávací paměti a ukazatele.
Přidání poznámek k pravidlu uzamčení a uzamčení vedlejších efektů Další informace naleznete v tématu Přidávání poznámek k chování uzamčení.
Označte vlastnosti ovladače a další vlastnosti specifické pro doménu.
Nebo můžete anotovat všechny parametry, aby byl záměr v celém prostředí jasný a aby bylo možné snadno zkontrolovat, že poznámky byly 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