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 memcpyzeigt:

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 von memcpy undefiniert. Verwendung memmove 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 die count 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

  1. Öffnen Sie in Visual Studio ein C++-Projekt, das SAL-Anmerkungen enthält.

  2. 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 *pIntangibt 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