Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
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 udělat návrh kódu srozumitelnějším jak pro lidi, tak pro nástroje pro analýzu kódu. Podívejte se na tento příklad, který ukazuje C runtime funkci 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, musí být zachovány určité vlastnosti, aby byla zajištěna správnost programu. Podíváte-li se jen na deklaraci, jako je v příkladu, nevíte, čím jsou. Bez poznámek SAL byste se museli spoléhat na dokumentaci nebo komentáře ke kódu. Tady je to, co říká dokumentace memcpy:
"
memcpyzkopíruje počet bajtů z src do dest;wmemcpyzkopíruje počet širokých znaků (dva bajty)." Pokud se zdroj a cíl překrývají, chovánímemcpynení definováno. Použijtememmoveke 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:
memcpyzkopírujecountpočet bajtů 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 a count existuje vztah, a také nedokáže efektivně odhadnout, zda vztah existuje. 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 chybnou implementaci 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 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ímu | _Inout_ |
Použitelná data se předávají do funkce a potenciálně se mění. |
| Výstup volajícímu | _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 volajícímu | _Outptr_ |
Jako 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ímu | _Inout_ |
_Inout_opt_ |
| Výstup volajícímu | _Out_ |
_Out_opt_ |
| Výstup ukazatele volajícímu | _Outptr_ |
_Outptr_opt_ |
Tyto poznámky pomáhají identifikovat možné neinicializované hodnoty a neplatné použití ukazatele null 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 pro analýzu kódu sady Visual Studio ke zjiště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žít nástroje 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 pro ř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 jednoprvkové vyrovnávací paměti.
Volající musí poskytnout vyrovnávací paměť a inicializovat ji.
_In_označuje pouze 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 kódu sady Visual Studio, potvrdí se, že volající předávají nenulový ukazatel do inicializované vyrovnávací paměti 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 sady 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 je předán ukazatel bez hodnoty NULL, jenž odkazuje na vyrovnávací paměť pro prvky, 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;
}
Analýza kódu sady Visual Studio 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.
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ěřuje, že tato funkce kontroluje hodnotu NULL před zrušením reference z pInt, a pokud pInt není NULL, je vyrovnávací paměť inicializována funkcí předtím, než ji funkce vrátí.
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:
_Out_ Podobně jako _Inout_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 sady Visual Studio ověří, že volající předají ukazatel bez hodnoty NULL do inicializované vyrovnávací paměti pro pInt, a že před vrácením pInt je stále non-NULL a vyrovnávací paměť je inicializována.
Příklad: Anotace _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ěří, zda tato funkce před přístupem k vyrovnávací paměti kontroluje hodnotu NULL, a pokud pInt není NULL, že vyrovnávací paměť je inicializována funkcí 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 kódu Visual Studio ověřuje, že tato funkce kontroluje hodnotu NULL před dereferencí a že vyrovnávací paměť je inicializována funkcí před jejím návratem.
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 vztah 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_ k tomu, aby udělal 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 kódu ve Visual Studiu posoudí, že volající předá do vyrovnávací paměti pInt nenulový ukazatel a že vyrovnávací paměť je inicializována funkcí 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 uzlovým funkcím typu list a k funkcím, které volají rozhraní API Win32, abyste získali co nejvíce výhod.
Kdy anotuji?
Tady je několik pokynů:
Označte všechny parametry ukazatele poznámkami.
Anotujte rozsah hodnot, aby analýza kódu mohla zabezpečit bezpečnost vyrovnávací paměti a ukazatele.
Okomentujte pravidla uzamčení a vedlejší účinky uzamčení. Další informace naleznete v tématu Poznámkování chování uzamykání.
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.