Megosztás:


A SAL ismertetése

A Microsoft forráskódos széljegyzetnyelve (SAL) olyan széljegyzeteket biztosít, amelyekkel leírhatja, hogy egy függvény hogyan használja a paramétereit, milyen feltételezéseket tesz ezekről, és milyen garanciákat biztosít, amikor befejeződik. A széljegyzetek <sal.h>fejlécfájlban vannak definiálva. A C++ Visual Studio kódelemzése SAL-széljegyzetekkel módosítja a függvények elemzését. A Windows-illesztőprogramok fejlesztéséhez készült SAL 2.0-ról további információt a Windows-illesztőprogramok SAL 2.0-s széljegyzetei című témakörben talál.

Natív módon a C és a C++ csak korlátozott lehetőségeket biztosít a fejlesztők számára a szándék és az állandóság következetes kifejezésére. A SAL-széljegyzetek használatával részletesebben is leírhatja a függvényeket, hogy az őket használó fejlesztők jobban megértsék a használatukat.

Mi az a SAL és miért érdemes használni?

Egyszerűen fogalmazva, az SAL egy olcsó módja annak, hogy a fordító ellenőrizze a kódot.

Az SAL értékesebbé teszi a kódot

A SAL segítségével érthetőbbé teheti a kódtervezést mind az emberek, mind a kódelemzési eszközök számára. Tekintse meg ezt a példát, amely a C futtatókörnyezet függvényt memcpymutatja be:

void * memcpy(
   void *dest,
   const void *src,
   size_t count
);

Meg tudja állapítani, hogy ez a függvény mit csinál? Ha egy függvényt implementál vagy meghív, bizonyos tulajdonságokat fenn kell tartani a program helyességének biztosítása érdekében. Csak egy olyan deklarációt tekintve, mint a példában, nem tudja, mik ezek. SAL-széljegyzetek nélkül dokumentációra vagy kód megjegyzéseire kell támaszkodnia. A dokumentáció a memcpy-ról a következőket mondja:

"memcpy átmásolja count bájtok számát a forrásból a destbe; wmemcpy átmásolja count széles karaktert (két bájtot)." Ha a forrás és a cél átfedésben van, a rendszer nem definiálja a viselkedést memcpy . Az átfedésben lévő régiók kezelésére használható memmove .
Fontos: Győződjön meg arról, hogy a célpuffer mérete megegyezik vagy nagyobb, mint a forráspuffer. További információ: Puffertúllépések elkerülése."

A dokumentáció néhány bitnyi információt tartalmaz, amelyek arra utalnak, hogy a kódnak bizonyos tulajdonságokat kell fenntartania a program helyességének biztosítása érdekében:

  • memcpy átmásolja a bájtokat a count forráspufferből a célpufferbe.

  • A célpuffernek legalább akkoranak kell lennie, mint a forráspuffer.

A fordító azonban nem tudja elolvasni a dokumentációt vagy az informális megjegyzéseket. Nem tudja, hogy van-e kapcsolat a két puffer countközött, és nem tud hatékonyan kitalálni egy kapcsolatot. A SAL a függvény tulajdonságaival és implementálásával kapcsolatos egyértelműbb információkat nyújthat, ahogy az itt látható:

void * memcpy(
   _Out_writes_bytes_all_(count) void *dest,
   _In_reads_bytes_(count) const void *src,
   size_t count
);

Figyelje meg, hogy ezek a széljegyzetek hasonlítanak a dokumentációban szereplő információkra, de tömörebbek, és szemantikai mintát követnek. A kód elolvasásakor gyorsan megértheti a függvény tulajdonságait, és hogy miként kerülheti el a puffertúlcsordulás biztonsági problémáit. Még jobb, hogy a szemantikai minták, amelyeket az SAL nyújt, javíthatják az automatizált kódelemzési eszközök hatékonyságát és eredményességét a potenciális hibák korai felfedezésében. Tegyük fel, hogy valaki a következő hibás implementációt wmemcpyírja:

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;
}

Ez az implementáció egy gyakori, egyszeri hibát tartalmaz. Szerencsére a kód szerzője tartalmazta a SAL pufferméret-megjegyzést – a kódelemző eszköz egyedül a függvény elemzésével tudta elkapni a hibát.

Az SAL alapjai

A SAL négy alapvető paramétertípust határoz meg, amelyeket használati minta szerint kategorizál.

Kategória Paraméter széljegyzete Leírás
Bemenet a hívott függvényhez _In_ Az adatok továbbítva lesznek a hívott függvénynek, és írásvédettként lesznek kezelve.
Bemenet a hívott függvényhez, és kimenet a hívónak _Inout_ A használható adatok továbbítva lesznek a függvénybe, és potenciálisan módosulnak.
Kimenet a hívónak _Out_ A hívó csak helyet biztosít a hívott függvénynek az íráshoz. A hívott függvény adatokat ír ebbe a térbe.
Mutató kimenete a hívónak _Outptr_ Például a hívó kimenete. A hívott függvény által visszaadott érték egy mutató.

