65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 PREfast 进行静态代码分析

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (13投票s)

2011年3月11日

CPOL

8分钟阅读

viewsIcon

79166

本文将介绍在 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

可为 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 能够检查类型在函数中的使用是否正确,而无需程序员注解使用该类型的每个函数参数。请注意 PWCHARPWSTR 之间的区别

typedef WCHAR *PWCHAR, *LPWCH, *PWCH;
typedef __nullterminated WCHAR *LPWSTR, *PWSTR;

__nullnullterminated 注解适用于由双 null 终止的“字符串数组”,例如类型为 REG_MULTI_SZ 的注册表值。

一些旧函数通常返回零终止的 string,但偶尔不会。经典的例子是 snprintfstrncpy,当缓冲区已满时,函数会省略 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 – 缓冲区注解摘要

信号强度

用法

大小

输出

可选

参数

省略
_deref
_deref_opt

省略
__in
__out
__inout

省略
_ecount
_bcount
_xcount(expr)

省略
_full
_part

省略
_opt

省略
(size)
(size, length)

示例

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)

作为 pushpop 的替代方法,您可以使用 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 中,您可以通过“分析”->“仅运行代码分析”菜单来启动特定项目的代码分析。

prefast-code-analysis/analyze.PNG

图 1 - 仅运行特定项目的代码分析

您还可以通过项目属性或命令行 switch /analyze 来控制代码分析。

prefast-code-analysis/properties.PNG

图 2 - 项目属性中的代码分析设置

分析示例

本节展示了一些带有缺陷的函数以及它们产生的 PREfast 警告。

未初始化的内存

prefast-code-analysis/defect1.PNG

警告 C6001: 使用未初始化的内存 'b': 行号: 16, 18, 23

内存泄漏

prefast-code-analysis/defect2.PNG

警告 C6211: 由于异常导致内存 'p1' 泄漏。请考虑使用局部 catch 块来清理内存: 行号: 21, 23, 25

传递 NULL 句柄

prefast-code-analysis/defect3.PNG

警告 C6387: '参数 1' 可能是 '0': 这不符合函数 'CloseHandle' 的规范: 行号: 16, 20

字符数/字节数不匹配

prefast-code-analysis/defect4.PNG

警告 C6057: 调用 'GetWindowsDirectoryW' 时,由于字符数/字节数不匹配导致缓冲区溢出。
警告 C6031: 返回值被忽略: 'GetWindowsDirectoryW'
警告 C6386: 缓冲区溢出: 访问 '参数 1',可写大小为 '200' 字节,但可能写入 '400' 字节: 行号: 16, 18

结论

不要期望 PREfast 能找到您代码中的所有缺陷。SAL 和 PREfast 的主要好处是,您可以通过一点前期工作找到更多错误,并使您的代码对其他程序员更易于理解。

通过我们独立的 软件代码审计服务 确保代码质量和安全性,提供对您的解决方案的全面评估。

参考文献

历史

  • 2011年3月11日:初始发布
© . All rights reserved.