了解 SAL
Microsoft 原始程式碼附註語言 (SAL) 提供可用來描述如何使用其參數的一組附註函式,假設並確保它會在完成時呼叫。附註在標頭檔中定義 <sal.h>。使用 SAL 附註函式修改C++ 的 Visual Studio 程式碼分析的分析。如需 Windows 驅動程式開發的 SAL 2.0 的詳細資訊,請參閱 SAL 視窗驅動程式的 2.0 附註。
原本, C 和 C++ 的開發人員只提供有限的方式來一致地明確目的而不變更屬性。藉由使用 SAL 附註,您才能夠更詳細地描述您的函式,以便使用它們的開發人員可以深入了解如何使用它們。
SAL是甚麼以及為何要使用他?
簡單來說, SAL 是一種可以讓編譯器檢查您的程式碼的模式。
SAL 讓程式碼更具價值
SAL 有助於讓您的程式碼設計易懂,不管是對於人還是程式碼分析工具來說。考慮顯示 C 執行階段函式的範例 memcpy:
void * memcpy(
void *dest,
const void *src,
size_t count
);
您可以說明這個函式是甚麼嗎?當函式實作或呼叫時,某些屬性必須維護來確保程式正確性。藉由查看一類的宣告像是在這個範例中,您不知道他們是甚麼。沒有 SAL 附註,您就必須依賴檔案或程式碼中的註解。 以下是 MSDN 文件中對於 memcpy的說法 :
「複製計算src的位元到dest。如果來源和目的重疊, 則memcpy 行為是未定義的。使用 memmove 處理重疊的區域。 安全性提示: 確定目的緩衝區大於或等於來源緩衝區。如需詳細資訊,請參閱 Avoiding Buffer Overruns 。」
建議的文件包含兩個欄位資訊的程式碼建議您必須維護某些屬性來確保程式正確性:
memcpy 複製 count 的位元組,他是從來源緩衝區至目的緩衝區的位元組。
目的緩衝區至少必須像來源緩衝區一樣大。
不過,編譯器無法讀取檔案或非正式的註解。 它不知道兩個緩衝區之間有關係和 count,它也不能有效地猜出關聯性。SAL 可以提供有關函式更清楚的屬性和實作,如下所示:
void * memcpy(
_Out_writes_bytes_all_(count) void *dest,
_In_reads_bytes_(count) const void *src,
size_t count
);
請注意這些附註類似 MSDN 文件中的資訊,但是,更為簡潔,並且遵循語意形式。 當您閱讀程式碼時,您可以快速了解這個函式屬性以及如何避免緩衝區滿溢的安全性問題。 甚至, SAL 提供可有效率提高自動化程式碼分析工具、分割早期發現的潛在bugs。假設有人撰寫 wmemcpy的這個多 Bug 的實作:
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;
}
這個實作會包含錯誤的通用。所幸,程式碼作者併入 SAL 緩衝區大小附註程式碼分析工具可以透過分析這個函式個別來攔截錯誤。
SAL 的基本概念
SAL 定義四種基本參數,並使用模式來分類。
分類 |
參數註釋 |
說明 |
---|---|---|
輸入來呼叫函式。 |
_In_ |
資料被傳遞給呼叫的函式且會被視為唯讀。 |
輸入來呼叫函示以及輸出給呼叫者。 |
_Inout_ |
可用的資料傳遞至函式且可能會被修改。 |
輸出給呼叫者 |
_Out_ |
呼叫者只提供空間給呼叫的函示來寫入。被呼叫函式將資料寫入該空間。 |
指標輸出至呼叫端 |
_Outptr_ |
如 輸出至呼叫端。由被呼叫的函式所傳回的值是指標。 |
這四個基本標註可以用各種方式明確化。根據預設,會假設參數需要其加註的指標必須為非 null 功能才會成功。 基本的附註最常用的變化指示參數是選擇性的指標,如果這是 null,則函式會在完成其工作後仍然會成功。
下表顯示如何區別必要和選擇性參數:
需要參數。 |
參數是選擇性的。 |
|
---|---|---|
輸入來呼叫函式。 |
_In_ |
_In_opt_ |
輸入來呼叫函示以及輸出給呼叫者。 |
_Inout_ |
_Inout_opt_ |
輸出給呼叫者 |
_Out_ |
_Out_opt_ |
指標輸出至呼叫端 |
_Outptr_ |
_Outptr_opt_ |
這些附註可協助您識別可能未初始化的值和無效 NULL 指標搭配使用與型式和精確模式。 傳遞 null 至需要的參數可能會造成當機,也可能會產生「失敗」傳回的錯誤碼。 無論使用哪種方式,函式無法在完成其工作成功。
SAL 範例
本節說明基本 SAL 附註的程式碼範例。
使用 Visual Studio 程式碼分析工具尋找缺失
在範例中, Visual Studio 程式碼分析工具與 SAL 附註一起被用來找出程式碼缺失。這裡說明如何做到。
使用 Visual Studio 程式碼分析工具和 SAL
在 Visual Studio 中,開啟包含 SAL 附註的C++ 專案。
在功能表列上,選取 [組建], [針對方案執行程式碼分析]。
回想此節中的 _In_ 範例。如果您在這個平台上執行的程式碼分析警告,他會顯示:
**C6387無效的參數值。**pInt 可以是"0":這並未遵守函式InCallee 的規格。
範例:_In_ 附註
_In_ 附註表示:
參數必須是有效的,而且不會修改。
函式只會從單一項目緩衝區中讀取。
呼叫端必須提供緩衝區並加以初始化。
_In_ 指定「唯讀」。一個常見錯誤是套用至 _In_ 應該有 _Inout_ 附註的參數。
_In_ 是合法的但會被非指標純量的分析器忽略。
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
}
如果您使用在這個範例的 Visual Studio 程式碼分析,它會驗證呼叫端傳遞非 null 指標。 pInt所初始化的緩衝區。在這種情況下, pInt 指標不可以是 null。
範例:_In_opt_ 附註
_In_opt_ 與 _In_一樣,但是有一點例外,就是輸入參數允許為 Null 值,因此函式應該進行檢查。
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 程式碼分析在存取緩衝區之前驗證 NULL 的功能驗證。
範例:_Out_ 附註
_Out_ 支援非 null 指標指向項目緩衝區傳遞的常見案例,而且函式會將這個項目初始化。呼叫端不需要在呼叫前先初始化緩衝區,會在它傳回之前被呼叫的函式加以初始化。
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 程式碼分析工具會根據呼叫端傳遞非 null 指標。 pInt 的緩衝區,並由函式初始化緩衝區,再將它傳回。
範例:_Out_opt_ 附註
_Out_opt_ 與 _Out_一樣,不過允許參數是空的,因此函式應該進行檢查。
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 程式碼分析會檢查驗證 NULL 的這個功能,以 pInt 解除參考之前 pInt ,而且,如果不是 null,緩衝區是由函式初始化,再將它傳回。
範例:_Inout_ 附註
_Inout_ 用來加註函式可能會變更的指標參數。 指標必須在呼叫之前指向有效初始化資料,即使有變更,它仍然必須傳回有效值。標記法指定函式可以自由讀取和寫入清單的項目緩衝區。呼叫端必須提供緩衝區並加以初始化。
注意事項 |
---|
如 _Out_, _Inout_ 必須適用於可修改的值。 |
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 程式碼分析驗證呼叫端傳遞非 null 指標。 pInt初始設定的緩衝區,在返回之前 pInt 非 null,而且緩衝區初始化。
範例:_Inout_opt_ 附註
_Inout_opt_ 和 _Inout_一樣,除了輸入變數可以為NULL,所以該函數必須被檢查。
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 程式碼分析會檢查在存取緩衝區之前驗證 NULL 的這個功能,而且 pInt 如果不是 null,緩衝區是在傳回之前被函式初始化。
範例:_Outptr_ 附註
_Outptr_ 用來加註要傳回指標的參數。 參數不能是 Null 和呼叫的函式會傳回在其非 null 指標和該指標指向初始化資料。
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 程式碼分析驗證呼叫端傳遞 *pInt的非 null 指標,並由函式初始化緩衝區,再將它傳回。
範例:_Outptr_opt_ 附註
_Outptr_opt_ 與 _Outptr_一樣,不過,參數是選擇性的呼叫端可以傳遞任何在參數中的 null 指標。
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 程式碼分析會檢查以 *pInt 進行取值前驗證 NULL 的這個功能,並由函式初始化緩衝區,再將它傳回。
範例:使用 _Out_ 的組合 _Success_ 附註
附註可套用至多個資料物件。 特別是您可以加註整個函式。其中一個最明顯的函式特性是它可以成功或失敗。但是,就像在緩衝區和其大小之間的關聯, C/C++ 無法表示函式成功或失敗。 藉由使用 _Success_ 附註,您可以說哪些函式成功出現。為 _Success_ 附註的參數只是一個表示法,表示當他為 true 時代表函式成功。運算式可以是任何註釋剖析器能處理的東西。函式傳回之後的附註的效果,只適用在當函式成功時。這個範例顯示 _Success_ 如何與 _Out_ 互動執行正確的項目。 您可以使用關鍵字 return 表示傳回值。
_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;
}
}
_Out_ 附註會導致 Visual Studio 程式碼分析驗證呼叫者傳遞非 null 指標到 pInt的緩衝區,並由函式初始化緩衝區,再將它傳回。
SAL最佳作法
將附註加入至現有的程式碼
SAL 是可幫助您提升程式碼的安全性和可靠性功能強大的技術。 在您學習SAL 之後,您可以將新提示加入至每天的工作。 在新的程式碼中,您可以藉由設計內文中使用 SAL 的規格,在舊版的程式碼,請在您每次更新時,加入標記法並且藉此加入優點。
Microsoft 公用標頭已加註。 因此,我們建議您應該在程式碼中先為葉子節點函式以及呼叫 Win32 應用程式開發介面的函式做註釋來取得最大的好處。
我何時註釋?
這裡是一些指導:
註釋所有指標參數。
註釋值範圍附註好讓程式碼分析可以確保緩衝區和指標安全。
註釋鎖定規則和鎖定副作用。如需詳細資訊,請參閱註釋鎖定行為。
註釋驅動程式屬性和其他網域特有的屬性。
或是您也可以加註所有參數讓您清楚的表示意圖且更輕鬆地檢查完成的附註。