Sdílet prostřednictvím


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 memcpymodulu 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:

  • memcpycount 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 wmemcpychyby:

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

  1. V sadě Visual Studio otevřete projekt C++, který obsahuje poznámky SAL.

  2. 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 pIntinicializovanou 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 pIntjiný 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é