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 elementumemcpy
jest niezdefiniowane. Służymemmove
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
kopiujecount
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 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
W programie Visual Studio otwórz projekt C++, który zawiera adnotacje SAL.
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 pInt
elementu 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 *pInt
elementu , 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 pInt
elementu 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ż
- Korzystanie z adnotacji SAL w celu zmniejszenia liczby defektów kodu C/C++
- Dodawanie adnotacji do parametrów funkcji i zwracanych wartości
- Zachowanie funkcji dodawania adnotacji
- Dodawanie adnotacji do struktur i klas
- Dodawanie adnotacji do zachowania blokującego
- Określanie miejsca i warunków stosowania adnotacji
- Najlepsze rozwiązania i przykłady