Ez a négy alapjegyzet többféleképpen is explicitebbé tehető. Alapértelmezés szerint a jegyzetekkel ellátott mutatóparaméterek kötelezőek – a függvény sikeréhez nem null értékűnek kell lenniük. Az alapjegyzetek leggyakrabban használt változata azt jelzi, hogy a mutatóparaméter nem kötelező – null érték esetén a függvény továbbra is sikeresen elvégezheti a munkáját.

Ez a táblázat bemutatja, hogyan lehet különbséget tenni a szükséges és a választható paraméterek között:

Paraméterekre van szükség A paraméterek megadása nem kötelező
Bemenet a hívott függvényhez _In_ _In_opt_
Bemenet a hívott függvényhez, és kimenet a hívónak _Inout_ _Inout_opt_
Kimenet a hívónak _Out_ _Out_opt_
Mutató kimenete a hívónak _Outptr_ _Outptr_opt_

Ezek a széljegyzetek segítenek azonosítani a lehetséges nem inicializált értékeket, valamint az érvénytelen null mutató formális és pontos használatát. A NULL megadása egy szükséges paraméternek összeomlást okozhat, vagy "sikertelen" hibakód visszaadását okozhatja. Akárhogy is, a függvény nem tudja elvégezni a feladatát.

SAL-példák

Ez a szakasz az alapszintű SAL-széljegyzetek kód példáit mutatja be.

Hibák keresése a Visual Studio kódelemző eszközével

A példákban a Visual Studio kódelemző eszközét SAL-széljegyzetekkel együtt használjuk a kódhibák kereséséhez. Ezt a következőképpen teheti meg.

Visual Studio-kódelemzési eszközök és SAL használata

  1. A Visual Studióban nyisson meg egy C++ projektet, amely SAL-széljegyzeteket tartalmaz.

  2. A menüsávon válassza a Build, a Kódelemzés futtatása a megoldáson lehetőséget.

    Tekintse meg az ebben a szakaszban található _In_ példát. Ha kódelemzést futtat rajta, ez a figyelmeztetés jelenik meg:

    C6387 Érvénytelen paraméterérték A "pInt" lehet "0": ez nem felel meg az "InCallee" függvény specifikációjának.

Példa: Az _In_ jelölés

A _In_ széljegyzet a következőt jelzi:

  • A paraméternek érvényesnek kell lennie, és nem módosítható.

  • A függvény csak az egyelemes pufferből lesz beolvasva.

  • A hívónak meg kell adnia a puffert, és inicializálnia kell azt.

  • _In_ az "írásvédett" értéket adja meg. Gyakori hiba, hogy olyan paraméterre kell alkalmazni _In_ , amely helyett a _Inout_ széljegyzetnek kell lennie.

  • _In_ a nem mutató skalárok esetében engedélyezett, de az elemző figyelmen kívül hagyja.

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
}

Ha Visual Studio-kódelemzést használ ebben a példában, az ellenőrzi, hogy a hívók átadják-e egy inicializált puffernek a nem null értékű mutatót pInt. Ebben az esetben a pInt mutató nem lehet NULL értékű.

Példa: A _In_opt_ annotáció

_In_opt_ megegyezik _In_azzal a kivétellel, hogy a bemeneti paraméter null értékű lehet, ezért a függvénynek ellenőriznie kell ezt.

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);
}

A Visual Studio kódelemzése ellenőrzi, hogy a függvény null értéket keres-e, mielőtt hozzáfér a pufferhez.

Példa: A _Out_ annotáció

_Out_ Támogat egy olyan gyakori forgatókönyvet, amelyben egy elempufferre mutató, nem NULL mutatót adnak át, és a függvény inicializálja az elemet. A hívónak nem kell inicializálnia a puffert a hívás előtt; a hívott függvény azt ígéri, hogy inicializálja azt, mielőtt visszatér.

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;
}

A Visual Studio kódelemzése ellenőrzi, hogy a hívó egy nem NULL mutatót ad-e át egy puffernek pInt , és hogy a függvény inicializálja a puffert, mielőtt visszatér.

Példa: A _Out_opt_ annotáció

_Out_opt_ megegyezik _Out_azzal a kivételel, hogy a paraméter null értékű, ezért a függvénynek ellenőriznie kell ezt.

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);
}

A Visual Studio kódelemzése megerősíti, hogy a függvény ellenőrzi, hogy pInt értéke NULL-e, mielőtt hivatkozna rá, és ha pInt nem NULL, akkor a függvény inicializálja a puffert, mielőtt visszatérne.

Példa: Az _Inout_ annotáció

_Inout_ olyan mutatóparaméter megjegyzésére szolgál, amelyet a függvény esetleg módosíthat. Az egérmutatónak a hívás előtt érvényes inicializált adatokra kell mutatnia, és még ha változik is, akkor is érvényes értékkel kell rendelkeznie. A széljegyzet azt határozza meg, hogy a függvény szabadon olvashat és írhat az egyelemes pufferbe. A hívónak meg kell adnia a puffert, és inicializálnia kell azt.

