Nuta
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować się zalogować lub zmienić katalog.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
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. Plik nagłówkowy <sal.h> definiuje adnotacje. 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 do 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 mogli konsekwentnie wyrażać zamierzenia i niezmienność. 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. Oto co dokumentacja dla memcpy mówi:
"
memcpykopiuje liczbę bajtów z src do dest;wmemcpykopiuje liczbę szerokich znaków (dwa bajty)." Jeśli źródło i miejsce docelowe nakładają się na siebie, zachowanie elementumemcpyjest niezdefiniowane. Użyjmemmove, aby obsługiwać nakładające się regiony.
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:
memcpykopiujecountbajty 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 a count, i nie może też skutecznie odgadnąć żadnego związku. 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. Wyobraź sobie, że ktoś pisze tę wadliwą implementację 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 dla wywoływanej funkcji i dane wyjściowe dla wywołującego | _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 dla wywoływanej funkcji i dane wyjściowe dla wywołującego | _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 do analizy kodu programu Visual Studio
W przykładach narzędzie analizy kodu programu Visual Studio jest używane razem z adnotacjami SAL w celu znalezienia wad kodu. Oto jak to zrobić.
Aby używać narzędzi do analizy kodu 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 części. 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 jednoelementowego bufora.
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_jest dozwolony, ale ignorowany przez analizator w skalarach niebędących wskaźnikami.
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 kodu Visual Studio w tym przykładzie, sprawdza, czy wywołujący przekazują wskaźnik inny 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 kodu programu Visual Studio 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;
}
Analiza kodu programu Visual Studio sprawdza, czy obiekt wywołujący przekazuje wskaźnik inny 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 kodu w Visual Studio weryfikuje, że ta funkcja sprawdza obecność wartości NULL przed dereferencjonowaniem pInt, a jeśli pInt nie jest 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 jednoelementowym buforze. 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 kodu programu Visual Studio sprawdza, czy obiekty wywołujące przekazują wskaźnik o wartości różnej od NULL do zainicjowanego buforu dla pInt elementu oraz że przed zwrotem pInt nadal nie ma wartości NULL, a bufor jest zainicjowany.
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 kodu programu Visual Studio 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 kodu w Visual Studio sprawdza, czy obiekt wywołujący przekazuje wskaźnik inny niż NULL dla elementu *pInt, i czy bufor został zainicjowany przez funkcję przed jej zwróceniem.
Przykład: adnotacja _Outptr_opt_
_Outptr_opt_ jest taki sam jak _Outptr_, z tą różnicą, że parametr jest opcjonalny — obiekt wywołujący może przekazać wartość NULL jako wskaźnik 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 kodu Visual Studio weryfikuje, że funkcja sprawdza wartość NULL przed odwołaniem do *pInt, oraz że bufor jest inicjowany przez funkcję przed jej zakończeniem.
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ć czymkolwiek, co jest w stanie 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 kodu w Visual Studio sprawdza, czy obiekt wywołujący przekazuje wskaźnik nie będący NULL-em do bufora dla pInt, i że bufor zostaje zainicjowany przez funkcję zanim ta się zakończy.
Najlepsza praktyka 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 domyślnie używać specyfikacji opartych na SAL 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ż oznaczone. Dlatego zalecamy, aby w projektach najpierw dodawać adnotacje do funkcji liściowych i funkcji wywołujących interfejsy API Win32, aby uzyskać jak największe korzyści.
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.
Adnotowanie reguł blokowania i skutki uboczne blokowania. Aby uzyskać więcej informacji, zobacz Adnotowanie zachowania blokowania.
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 była jasna przez cały czas 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
- Adnotowanie zachowania funkcji
- 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