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

"memcpy zkopíruje počet bajtů z src do dest; wmemcpy zkopíruje počet širokých znaků (dva bajty)." Pokud se zdroj a cíl překrývají, chování memcpy není definováno. Použijte 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 zkopíruje count poč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

  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 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.

Viz také