Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
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 sind 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 Treibern mit SAL 2.0 für Windows 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?
Kurz gesagt, SAL ist eine kostengünstige Möglichkeit, den Compiler Ihren Code überprüfen zu lassen.
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 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 ist, was die Dokumentation von memcpy sagt:
"
memcpykopiert count Bytes von src nach dest;wmemcpykopiert count breite Zeichen (zwei Bytes). Wenn sich Quell und Ziel überlappen, ist das Verhalten vonmemcpyundefiniert. Verwenden Siememmove, um die überlappenden 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 beibehalten muss, um die Programmkorrektur sicherzustellen:
memcpykopiert diecountBytes 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 weiß nicht, dass es eine Beziehung zwischen den beiden Puffern und count gibt, und es kann auch nicht effektiv eine Beziehung erraten. 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, jemand schreibt 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 häufigen 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_ |
Die Daten werden an die aufgerufene Funktion übergeben und als schreibgeschützt behandelt. |
| Eingabe in die aufgerufene Funktion und Ausgabe an den Aufrufer | _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 an den Aufrufer | _Outptr_ |
Wie Ausgabe an Aufrufer. 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 in die aufgerufene Funktion und Ausgabe an den Aufrufer | _Inout_ |
_Inout_opt_ |
| Ausgabe an Anrufer | _Out_ |
_Out_opt_ |
| Ausgabe des Zeigers an den 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.
SAL-Beispiele
Dieser Abschnitt enthält Codebeispiele für die grundlegenden SAL-Anmerkungen.
Verwenden des Visual Studio-Codeanalysetools zum Auffinden von Fehlern
In den Beispielen wird das Visual Studio-Codeanalysetool 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 in der Menüleiste Erstellen, Codeanalyse für Lösung ausführen.
Betrachten Sie das Beispiel _In_ in diesem Abschnitt. Wenn Sie die 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 Anmerkung _In_
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 durch den Analyzer bei 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 die Codeanalyse von Visual Studio für dieses Beispiel verwenden, wird überprüft, ob die Aufrufer einen Zeiger auf einen initialisierten Puffer für pInt übergeben, der nicht Null ist. In diesem Fall pInt kann der Zeiger nicht NULL sein.
Beispiel: Die Anmerkung _In_opt_
_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-Codeanalyse überprüft, ob die Funktion auf NULL überprüft, bevor sie auf den Puffer zugreift.
Beispiel: Die Anmerkung _Out_
_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;
}
Visual Studio-Codeanalyse ü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 Anmerkung _Out_opt_
_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);
}
Die Codeanalyse von Visual Studio überprüft, ob diese Funktion vor der Dereferenzierung auf NULL prüft und, wenn pInt nicht NULL ist, dass der Puffer von der Funktion initialisiert wird, bevor die Funktion zurückkehrt.
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-Codeanalyse überprüft, ob Aufrufer einen Nicht-NULL-Zeiger an einen initialisierten Puffer pIntübergeben, und dass vor der Rückgabe pInt noch nicht NULL ist und der Puffer initialisiert wird.
Beispiel: Die Anmerkung _Inout_opt_
_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-Codeanalyse überprüft, ob diese Funktion vor dem Zugriff auf den Puffer auf NULL überprüft 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);
}
Die Codeanalyse von Visual Studio überprüft, dass der Aufrufer einen Nicht-NULL-Zeiger für *pInt übergibt und dass der Puffer von der Funktion initialisiert wird, bevor sie zurückkehrt.
Beispiel: Die Anmerkung _Outptr_opt_
_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);
}
Die Codeanalyse von Visual Studio bestätigt, dass diese Funktion auf NULL prüft, bevor *pInt dereferenziert wird, und dass der Puffer von der Funktion initialisiert wird, bevor sie zurückkehrt.
Beispiel: Die Anmerkung _Success_ 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 einfach ein Ausdruck, der, wenn er wahr ist, angibt, dass die Funktion erfolgreich war. Der Ausdruck kann alles sein, was der Anmerkungsparser verarbeiten kann. Die Auswirkungen der Anmerkungen nach der Rückkehr der Funktion sind nur anwendbar, wenn die Funktion erfolgreich ist. Dieses Beispiel zeigt, wie _Success_ mit _Out_ interagiert, um das Richtige zu tun. Sie können das Schlüsselwort 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 die Visual Studio-Codeanalyse ü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 Wertebereich-Annotationen, damit die Codeanalyse die Sicherheit von Puffern und Zeigern gewährleisten kann.
Kommentieren Sie Sperrregeln und deren Nebeneffekte. Weitere Informationen finden Sie unter Annotating Locking Behavior.
Kommentieren Sie Treibereigenschaften und andere domänenspezifische 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
- Sperrverhalten annotieren
- Angeben, wann und wo eine Anmerkung gültig ist
- Empfohlene Vorgehensweisen und Beispiele