Einführung in SAL
Die Microsoft-Quellcodeanmerkungssprache (SOURCE-Code Annotation Language, SAL) stellt eine Reihe von Anmerkungen bereit, die Sie verwenden können, um zu beschreiben, wie eine Funktion ihre Parameter verwendet, die Annahmen, die sie dazu macht, und die Garantien, die sie nach Abschluss der Funktion macht. Die Anmerkungen werden in der Headerdatei <sal.h>
definiert. Visual Studio-Codeanalyse für C++ verwendet SAL-Anmerkungen, um die Analyse von Funktionen zu ändern. Weitere Informationen zur ENTWICKLUNG von SAL 2.0 für Windows-Treiber finden Sie unter SAL 2.0 Anmerkungen für Windows-Treiber.
Systemintern bieten C und C++ nur begrenzte Möglichkeiten für Entwickler, die Absicht und Invarianz konsistent auszudrücken. Mithilfe von SAL-Anmerkungen können Sie Ihre Funktionen ausführlicher beschreiben, damit Entwickler, die sie verwenden, besser verstehen können, wie sie verwendet werden.
Was ist SAL und warum sollten Sie es verwenden?
Einfach gesagt, SAL ist eine kostengünstige Möglichkeit, den Compiler Ihren Code für Sie zu überprüfen.
SAL steigert den Wert des Codes
SAL kann Ihnen helfen, Ihr Codedesign verständlicher zu gestalten, sowohl für Menschen als auch für Codeanalysetools. Betrachten Sie dieses Beispiel, das die C-Laufzeitfunktion memcpy
zeigt:
void * memcpy(
void *dest,
const void *src,
size_t count
);
Können Sie feststellen, was diese Funktion bewirkt? Wenn eine Funktion implementiert oder aufgerufen wird, müssen bestimmte Eigenschaften Standard beibehalten werden, um die Programmkorrektur sicherzustellen. Wenn Sie sich nur eine Deklaration wie die im Beispiel ansehen, wissen Sie nicht, was sie sind. Ohne SAL-Anmerkungen müssten Sie sich auf Dokumentationen oder Codekommentare verlassen. Hier erfahren Sie, wofür die Dokumentation memcpy
steht:
"
memcpy
Kopien zählen Bytes von src bis dest;wmemcpy
Kopien zählen breite Zeichen (zwei Bytes). Wenn sich Quell und Ziel überlappen, ist das Verhalten vonmemcpy
undefiniert. Verwendungmemmove
um überlappende Bereiche zu behandeln.
Wichtig: Stellen Sie sicher, dass der Zielpuffer die gleiche Größe oder größer als der Quellpuffer ist. Weitere Informationen finden Sie unter "Vermeiden von Pufferüberläufen".
Die Dokumentation enthält einige Bits von Informationen, die vorschlagen, dass Ihr Code bestimmte Eigenschaften Standard enthalten muss, um die Programmkorrektur sicherzustellen:
memcpy
kopiert diecount
Bytes aus dem Quellpuffer in den Zielpuffer.Der Zielpuffer muss mindestens so groß sein wie der Quellpuffer.
Der Compiler kann die Dokumentation oder informelle Kommentare jedoch nicht lesen. Es ist nicht bekannt, dass es eine Beziehung zwischen den beiden Puffern gibt count
, und es kann auch nicht effektiv über eine Beziehung erraten werden. SAL könnte mehr Klarheit über die Eigenschaften und die Implementierung der Funktion bieten, wie hier gezeigt:
void * memcpy(
_Out_writes_bytes_all_(count) void *dest,
_In_reads_bytes_(count) const void *src,
size_t count
);
Beachten Sie, dass diese Anmerkungen den Informationen in der Dokumentation ähneln, aber sie sind präziser und folgen einem semantischen Muster. Wenn Sie diesen Code lesen, können Sie schnell die Eigenschaften dieser Funktion verstehen und wie Pufferüberlaufsicherheitsprobleme vermieden werden. Noch besser können die semantischen Muster, die SAL bereitstellt, die Effizienz und Effektivität automatisierter Codeanalysetools bei der frühen Ermittlung potenzieller Fehler verbessern. Stellen Sie sich vor, dass jemand diese Fehlerhafte Implementierung von 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;
}
Diese Implementierung enthält einen allgemeinen Off-by-One-Fehler. Glücklicherweise enthielt der Codeautor die SAL-Puffergrößenanmerkung – ein Codeanalysetool könnte den Fehler erfassen, indem er diese Funktion allein analysiert.
Grundlagen von SAL
SAL definiert vier grundlegende Arten von Parametern, die nach Verwendungsmustern kategorisiert werden.
Kategorie | Parameteranmerkung | Beschreibung |
---|---|---|
Eingabe für aufgerufene Funktion | _In_ |
Daten werden an die aufgerufene Funktion übergeben und als schreibgeschützt behandelt. |
Eingabe für die aufgerufene Funktion und Ausgabe des Aufrufers | _Inout_ |
Verwendbare Daten werden an die Funktion übergeben und potenziell geändert. |
Ausgabe an Anrufer | _Out_ |
Der Aufrufer bietet nur Platz für die aufgerufene Funktion, in die geschrieben werden soll. Die aufgerufene Funktion schreibt Daten in diesen Bereich. |
Ausgabe des Zeigers zum Aufrufer | _Outptr_ |
Like Output to caller. Der von der aufgerufenen Funktion zurückgegebene Wert ist ein Zeiger. |
Diese vier grundlegenden Anmerkungen können auf verschiedene Weise expliziter gemacht werden. Standardmäßig werden annotierte Zeigerparameter als erforderlich angenommen– sie müssen nicht NULL sein, damit die Funktion erfolgreich ausgeführt werden kann. Die am häufigsten verwendete Variation der grundlegenden Anmerkungen gibt an, dass ein Zeigerparameter optional ist – wenn es NULL ist, kann die Funktion trotzdem erfolgreich sein.
In dieser Tabelle wird gezeigt, wie Sie zwischen den erforderlichen und optionalen Parametern unterscheiden:
Parameter sind erforderlich | Parameter sind optional | |
---|---|---|
Eingabe für aufgerufene Funktion | _In_ |
_In_opt_ |
Eingabe für die aufgerufene Funktion und Ausgabe des Aufrufers | _Inout_ |
_Inout_opt_ |
Ausgabe an Anrufer | _Out_ |
_Out_opt_ |
Ausgabe des Zeigers zum Aufrufer | _Outptr_ |
_Outptr_opt_ |
Diese Anmerkungen helfen beim Identifizieren möglicher nicht initialisierter Werte und ungültiger Nullzeiger, die auf formale und genaue Weise verwendet werden. Das Übergeben von NULL an einen erforderlichen Parameter kann zu einem Absturz führen oder dazu führen, dass ein Fehlercode "fehlgeschlagen" zurückgegeben wird. Auf beide Weise kann die Funktion nicht erfolgreich sein.
Beispiele zu SAL
Dieser Abschnitt enthält Codebeispiele für die grundlegenden SAL-Anmerkungen.
Suchen von Fehlern mit den Visual Studio-Codeanalysetools
In den Beispielen wird das Visual Studio Code Analysis-Tool zusammen mit SAL-Anmerkungen verwendet, um Codefehler zu finden. Hierzu gehst du wie folgt vor.
So verwenden Sie Visual Studio-Codeanalysetools und SAL
Öffnen Sie in Visual Studio ein C++-Projekt, das SAL-Anmerkungen enthält.
Wählen Sie auf der Menüleiste "Erstellen" die Option "Codeanalyse für Lösung ausführen" aus.
Betrachten Sie das _In_-Beispiel in diesem Abschnitt. Wenn Sie codeanalyse ausführen, wird diese Warnung angezeigt:
C6387 Ungültiger Parameterwert 'pInt' könnte '0' lauten: Dies entspricht nicht der Spezifikation für die Funktion 'InCallee'.
Beispiel: Die _In_-Anmerkung
Die _In_
Anmerkung gibt folgendes an:
Der Parameter muss gültig sein und wird nicht geändert.
Die Funktion liest nur aus dem Einzelelementpuffer.
Der Aufrufer muss den Puffer bereitstellen und initialisieren.
_In_
Gibt "schreibgeschützt" an. Ein häufiger Fehler besteht darin, auf einen Parameter anzuwenden_In_
, der stattdessen über die_Inout_
Anmerkung verfügen sollte._In_
ist zulässig, wird jedoch von der Analyse auf Nicht-Zeiger-Skalaren ignoriert.
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
}
Wenn Sie Visual Studio Code Analysis in diesem Beispiel verwenden, wird überprüft, ob die Aufrufer einen Nicht-Null-Zeiger an einen initialisierten Puffer übergeben pInt
. In diesem Fall pInt
kann der Zeiger nicht NULL sein.
Beispiel: Die _In_opt_ Anmerkung
_In_opt_
ist identisch mit _In_
, mit der Ausnahme, dass der Eingabeparameter NULL sein darf, und daher sollte die Funktion dies überprüfen.
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);
}
Visual Studio Code Analysis überprüft, ob die Funktion auf NULL überprüft, bevor sie auf den Puffer zugreift.
Beispiel: Die _Out_Anmerkung
_Out_
unterstützt ein gängiges Szenario, in dem ein Nicht-NULL-Zeiger, der auf einen Elementpuffer verweist, übergeben wird und die Funktion das Element initialisiert. Der Aufrufer muss den Puffer nicht vor dem Aufruf initialisieren. die aufgerufene Funktion verspricht, sie zu initialisieren, bevor sie zurückgegeben wird.
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;
}
Das Visual Studio Code Analysis Tool überprüft, ob der Aufrufer einen Nicht-NULL-Zeiger an einen Puffer pInt
übergibt und dass der Puffer von der Funktion initialisiert wird, bevor er zurückgegeben wird.
Beispiel: Die _Out_opt_ Anmerkung
_Out_opt_
ist identisch mit _Out_
, mit der Ausnahme, dass der Parameter NULL sein darf, und daher sollte die Funktion dies überprüfen.
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);
}
Visual Studio Code Analysis überprüft, ob diese Funktion vor der pInt
Ableitung auf NULL sucht und wenn pInt
nicht NULL ist, dass der Puffer von der Funktion initialisiert wird, bevor sie zurückgegeben wird.
Beispiel: Die _Inout_-Anmerkung
_Inout_
wird verwendet, um einen Zeigerparameter zu kommentieren, der von der Funktion geändert werden kann. Der Zeiger muss vor dem Aufruf auf gültige initialisierte Daten zeigen, und selbst wenn er geändert wird, muss er trotzdem einen gültigen Wert für die Rückgabe haben. Die Anmerkung gibt an, dass die Funktion frei aus einem Elementpuffer lesen und schreiben kann. Der Aufrufer muss den Puffer bereitstellen und initialisieren.
Hinweis
Wie _Out_
, _Inout_
muss auf einen modifizierbaren Wert angewendet werden.
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
}
Visual Studio Code Analysis überprüft, ob Aufrufer einen Nicht-NULL-Zeiger an einen initialisierten Puffer pInt
übergeben, und dass vor der Rückgabe pInt
immer noch nicht NULL ist und der Puffer initialisiert wird.
Beispiel: Die _Inout_opt_ Anmerkung
_Inout_opt_
ist identisch mit _Inout_
, mit der Ausnahme, dass der Eingabeparameter NULL sein darf, und daher sollte die Funktion dies überprüfen.
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);
}
Visual Studio Code Analysis überprüft, ob diese Funktion auf NULL überprüft, bevor sie auf den Puffer zugreift und wenn pInt
nicht NULL ist, dass der Puffer von der Funktion initialisiert wird, bevor sie zurückgegeben wird.
Beispiel: Die _Outptr_-Anmerkung
_Outptr_
wird verwendet, um einen Parameter zu kommentieren, der einen Zeiger zurückgeben soll. Der Parameter selbst sollte nicht NULL sein, und die aufgerufene Funktion gibt einen Nicht-NULL-Zeiger darin zurück, und dieser Zeiger verweist auf initialisierte Daten.
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);
}
Visual Studio Code Analysis überprüft, ob der Aufrufer einen Nicht-NULL-Zeiger *pInt
angibt und dass der Puffer von der Funktion initialisiert wird, bevor sie zurückgegeben wird.
Beispiel: Die _Outptr_opt_ Anmerkung
_Outptr_opt_
ist identisch mit _Outptr_
der Ausnahme, dass der Parameter optional ist – der Aufrufer kann einen NULL-Zeiger für den Parameter übergeben.
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);
}
Visual Studio Code Analysis überprüft, ob diese Funktion vor der *pInt
Ableitung auf NULL sucht und dass der Puffer von der Funktion initialisiert wird, bevor sie zurückgegeben wird.
Beispiel: Die _Success_Anmerkung in Kombination mit _Out_
Anmerkungen können auf die meisten Objekte angewendet werden. Insbesondere können Sie eine ganze Funktion kommentieren. Eines der offensichtlichsten Merkmale einer Funktion besteht darin, dass sie erfolgreich oder fehlschlagen kann. Wie die Zuordnung zwischen einem Puffer und seiner Größe kann C/C++ jedoch keinen Funktionserfolg oder Fehler ausdrücken. Mithilfe der _Success_
Anmerkung können Sie sagen, wie erfolgreich eine Funktion aussieht. Der Parameter für die _Success_
Anmerkung ist nur ein Ausdruck, der angibt, dass die Funktion erfolgreich war. Der Ausdruck kann alles sein, was der Anmerkungsparser verarbeiten kann. Die Auswirkungen der Anmerkungen nach der Rückgabe der Funktion gelten nur, wenn die Funktion erfolgreich ausgeführt wird. In diesem Beispiel wird gezeigt, wie _Success_
sie mit _Out_
der richtigen Sache interagieren. Sie können die Schlüsselwort (keyword) return
verwenden, um den Rückgabewert darzustellen.
_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;
}
}
Die _Out_
Anmerkung bewirkt, dass visual Studio Code Analysis überprüft, ob der Aufrufer einen Nicht-NULL-Zeiger an einen Puffer pInt
übergibt und dass der Puffer von der Funktion initialisiert wird, bevor sie zurückgegeben wird.
Bewährte Methoden für SAL
Hinzufügen von Anmerkungen zu vorhandenem Code
SAL ist eine leistungsstarke Technologie, die Ihnen helfen kann, die Sicherheit und Zuverlässigkeit Ihres Codes zu verbessern. Nachdem Sie SAL gelernt haben, können Sie die neue Fähigkeit auf Ihre tägliche Arbeit anwenden. Im neuen Code können Sie SAL-basierte Spezifikationen nach Design verwenden; Im älteren Code können Sie Anmerkungen inkrementell hinzufügen und dadurch die Vorteile bei jeder Aktualisierung erhöhen.
Öffentliche Microsoft-Header werden bereits kommentiert. Daher empfehlen wir, dass Sie in Ihren Projekten zunächst Blattknotenfunktionen und -funktionen kommentieren, die Win32-APIs aufrufen, um den größten Nutzen zu erzielen.
Wann sollte ich Anmerkungen einfügen?
Hier finden Sie einige Richtlinien:
Kommentieren Sie alle Zeigerparameter.
Kommentieren Sie Anmerkungen zum Wertbereich, damit die Codeanalyse Puffer- und Zeigersicherheit gewährleisten kann.
Kommentieren Sie Sperrregeln und Sperren von Nebeneffekten. Weitere Informationen finden Sie unter Annotating Locking Behavior.
Kommentieren Sie Treibereigenschaften und andere do Standard spezifische Eigenschaften.
Sie können auch alle Parameter kommentieren, damit Ihre Absicht überall klar wird, und sie können ganz einfach überprüfen, ob Anmerkungen vorgenommen wurden.
Siehe auch
- Verwenden von SAL-Anmerkungen zum Reduzieren von C/C++-Codefehlern
- Hinzufügen einer Anmerkung zu Funktionsparametern und Rückgabewerten
- Hinzufügen einer Anmerkung zum Funktionsverhalten
- Hinzufügen einer Anmerkung zu Strukturen und Klassen
- Hinzufügen einer Anmerkung zum Sperrverhalten
- Angeben, wann und wo eine Anmerkung gültig ist
- Empfohlene Vorgehensweisen und Beispiele