使用 PREfast 进行静态代码分析






4.93/5 (13投票s)
本文将介绍在 Visual Studio 中使用 PREfast 进行静态代码分析的基础知识。
引言
如果您是 Windows 开发者,您一定注意到 Windows SDK 头文件中一些奇怪的注解。类似这样:
DWORD WINAPI WaitForMultipleObjects(
__in DWORD nCount,
__in_ecount(nCount) CONST HANDLE *lpHandles,
__in BOOL bWaitAll,
__in DWORD dwMilliseconds
);
本文将介绍它是什么以及它如何对您有所帮助。此外,它还将揭示在 Visual Studio 中使用 PREfast 进行静态代码分析的基础知识。
什么是 PREfast?
PREfast 是一个静态代码分析工具(这意味着分析是在编译时进行的)。它可以查找 C/C++ 代码中的缺陷,例如缓冲区溢出、null
指针解引用、忘记检查函数返回值等等。由于有更严格的一套规则需要代码遵循,因此它在检查内核模式代码方面尤其出色。您可以将 PREfast 视为一个自动代码审查工具。
为了更好地完成工作,PREfast 需要了解有关代码的附加信息。这时注解就派上用场了。
注解
注解是特殊的宏。在任何上下文中安全使用它们,因为当代码正常编译时,这些宏会被展开为空。只有当 PREfast 运行时,这些宏才会被展开为有意义的定义。您可以在 specstrings.h 中找到通用注解,在 driverspecs.h 中找到驱动程序特定的注解。系统头文件包含了注解的头文件,因此在大多数情况下,您无需单独包含它们。
注解扩展了函数原型,并描述了函数与其调用者之间的契约。这使得 PREfast 能够更准确地分析代码,显著减少误报和漏报。
使用注解的另一个主要好处是文档。PREfast 注解非常易于阅读,它们构成了与代码保持同步的文档。程序员无需搜索外部文档或进行猜测和实验。
源代码注解集构成了标准注解语言 (SAL)。
注解修饰符
出于各种实现相关的原因,许多必须应用于函数参数的注解必须表示为单个宏,而不是一系列相邻的宏。这是通过向注解添加修饰符来完成的,以构成一个更完整的注解。
例如,__in
注解可以附加 _opt
修饰符,resulting annotation 将是 __in_opt
。
注解在代码中的放置位置
注解可以应用于整个函数、单个函数参数以及 typedef
声明,包括函数类型的声明。
通用注解
本节介绍最常用的注解。您可以在驱动程序代码和非驱动程序代码中使用它们。通用注解定义在 specstrings.h 中,并在 specstrings_strict.h 中附带详细注释。
数据流方向
下面列出了最基本的注解,应在所有地方使用它们
__in
__out
__inout
这些注解有助于识别未初始化的值被错误使用的情况。正式来说,__in
表示传递的参数值在函数调用之前必须是有效的,并且函数不会更改它。__out
表示函数返回一个有效值,并且 PREfast 可以在函数调用之前忽略该值。__inout
表示参数在函数调用之前和之后都必须有效,并且函数会更改该值。
带有这些注解的指针的有效值不能是 NULL
。
示例
__out HANDLE WINAPI FindFirstFileW(
__in LPCWSTR lpFileName,
__out LPWIN32_FIND_DATAW lpFindFileData
);
LONG __cdecl InterlockedDecrement (
__inout LONG volatile *lpAddend
);
可选性
_opt
修饰符表示参数是可选的,因此指针的值可以是 NULL
。
__in_opt
__out_opt
__inout_opt
示例
__out_opt HANDLE WINAPI CreateThread(
__in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes,
__in SIZE_T dwStackSize,
__in LPTHREAD_START_ROUTINE lpStartAddress,
__in_opt LPVOID lpParameter,
__in DWORD dwCreationFlags,
__out_opt LPDWORD lpThreadId
);
WINBASEAPI BOOL WINAPI CreateProcessW(
__in_opt LPCWSTR lpApplicationName,
__inout_opt LPWSTR lpCommandLine,
__in_opt LPSECURITY_ATTRIBUTES lpProcessAttributes,
__in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes,
__in BOOL bInheritHandles,
__in DWORD dwCreationFlags,
__in_opt LPVOID lpEnvironment,
__in_opt LPCWSTR lpCurrentDirectory,
__in LPSTARTUPINFOW lpStartupInfo,
__out LPPROCESS_INFORMATION lpProcessInformation
);
解引用
__deref
修饰符表示注解应应用于参数的解引用值,而不是参数本身
__deref_in
__deref_out
__deref_inout
为了使事情更清楚,下表显示了如何组合 __deref
和 _opt
修饰符以及每个组合对应的规则。
表 1 - __deref
和 _opt
组合
参数 | *参数 | |
__deref_out | 非 NULL | 非 NULL |
__deref_out_opt | 非 | 可为 NULL |
__deref_opt_out | 可为 NULL | 非 NULL |
__deref_opt_out_opt | 可为 NULL | 可为 NULL |
示例
BOOL WINAPI OpenProcessToken (
__in HANDLE ProcessHandle,
__in DWORD DesiredAccess,
__deref_out PHANDLE TokenHandle
);
零终止
_z
修饰符表示缓冲区是零终止的
__in_z
__out_z
__inout_z
示例
struct hostent FAR * PASCAL FAR gethostbyname(__in_z const char FAR * name);
WINOLEAUTAPI_(BSTR) SysAllocString(__in_z_opt const OLECHAR * psz);
字符串
有针对 string
的特殊注解
__nullterminated
__nullnullterminated
__possibly_notnullterminated
它们在应用于 typedef
声明时很有用。这些注解使 PREfast 能够检查类型在函数中的使用是否正确,而无需程序员注解使用该类型的每个函数参数。请注意 PWCHAR
和 PWSTR
之间的区别
typedef WCHAR *PWCHAR, *LPWCH, *PWCH;
typedef __nullterminated WCHAR *LPWSTR, *PWSTR;
__nullnullterminated
注解适用于由双 null
终止的“字符串数组”,例如类型为 REG_MULTI_SZ
的注册表值。
一些旧函数通常返回零终止的 string
,但偶尔不会。经典的例子是 snprintf
和 strncpy
,当缓冲区已满时,函数会省略 null
终止符。__possibly_notnullterminated
注解描述了这种行为。
示例
WINOLEAPI StgIsStorageFile(
__in __nullterminated const WCHAR* pwcsName);
__out __nullnullterminated LPCH WINAPI
GetEnvironmentStrings(VOID);
int _snprintf(
__out_ecount(count) __possibly_notnullterminated LPSTR buffer,
__in size_t count,
__in LPCSTR *format,
...
);
缓冲区大小注解
代码中的许多错误,特别是安全错误,是由缓冲区溢出引起的,在缓冲区溢出中,一个可变大小的对象被传递。以下注解可用于描述调用方和被调用方之间关于缓冲区大小的契约
_ecount(size)
_bcount(size)
_xcount(expr)
_full(size)
_part(size, length)
使用 _ecount(size)
以元素数量表示缓冲区的大小。使用 _bcount(size)
以字节数表示缓冲区的大小。size
参数可以是任何在编译时有意义的通用表达式。它可以是一个数字,但通常是正在注解的函数中某个参数的名称(要引用函数返回值,请在大小规范中使用 return
)。_xcount(expr)
设计用于复杂情况,当缓冲区大小无法表示为简单的字节或元素计数时。例如,计数可能在一个全局变量中,或者由枚举隐式指定。PREfast 将 expr
视为注释,不使用它来检查缓冲区大小。expr 可以是任何对读者有意义的内容。
_full
修饰符表示整个缓冲区已初始化(这与其他注解是多余的)。_part
修饰符表示缓冲区的一部分已初始化,并明确指出有多少。
下表总结了可组合以描述缓冲区的注解。
表 2 – 缓冲区注解摘要
信号强度 | 用法 | 大小 | 输出 | 可选 | 参数 |
省略 | 省略 | 省略 | 省略 | 省略 | 省略 |
示例
DWORD WINAPI WaitForMultipleObjects(
__in DWORD nCount,
__in_ecount(nCount) CONST HANDLE *lpHandles,
__in BOOL bWaitAll,
__in DWORD dwMilliseconds
);
DWORD WINAPI GetLogicalDriveStringsW(
__in DWORD nBufferLength,
__out_ecount_part_opt(nBufferLength, return + 1) LPWSTR lpBuffer
);
__bcount_opt(dwSize) LPVOID WINAPI VirtualAlloc(
__in_opt LPVOID lpAddress,
__in SIZE_T dwSize,
__in DWORD flAllocationType,
__in DWORD flProtect
);
BOOL APIENTRY VerQueryValueW(
__in LPCVOID pBlock,
__in LPCWSTR lpSubBlock,
__deref_out_xcount("buffer can be PWSTR or DWORD*") LPVOID * lplpBuffer,
__out PUINT puLen
);
BOOL WINAPI ReadFile(
__in HANDLE hFile,
__out_bcount_part_opt(nNumberOfBytesToRead, *lpNumberOfBytesRead) LPVOID lpBuffer,
__in DWORD nNumberOfBytesToRead,
__out_opt LPDWORD lpNumberOfBytesRead,
__inout_opt LPOVERLAPPED lpOverlapped
);
保留参数
__reserved
注解确保在未来版本中,可以可靠地检测到旧函数调用者。此注解要求提供的参数是 0
或 NULL
,具体取决于类型。
示例
WINAPI CreateDesktopW(
__in LPCWSTR lpszDesktop,
__reserved LPCWSTR lpszDevice,
__reserved LPDEVMODEW pDevmode,
__in DWORD dwFlags,
__in ACCESS_MASK dwDesiredAccess,
__in_opt LPSECURITY_ATTRIBUTES lpsa);
BOOLAPI InternetTimeToSystemTimeW(
__in LPCWSTR lpszTime,
__out SYSTEMTIME *pst,
__reserved DWORD dwReserved
);
函数返回值
代码中常见的情况是假设函数调用总是成功的,而不检查返回值。__checkReturn
注解指示应检查函数返回值。
示例
__checkReturn DWORD WINAPI GetLastError(
VOID
);
__checkReturn SOCKET WSAAPI socket(
__in int af,
__in int type,
__in int protocol
);
内核模式注解
有许多专门为内核模式代码设计的注解。它们定义在 driverspecs.h 中,并以 __drv
作为前缀。
以下是其中一些的列表
__drv_isObjectPointer
__drv_inTry
__drv_allocatesMem(type)
__drv_freesMem(type)
__drv_acquiresResource(kind)
__drv_releasesResource(kind)
__drv_acquiresCriticalRegion
__drv_releasesCriticalRegion
__drv_acquiresCancelSpinLock
__drv_releasesCancelSpinLock
__drv_mustHold(kind)
__drv_neverHold(kind)
__drv_mustHoldCriticalRegion
__drv_neverHoldCriticalRegion
__drv_mustHoldCancelSpinLock
__drv_neverHoldCancelSpinLock
__drv_acquiresExclusiveResource(kind)
__drv_releasesExclusiveResource(kind)
__drv_maxIRQL(value)
__drv_minIRQL(value)
__drv_floatUsed
__drv_floatSaved
__drv_floatRestored
__drv_strictTypeMatch(mode)
__drv_strictType(typename, mode)
__drv_when(cond, anno_list)
__drv_valueIs(list)
__drv_clearDoInit(yes|no)
__drv_dispatchType(type)
使用 PREfast 进行驱动程序开发值得另写一篇文章,本文不涵盖。
误报
如果您确定 PREfast 警告是误报或只是不需要修复的噪音,您可以使用 #pragma warning
指令来抑制警告。
在 #pragma warning
指令中,使用 PREfast 警告编号来标识要抑制的警告。您可以使用 (push)
和 (pop)
语句将指令的作用范围限定在产生误报的代码行内。
#pragma warning(push)
#pragma warning(disable: XXXX)
// the following code will not produce specified warning
…
#pragma warning(pop)
作为 push
和 pop
的替代方法,您可以使用 suppress
语句来仅抑制下一行代码的警告。
#pragma warning(suppress : XXXX)
… // this line will not produce specified warning
此外,您还可以使用 __analysis_assume (expr)
源代码注解提供有关代码的假设,其中 expr
是任何假定求值为 true
的表达式。以下示例消除了 null
指针解引用警告。
__analysis_assume(foo != NULL);
*foo = 0;
在 Visual Studio 中运行代码分析
PREfast 已集成到 Visual Studio 2010 Ultimate 和 Premium 版本、Visual Studio 2008 Development Edition 和 Team Suite 中。它也可用在 Windows SDK 编译器中,并作为 Windows DDK 中的独立工具。
在 Visual Studio 中,您可以通过“分析”->“仅运行代码分析”菜单来启动特定项目的代码分析。
您还可以通过项目属性或命令行 switch
/analyze
来控制代码分析。
分析示例
本节展示了一些带有缺陷的函数以及它们产生的 PREfast 警告。
未初始化的内存
警告 C6001: 使用未初始化的内存 'b': 行号: 16, 18, 23
内存泄漏
警告 C6211: 由于异常导致内存 'p1' 泄漏。请考虑使用局部 catch 块来清理内存: 行号: 21, 23, 25
传递 NULL 句柄
警告 C6387: '参数 1' 可能是 '0': 这不符合函数 'CloseHandle
' 的规范: 行号: 16, 20
字符数/字节数不匹配
警告 C6057: 调用 'GetWindowsDirectoryW
' 时,由于字符数/字节数不匹配导致缓冲区溢出。
警告 C6031: 返回值被忽略: 'GetWindowsDirectoryW
'
警告 C6386: 缓冲区溢出: 访问 '参数 1',可写大小为 '200' 字节,但可能写入 '400' 字节: 行号: 16, 18
结论
不要期望 PREfast 能找到您代码中的所有缺陷。SAL 和 PREfast 的主要好处是,您可以通过一点前期工作找到更多错误,并使您的代码对其他程序员更易于理解。
通过我们独立的 软件代码审计服务 确保代码质量和安全性,提供对您的解决方案的全面评估。
参考文献
- SAL/PREfast 的经验
- PREfast 注解
- PREfast 逐步指南
- 关于 PREfast for Drivers
- PREfast:更少的 Bug,更高的可靠性
- Prefast 和 SAL 注解
历史
- 2011年3月11日:初始发布