Not
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
Microsofts källkodsanteckningsspråk (SAL) innehåller en uppsättning anteckningar som du kan använda för att beskriva hur en funktion använder sina parametrar, de antaganden som den gör om dem och de garantier som den gör när den är klar. Anteckningarna definieras i huvudfilen <sal.h>. Visual Studio-kodanalys för C++ använder SAL-anteckningar för att ändra dess analys av funktioner. För mer information om utveckling av SAL 2.0 för Windows-drivrutiner, se SAL 2.0-annotationer för Windows-drivrutiner.
Internt tillhandahåller C och C++ endast begränsade sätt för utvecklare att konsekvent uttrycka avsikt och invarians. Genom att använda SAL-anteckningar kan du beskriva dina funktioner mer detaljerat så att utvecklare som använder dem bättre kan förstå hur de ska användas.
Vad är SAL och varför ska du använda det?
Enkelt uttryckt är SAL ett billigt sätt att låta kompilatorn kontrollera koden åt dig.
SAL gör koden mer värdefull
SAL kan hjälpa dig att göra din koddesign mer begriplig, både för människor och för verktyg för kodanalys. Se det här exemplet som visar C-körningsfunktionen memcpy:
void * memcpy(
void *dest,
const void *src,
size_t count
);
Kan du se vad den här funktionen gör? När en funktion implementeras eller anropas måste vissa egenskaper underhållas för att säkerställa programmets korrekthet. Bara genom att titta på en deklaration som den i exemplet vet du inte vad de är. Utan SAL-anteckningar måste du förlita dig på dokumentation eller kodkommentarer. Här är vad dokumentationen för memcpy säger:
"
memcpykopierar antal byte från src till dest;wmemcpykopierar antal breda tecken (två byte)." Om källan och målet överlappar varandra är beteendetmemcpyför odefinierat. Användmemmoveför att hantera överlappande regioner.
Viktig: Kontrollera att målbufferten är lika stor eller större än källbufferten. Mer information finns i Undvika buffertöverskridningar."
Dokumentationen innehåller ett par bitar av information som tyder på att koden måste underhålla vissa egenskaper för att säkerställa programmets korrekthet:
memcpykopierarcountbyte från källbufferten till målbufferten.Målbufferten måste vara minst lika stor som källbufferten.
Kompilatorn kan dock inte läsa dokumentationen eller informella kommentarer. Den vet inte att det finns en relation mellan de två buffertarna och count, och det kan inte heller effektivt gissa sig fram till en relation. SAL kan ge mer klarhet om egenskaperna och implementeringen av funktionen, som du ser här:
void * memcpy(
_Out_writes_bytes_all_(count) void *dest,
_In_reads_bytes_(count) const void *src,
size_t count
);
Observera att dessa anteckningar liknar informationen i dokumentationen, men de är mer koncisa och följer ett semantiskt mönster. När du läser den här koden kan du snabbt förstå egenskaperna för den här funktionen och hur du undviker buffertöverskridna säkerhetsproblem. Ännu bättre är att de semantiska mönster som SAL tillhandahåller kan förbättra effektiviteten och effektiviteten för automatiserade kodanalysverktyg vid tidig upptäckt av potentiella buggar. Anta att någon skriver den här buggiga implementeringen av 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;
}
Den här implementeringen innehåller ett vanligt off-by-one-fel. Som tur var inkluderade kodförfattaren kommentaren om SAL-buffertstorlek – ett kodanalysverktyg kan fånga buggen genom att analysera den här funktionen ensam.
Grundläggande om SAL
SAL definierar fyra grundläggande typer av parametrar, som kategoriseras efter användningsmönster.
| Kategori | Parameteranteckning | Beskrivning |
|---|---|---|
| Indata till anropad funktion | _In_ |
Data skickas till den anropade funktionen och behandlas som skrivskyddad. |
| Indata till den anropade funktionen och utdata till anroparen | _Inout_ |
Användbara data skickas till funktionen och kan ändras. |
| Utdata till anroparen | _Out_ |
Anroparen ger bara utrymme för den anropade funktionen att skriva till. Den anropade funktionen skriver data till det utrymmet. |
| Utdata från pekaren till anroparen | _Outptr_ |
Som Utdata till anropare. Värdet som returneras av den anropade funktionen är en pekare. |
Dessa fyra grundläggande anteckningar kan göras tydligare på olika sätt. Som standard antas kommenterade pekarparametrar vara obligatoriska– de måste vara icke-NULL för att funktionen ska lyckas. Den vanligaste varianten av de grundläggande anteckningarna anger att en pekarparameter är valfri– om den är NULL kan funktionen fortfarande lyckas utföra sitt arbete.
Den här tabellen visar hur du skiljer mellan obligatoriska och valfria parametrar:
| Parametrar krävs | Parametrar är valfria | |
|---|---|---|
| Indata till anropad funktion | _In_ |
_In_opt_ |
| Indata till den anropade funktionen och utdata till anroparen | _Inout_ |
_Inout_opt_ |
| Utdata till anroparen | _Out_ |
_Out_opt_ |
| Utdata från pekaren till anroparen | _Outptr_ |
_Outptr_opt_ |
Dessa anteckningar hjälper dig att identifiera möjliga onitialiserade värden och ogiltiga null-pekare som används på ett formellt och korrekt sätt. Om du skickar NULL till en obligatorisk parameter kan det orsaka en krasch, eller så kan det leda till att en felkod "misslyckades" returneras. Hur som helst kan funktionen inte lyckas med sitt jobb.
SAL-exempel
Det här avsnittet visar kodexempel för de grundläggande SAL-anteckningarna.
Använda Visual Studio-kodanalysverktyget för att hitta defekter
I exemplen används analysverktyget för Visual Studio-kod tillsammans med SAL-anteckningar för att hitta kodfel. Så här gör du det.
Så här använder du Visual Studio-kodanalysverktyg och SAL
I Visual Studio öppnar du ett C++-projekt som innehåller SAL-anteckningar.
På menyraden väljer du Skapa, Kör kodanalys på lösning.
Överväg _In_-exemplet i det här avsnittet. Om du kör kodanalys på den visas den här varningen:
Ogiltigt parametervärde för C6387 "pInt" kan vara "0": detta följer inte specifikationen för funktionen "InCallee".
Exempel: Anteckningen _In_
Kommentaren _In_ anger att:
Parametern måste vara giltig och kommer inte att ändras.
Funktionen läser bara från bufferten med ett element.
Anroparen måste ange bufferten och initiera den.
_In_anger "skrivskyddad". Ett vanligt misstag är att tillämpa på_In_en parameter som ska ha anteckningen_Inout_i stället._In_är tillåten men ignoreras av analysatorn på icke-pekarskalärer.
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
}
Om du använder Visual Studio-kodanalys i det här exemplet verifieras det att anroparna skickar en icke-nullpekare till en initierad buffert för pInt. I det här fallet pInt kan pekaren inte vara NULL.
Exempel: Annoteringen _In_opt_
_In_opt_ är samma som _In_, förutom att indataparametern tillåts vara NULL och därför bör funktionen söka efter detta.
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-kodanalys verifierar att funktionen söker efter NULL innan den kommer åt bufferten.
Exempel: Anteckningen _Out_
_Out_ stöder ett vanligt scenario där en icke-NULL-pekare som pekar på en elementbuffert skickas in och funktionen initierar elementet. Anroparen behöver inte initiera bufferten före anropet. den anropade funktionen lovar att initiera den innan den returneras.
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-kodanalys verifierar att anroparen skickar en icke-NULL-pekare till en buffert för pInt och att bufferten initieras av funktionen innan den returneras.
Exempel: Kommentaren _Out_opt_
_Out_opt_ är samma som _Out_, förutom att parametern tillåts vara NULL och därför bör funktionen söka efter detta.
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-kodanalys verifierar att den här funktionen söker efter NULL innan pInt avrefereras, och om pInt inte är NULL, att funktionen initierar bufferten innan den returneras.
Exempel: Anteckningen _Inout_
_Inout_ används för att kommentera en pekarparameter som kan ändras av funktionen. Pekaren måste peka på giltiga initierade data före anropet, och även om den ändras måste den fortfarande ha ett giltigt värde vid returen. Kommentaren anger att funktionen fritt kan läsa från och skriva till enelementsbufferten. Anroparen måste ange bufferten och initiera den.
Anmärkning
Som _Out_, _Inout_ måste gälla för ett ändringsbart värde.
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-kodanalys verifierar att anropare skickar en icke-NULL-pekare till en initierad buffert för pInt, och att pInt fortfarande inte är NULL och att bufferten är initierad innan funktionen returnerar.
Exempel: Anmärkningen _Inout_opt_
_Inout_opt_ är samma som _Inout_, förutom att indataparametern tillåts vara NULL och därför bör funktionen söka efter detta.
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-kodanalys verifierar att den här funktionen söker efter NULL innan den kommer åt bufferten, och om pInt den inte är NULL initieras bufferten av funktionen innan den returneras.
Exempel: Annoteringen _Outptr_
_Outptr_ används för att kommentera en parameter som är avsedd att returnera en pekare. Själva parametern ska inte vara NULL och den anropade funktionen returnerar en icke-NULL-pekare i den och pekaren pekar på initierade data.
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-kodanalys verifierar att anroparen skickar en icke-NULL-pekare för *pIntoch att bufferten initieras av funktionen innan den returneras.
Exempel: Attributet _Outptr_opt_
_Outptr_opt_ är samma som _Outptr_, förutom att parametern är valfri – anroparen kan skicka in en NULL-pekare för parametern.
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-kodanalys verifierar att den här funktionen söker efter NULL innan *pInt den avrefereras och att bufferten initieras av funktionen innan den returneras.
Exempel: Kommentaren _Success_ i kombination med _Out_
Anteckningar kan tillämpas på de flesta objekt. I synnerhet kan du kommentera en hel funktion. En av de mest uppenbara egenskaperna hos en funktion är att den kan lyckas eller misslyckas. Men precis som associationen mellan en buffert och dess storlek kan C/C++ inte uttrycka funktionsframgång eller fel. Med hjälp av anteckningen _Success_ kan du säga hur en funktions framgång ser ut. Parametern till anteckningen _Success_ är bara ett uttryck som när det är sant anger att funktionen har lyckats. Uttrycket kan vara allt som anteckningsparsern kan hantera. Effekterna av anteckningarna efter att funktionen returneras gäller endast när funktionen lyckas. Det här exemplet visar hur _Success_ interagerar med _Out_ för att göra det rätta. Du kan använda nyckelordet return för att representera returvärdet.
_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;
}
}
Kommentaren _Out_ gör att Visual Studio-kodanalysen verifierar att anroparen skickar en icke-NULL-pekare till en buffert för pIntoch att bufferten initieras av funktionen innan den returneras.
Bästa praxis för SAL
Lägga till anteckningar i befintlig kod
SAL är en kraftfull teknik som kan hjälpa dig att förbättra kodens säkerhet och tillförlitlighet. När du har lärt dig SAL kan du använda den nya färdigheten i ditt dagliga arbete. I ny kod kan du använda SAL-baserade specifikationer genom hela designprocessen; i äldre kod kan du lägga till annoteringar stegvis och därmed öka fördelarna varje gång du uppdaterar.
Microsofts offentliga rubriker har redan kommenterats. Därför föreslår vi att du i dina projekt först annoterar funktioner för lövnoder och funktioner som anropar Win32-API:er för att få mest nytta.
När kommenterar jag?
Här följer några riktlinjer:
Kommentera alla pekarparametrar.
Kommentera värdeintervallanteckningar så att kodanalys kan säkerställa buffert- och pekarsäkerhet.
Kommentera låsningsregler och låsningsbieffekter. Mer information finns i Kommentera låsningsbeteende.
Kommentera drivrutinsegenskaper och andra domänspecifika egenskaper.
Eller så kan du kommentera alla parametrar för att göra avsikten tydlig hela och för att göra det enkelt att kontrollera att anteckningar har gjorts.