Udostępnij przez


Poznanie SAL

Język adnotacji kodu źródłowego (SAL) firmy Microsoft udostępnia zestaw adnotacji, których można użyć do opisania sposobu używania jego parametrów przez funkcję, założeń, które ich dotyczy, oraz gwarancji, które wykonuje po zakończeniu. Adnotacje są definiowane w pliku <sal.h>nagłówka . Analiza kodu programu Visual Studio dla języka C++ używa adnotacji SAL do modyfikowania analizy funkcji. Aby uzyskać więcej informacji na temat sal 2.0 na potrzeby opracowywania sterowników systemu Windows, zobacz Adnotacje SAL 2.0 dla sterowników systemu Windows.

Natywnie języki C i C++ zapewniają tylko ograniczone sposoby, aby deweloperzy konsekwentnie wyrażali intencję i wariancję. Korzystając z adnotacji SAL, możesz szczegółowo opisać swoje funkcje, aby deweloperzy korzystający z nich mogli lepiej zrozumieć, jak ich używać.

Co to jest SAL i dlaczego należy go używać?

Po prostu stwierdził, SAL to niedrogi sposób, aby umożliwić kompilatorowi sprawdzenie kodu.

SAL sprawia, że kod jest bardziej cenny

Sal może ułatwić zrozumienie projektu kodu zarówno dla ludzi, jak i narzędzi do analizy kodu. Rozważmy ten przykład pokazujący funkcję memcpyśrodowiska uruchomieniowego języka C:

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

Czy możesz powiedzieć, co robi ta funkcja? Po zaimplementowaniu lub wywołaniu funkcji należy zachować niektóre właściwości, aby zapewnić poprawność programu. Po prostu patrząc na deklarację, taką jak w przykładzie, nie wiesz, czym są. Bez adnotacji SAL należy polegać na dokumentacji lub komentarzach kodu. Poniżej przedstawiono dokumentację dotyczącą memcpy następujących dokumentacji:

"memcpy kopiuje zlicza bajty z src do dest; wmemcpy kopiuje zlicza szerokie znaki (dwa bajty). Jeśli źródło i miejsce docelowe nakładają się na siebie, zachowanie elementu memcpy jest niezdefiniowane. Służy memmove do obsługi nakładających się regionów.
Ważne: upewnij się, że bufor docelowy ma ten sam rozmiar lub większy niż bufor źródłowy. Aby uzyskać więcej informacji, zobacz Unikanie przekroków buforu.

Dokumentacja zawiera kilka bitów informacji sugerujących, że kod musi zachować pewne właściwości, aby zapewnić poprawność programu:

  • memcpy kopiuje count bajty z buforu źródłowego do buforu docelowego.

  • Bufor docelowy musi być co najmniej tak duży, jak bufor źródłowy.

Jednak kompilator nie może odczytać dokumentacji ani nieformalnych komentarzy. Nie wiadomo, że istnieje relacja między dwoma buforami i count, a także nie może skutecznie odgadnąć relacji. Sal może zapewnić większą przejrzystość właściwości i implementacji funkcji, jak pokazano poniżej:

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

Zwróć uwagę, że te adnotacje przypominają informacje w dokumentacji, ale są one bardziej zwięzłe i są zgodne ze wzorcem semantycznym. Po przeczytaniu tego kodu możesz szybko zrozumieć właściwości tej funkcji i uniknąć problemów z zabezpieczeniami przepełnienia buforu. Jeszcze lepiej, semantyczne wzorce, które zapewnia SAL, mogą poprawić wydajność i skuteczność zautomatyzowanych narzędzi do analizy kodu we wczesnym odnajdywaniem potencjalnych usterek. Załóżmy, że ktoś pisze tę implementację usterek w programie 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;
}

Ta implementacja zawiera typowy błąd poza jednym. Na szczęście autor kodu uwzględnił adnotację rozmiaru buforu SAL — narzędzie do analizy kodu może przechwycić usterkę, analizując tę funkcję samodzielnie.

Podstawy SAL

Sal definiuje cztery podstawowe rodzaje parametrów, które są podzielone na kategorie według wzorca użycia.

Kategoria Adnotacja parametru opis
Dane wejściowe do wywoływanej funkcji _In_ Dane są przekazywane do wywoływanej funkcji i są traktowane jako tylko do odczytu.
Dane wejściowe do wywoływanej funkcji i dane wyjściowe wywołujące _Inout_ Dane użyteczne są przekazywane do funkcji i potencjalnie modyfikowane.
Dane wyjściowe elementu wywołującego _Out_ Obiekt wywołujący zapewnia tylko miejsce dla wywoływanej funkcji do zapisu. Wywołana funkcja zapisuje dane w tym miejscu.
Dane wyjściowe wskaźnika do obiektu wywołującego _Outptr_ Na przykład dane wyjściowe do elementu wywołującego. Wartość zwracana przez wywołaną funkcję jest wskaźnikiem.