Megjegyzés

Az _Out_-hoz hasonlóan a _Inout_-nek is egy módosítható értékre kell vonatkoznia.

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
}

A Visual Studio kódelemzése ellenőrzi, hogy a hívók egy nem NULL mutatót adnak át egy inicializált pufferhez pInt, és hogy a visszatérés előtt pInt továbbra is nem NULL értékű, és a puffer inicializálva van.

Példa: A _Inout_opt_ annotáció

_Inout_opt_ megegyezik _Inout_azzal a kivétellel, hogy a bemeneti paraméter null értékű lehet, ezért a függvénynek ellenőriznie kell ezt.

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);
}

A Visual Studio kódelemzése ellenőrzi, hogy ez a függvény a NULL értéket ellenőrzi-e, mielőtt hozzáfér a pufferhez, és ha pInt nem NULL, akkor a függvény inicializálja a puffert, mielőtt visszatér.

Példa: A _Outptr_ annotáció

_Outptr_ olyan paraméter megjegyzésére szolgál, amely egy mutató visszaadására szolgál. Maga a paraméter nem lehet NULL, és a hívott függvény egy nem NULL mutatót ad vissza benne, és ez a mutató inicializált adatokra mutat.

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);
}

A Visual Studio kódelemzése ellenőrzi, hogy a hívó egy nem NULL mutatót *pIntad-e át, és hogy a függvény inicializálja a puffert, mielőtt visszatér.

Példa: A _Outptr_opt_ jegyzet

_Outptr_opt_ ugyanaz, mint _Outptr_– kivéve, hogy a paraméter nem kötelező – a hívó null mutatót adhat a paraméterhez.

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);
}

A Visual Studio kódelemzése ellenőrzi, hogy ez a függvény a null *pInt értéket ellenőrzi-e a dereferencia előtt, és hogy a függvény inicializálja a puffert, mielőtt visszatér.

Példa: A _Success_ annotáció kombinálva az _Out_ -tal

A széljegyzetek a legtöbb objektumra alkalmazhatók. Különösen, egy egész függvényt is kommentelhet. A függvények egyik legnyilvánvalóbb jellemzője, hogy sikeresek vagy sikertelenek lehetnek. A puffer és a mérete közötti társításhoz hasonlóan a C/C++ nem tudja kifejezni a függvény sikerességét vagy meghibásodását. A _Success_ jelölés használatával meghatározhatja, hogy milyen egy függvény sikeres működése. Az _Success_ annotáció paramétere csupán egy olyan kifejezés, amelyik igaz, amikor azt jelzi, hogy a függvény sikerült. A kifejezés bármi lehet, amit a széljegyzetelemző kezelni tud. A függvény visszatérését követő széljegyzetek hatása csak akkor alkalmazható, ha a függvény sikeres. Ez a példa bemutatja, hogyan _Success_ működik együtt _Out_ a helyes dolgot elérve. A kulcsszóval return a visszatérési értéket jelölheti.

_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;
   }
}

A _Out_ megjegyzés hatására a Visual Studio kódelemzése ellenőrzi, hogy a hívó egy nem NULL mutatót ad-e át egy puffernek pInt, és hogy a függvény inicializálja a puffert, mielőtt visszatér.

SAL legjobb gyakorlat

Széljegyzetek hozzáadása meglévő kódhoz

A SAL egy hatékony technológia, amely segíthet a kód biztonságának és megbízhatóságának javításában. A SAL elsajátítása után alkalmazhatja az új készséget a napi munkájára. Az új kódban az SAL-alapú specifikációkat a teljes terv szerint használhatja; a régebbi kódban növekményesen adhat hozzá széljegyzeteket, és ezáltal minden frissítéskor növelheti az előnyöket.

A Nyilvános Microsoft-fejlécek már széljegyzetekkel vannak eljegyzve. Ezért javasoljuk, hogy a legnagyobb előny eléréséhez a projektjeiben először jelölje meg a levélcsomópont-függvényeket és azokat a függvényeket, amelyek Win32 API-kat hívnak.

Mikor jegyzetelek?

Íme néhány irányelv:

  • Jegyzetelje az összes mutatóparamétert.

  • Jegyzetelje az értéktartomány-széljegyzeteket, hogy a kódelemzés biztosítsa a puffer és a mutató biztonságát.

  • Széljegyzetek a zárolási szabályok és a zárolási mellékhatások. A további információkért lásd: A zárolási viselkedés annotálása.

  • Megjegyzést fűz az illesztőprogram tulajdonságaihoz és más tartományspecifikus tulajdonságokhoz.

Megjegyzéseket is fűzhet az összes paraméterhez, hogy a szándéka teljes egészében egyértelmű legyen, és hogy könnyen ellenőrizze, hogy a széljegyzetek elkészültek-e.

Lásd még