Te cztery podstawowe adnotacje mogą być bardziej wyraźne na różne sposoby. Domyślnie przyjmuje się, że wymagane są parametry wskaźnika z adnotacjami — muszą mieć wartość inną niż NULL, aby funkcja zakończyła się pomyślnie. Najczęściej używana odmiana adnotacji podstawowych wskazuje, że parametr wskaźnika jest opcjonalny — jeśli ma wartość NULL, funkcja nadal może pomyślnie wykonać swoją pracę.

W tej tabeli przedstawiono sposób rozróżniania wymaganych i opcjonalnych parametrów:

Wymagane są parametry Parametry są opcjonalne
Dane wejściowe do wywoływanej funkcji _In_ _In_opt_
Dane wejściowe do wywoływanej funkcji i dane wyjściowe wywołujące _Inout_ _Inout_opt_
Dane wyjściowe elementu wywołującego _Out_ _Out_opt_
Dane wyjściowe wskaźnika do obiektu wywołującego _Outptr_ _Outptr_opt_

Te adnotacje pomagają zidentyfikować możliwe niezainicjowane wartości i nieprawidłowe użycie wskaźnika null w formalny i dokładny sposób. Przekazanie wartości NULL do wymaganego parametru może spowodować awarię lub może spowodować zwrócenie kodu błędu "niepowodzenie". Tak czy inaczej, funkcja nie może wykonać zadania.

Przykłady sal

W tej sekcji przedstawiono przykłady kodu dla podstawowych adnotacji SAL.

Znajdowanie wad za pomocą narzędzia Visual Studio Code Analysis Tool

W przykładach narzędzie Visual Studio Code Analysis jest używane razem z adnotacjami SAL w celu znalezienia wad kodu. Oto jak to zrobić.

Aby używać narzędzi do analizy kodu programu Visual Studio i sal

  1. W programie Visual Studio otwórz projekt C++, który zawiera adnotacje SAL.

  2. Na pasku menu wybierz pozycję Kompiluj, Uruchom analizę kodu w rozwiązaniu.

    Rozważmy przykład _In_ w tej sekcji. Jeśli uruchomisz na nim analizę kodu, zostanie wyświetlone następujące ostrzeżenie:

    Nieprawidłowa wartość parametru C6387 "pInt" może wynosić "0": nie jest zgodna ze specyfikacją funkcji "InCallee".

Przykład: adnotacja _In_

Adnotacja _In_ wskazuje, że:

  • Parametr musi być prawidłowy i nie zostanie zmodyfikowany.

  • Funkcja będzie odczytywać tylko z buforu pojedynczego elementu.

  • Obiekt wywołujący musi podać bufor i zainicjować go.

  • _In_ określa wartość "tylko do odczytu". Typowym błędem jest zastosowanie _In_ do parametru, który powinien mieć adnotację _Inout_ zamiast tego.

  • _In_ parametr jest dozwolony, ale ignorowany przez analizator w skalarach innych niż wskaźnik.

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
}

Jeśli używasz analizy programu Visual Studio Code w tym przykładzie, sprawdza, czy obiekty wywołujące przekazują wskaźnik o wartości innej niż null do zainicjowanego buforu dla elementu pInt. W takim przypadku pInt wskaźnik nie może mieć wartości NULL.

Przykład: adnotacja _In_opt_

_In_opt_ parametr jest taki sam jak _In_, z tą różnicą, że parametr wejściowy może mieć wartość NULL i dlatego funkcja powinna sprawdzić to.

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

Analiza programu Visual Studio Code sprawdza, czy funkcja sprawdza wartość NULL przed uzyskaniem dostępu do buforu.

Przykład: adnotacja _Out_

_Out_ obsługuje typowy scenariusz, w którym wskaźnik inny niż NULL wskazujący bufor elementu jest przekazywany, a funkcja inicjuje element. Obiekt wywołujący nie musi zainicjować buforu przed wywołaniem; wywołana funkcja obiecuje zainicjować ją przed jej zwróceniem.

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

Narzędzie Visual Studio Code Analysis Tool sprawdza, czy obiekt wywołujący przekazuje wskaźnik o wartości innej niż NULL do buforu pInt i że bufor jest inicjowany przez funkcję przed zwróceniem.

Przykład: adnotacja _Out_opt_

_Out_opt_ parametr jest taki sam jak _Out_, z tą różnicą, że parametr może mieć wartość NULL i dlatego funkcja powinna sprawdzić to.

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

Analiza programu Visual Studio Code sprawdza, czy ta funkcja sprawdza wartość NULL przed pInt wyłuszczeniem, a jeśli pInt nie ma wartości NULL, bufor jest inicjowany przez funkcję przed jej zwróceniem.

Przykład: adnotacja _Inout_

_Inout_ Służy do dodawania adnotacji do parametru wskaźnika, który może zostać zmieniony przez funkcję. Wskaźnik musi wskazywać prawidłowe zainicjowane dane przed wywołaniem, a nawet jeśli ulegnie zmianie, nadal musi mieć prawidłową wartość po powrocie. Adnotacja określa, że funkcja może swobodnie odczytywać i zapisywać w buforze jednego elementu. Obiekt wywołujący musi podać bufor i zainicjować go.

Uwaga

Podobnie jak _Out_, _Inout_ musi mieć zastosowanie do wartości modyfikowalnej.

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
}

Analiza programu Visual Studio Code sprawdza, czy obiekty wywołujące przekazują wskaźnik inny niż NULL do zainicjowanego buforu dla pIntelementu i że przed zwróceniem pInt jest nadal inny niż NULL, a bufor jest inicjowany.

Przykład: adnotacja _Inout_opt_

_Inout_opt_ parametr jest taki sam jak _Inout_, z tą różnicą, że parametr wejściowy może mieć wartość NULL i dlatego funkcja powinna sprawdzić to.

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

Analiza programu Visual Studio Code sprawdza, czy ta funkcja sprawdza wartość NULL przed uzyskaniem dostępu do buforu, a jeśli pInt nie ma wartości NULL, bufor jest inicjowany przez funkcję przed jej zwróceniem.

Przykład: adnotacja _Outptr_

_Outptr_ Służy do dodawania adnotacji do parametru, który ma zwrócić wskaźnik. Sam parametr nie powinien mieć wartości NULL, a wywołana funkcja zwraca w nim wskaźnik inny niż NULL, a wskaźnik wskazuje na zainicjowane dane.

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

Analiza programu Visual Studio Code sprawdza, czy obiekt wywołujący przekazuje wskaźnik inny niż NULL dla *pIntelementu , a bufor jest inicjowany przez funkcję przed zwróceniem.

Przykład: adnotacja _Outptr_opt_

_Outptr_opt_ parametr jest taki sam jak _Outptr_parametr , z tą różnicą, że parametr jest opcjonalny — obiekt wywołujący może przekazać wskaźnik NULL dla 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);
}

Analiza programu Visual Studio Code sprawdza, czy ta funkcja sprawdza wartość NULL przed *pInt wyłuszczeniem i że bufor jest inicjowany przez funkcję przed jej zwróceniem.

Przykład: adnotacja _Success_ w połączeniu z _Out_

Adnotacje można stosować do większości obiektów. W szczególności można dodawać adnotacje do całej funkcji. Jedną z najbardziej oczywistych cech funkcji jest to, że może ona zakończyć się powodzeniem lub niepowodzeniem. Ale podobnie jak skojarzenie między buforem a jego rozmiarem, C/C++ nie może wyrazić powodzenia lub niepowodzenia funkcji. Używając adnotacji _Success_ , możesz powiedzieć, jak wygląda powodzenie funkcji. Parametr adnotacji _Success_ jest tylko wyrażeniem, które w przypadku wartości true wskazuje, że funkcja zakończyła się pomyślnie. Wyrażenie może być niczym, co może obsłużyć analizator adnotacji. Efekty adnotacji po zwracaniu funkcji mają zastosowanie tylko wtedy, gdy funkcja zakończy się pomyślnie. W tym przykładzie pokazano, jak _Success_ współdziała z _Out_ nimi, aby wykonać odpowiednie czynności. Możesz użyć słowa kluczowego return , aby reprezentować wartość zwracaną.

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

Adnotacja _Out_ powoduje, że analiza programu Visual Studio Code sprawdza, czy obiekt wywołujący przekazuje wskaźnik o wartości innej niż NULL do buforu dla pIntelementu i że bufor jest inicjowany przez funkcję przed zwróceniem.

Najlepsze rozwiązanie SAL

Dodawanie adnotacji do istniejącego kodu

SAL to zaawansowana technologia, która może pomóc w zwiększeniu bezpieczeństwa i niezawodności kodu. Po zapoznaniu się z SAL możesz zastosować nowe umiejętności do codziennej pracy. W nowym kodzie można używać specyfikacji opartych na sal zgodnie z projektem w całym projekcie; w starszym kodzie można dodawać adnotacje przyrostowo, a tym samym zwiększać korzyści przy każdej aktualizacji.

Nagłówki publiczne firmy Microsoft są już adnotacjami. Dlatego zalecamy, aby w projektach najpierw dodawać adnotacje do funkcji węzła liścia i funkcji wywołujących interfejsy API Win32, aby uzyskać największą korzyść.

Kiedy dodawać adnotacje?

Oto kilka wskazówek:

  • Dodaj adnotację do wszystkich parametrów wskaźnika.

  • Dodaj adnotacje zakresu wartości, aby analiza kodu mogła zapewnić bezpieczeństwo buforu i wskaźnika.

  • Dodawanie adnotacji do reguł blokowania i blokowanie skutków ubocznych. Aby uzyskać więcej informacji, zobacz Dodawanie adnotacji do zachowania blokującego.

  • Dodawanie adnotacji do właściwości sterownika i innych właściwości specyficznych dla domeny.

Możesz też dodać adnotacje do wszystkich parametrów, aby intencja została wyczysztana i ułatwić sprawdzenie, czy adnotacje zostały wykonane.

Zobacz też