第四章 探索 Windows 2000 的内存管理机制
翻译: Kendiv ( fcczj@263.net )
更新: Sunday, February 17, 2005
声明:转载请注明出处,并保证文章的完整性,本人保留译文的所有权利。
尽管 Spy 设备使用可缓冲的 I/O ,但它还是会检查输入 / 输出缓冲区的有效性。因为客户端程序传入的数据可能比所需的少或者提供的缓冲区不够容纳输出数据。系统不能捕获这些语意错误,因为它不知道在一次 IOCTL 传输中所传输的数据的类型。因此, SpyDispatcher() 调用帮助函数 SpyInput*() 和 SpyOutput*() 来从 I/O 缓冲区中复制或写入数据。这些函数仅在缓冲区大小与操作的需求相匹配时才执行。 列表 4-10 给出了基本的输入函数, 列表 4-11 给出了基本的输出函数。 SpyInputBinary() 和 SpyOutputBinary() 被广泛的使用,它们测试缓冲区的大小,如果 OK ,则使用 Windows 2000 运行时库函数 RtlCopyMemory() 复制被请求的数据。剩余的函数只是上述两个基本函数的简单外包,用来操作常见的数据类型 DWord , BOOL , PVOID 和 HANDLE 等。 SpyOutputBlock() 复制由调用者在 SPY_MEMORY_BLOCK 结构中指定的数据块,当然这需要首先验证请求范围内的字节都是可读的。如果传入的输入缓冲区的大小不正确, SpyInput*() 函数将返回 STATUS_INVALID_BUFFER_SIZE ,如果输出缓冲区比需要的小, SpyOutput*() 函数将返回 STATUS_BUFFER_TOO_SMALL 。
NTSTATUS SpyInputBinary (PVOID pData,
DWORD dData,
PVOID pInput,
DWORD dInput)
{
NTSTATUS ns = STATUS_INVALID_BUFFER_SIZE;
if (dData <= dInput)
{
RtlCopyMemory (pData, pInput, dData);
ns = STATUS_SUCCESS;
}
return ns;
}
// -----------------------------------------------------------------
NTSTATUS SpyInputDword (PDWORD pdValue,
PVOID pInput,
DWORD dInput)
{
return SpyInputBinary (pdValue, DWORD_, pInput, dInput);
}
// -----------------------------------------------------------------
NTSTATUS SpyInputBool (PBOOL pfValue,
PVOID pInput,
DWORD dInput)
{
return SpyInputBinary (pfValue, BOOL_, pInput, dInput);
}
// -----------------------------------------------------------------
NTSTATUS SpyInputPointer (PPVOID ppAddress,
PVOID pInput,
DWORD dInput)
{
return SpyInputBinary (ppAddress, PVOID_, pInput, dInput);
}
// -----------------------------------------------------------------
NTSTATUS SpyInputHandle (PHANDLE phObject,
PVOID pInput,
DWORD dInput)
{
return SpyInputBinary (phObject, HANDLE_, pInput, dInput);
}
列表 4-10. 从 IOCTL 缓冲区中读取输入数据
NTSTATUS SpyOutputBinary (PVOID pData,
DWORD dData,
PVOID pOutput,
DWORD dOutput,
PDWORD pdInfo)
{
NTSTATUS ns = STATUS_BUFFER_TOO_SMALL;
*pdInfo = 0;
if (dData <= dOutput)
{
RtlCopyMemory (pOutput, pData, *pdInfo = dData);
ns = STATUS_SUCCESS;
}
return ns;
}
// -----------------------------------------------------------------
NTSTATUS SpyOutputBlock (PSPY_MEMORY_BLOCK psmb,
PVOID pOutput,
DWORD dOutput,
PDWORD pdInfo)
{
NTSTATUS ns = STATUS_INVALID_PARAMETER;
if (SpyMemoryTestBlock (psmb->pAddress, psmb->dBytes))
{
ns = SpyOutputBinary (psmb->pAddress, psmb->dBytes,
pOutput, dOutput, pdInfo);
}
return ns;
}
// -----------------------------------------------------------------
NTSTATUS SpyOutputDword (DWORD dValue,
PVOID pOutput,
DWORD dOutput,
PDWORD pdInfo)
{
return SpyOutputBinary (&dValue, DWORD_,
pOutput, dOutput, pdInfo);
}
// -----------------------------------------------------------------
NTSTATUS SpyOutputBool (BOOL fValue,
PVOID pOutput,
DWORD dOutput,
PDWORD pdInfo)
{
return SpyOutputBinary (&fValue, BOOL_,
pOutput, dOutput, pdInfo);
}
// -----------------------------------------------------------------
NTSTATUS SpyOutputPointer (PVOID pValue,
PVOID pOutput,
DWORD dOutput,
PDWORD pdInfo)
{
return SpyOutputBinary (&pValue, PVOID_,
pOutput, dOutput, pdInfo);
}
列表 4-11. 向 IOCTL 的缓冲区中写入数据
你可能注意到 列表 4-7 中的 SpyDispatcher() 还引用了其他的 SpyInput*() 和 SpyOutput*() 函数。尽管这些函数最终还是调用 SpyInputBinary() 和 SpyOutputBinary() ,但它们还是比 列表 4-10 和 4-11 中的基本函数要复杂些,因此,稍后我们在讨论它们。现在,让我们从 SpyDispatcher() 开始,一步步的分析它的 switch/case 语句。
IOCTL 函数 SPY_IO_VERSION_INFO
IOCTL 的 SPY_IO_VERSION_INFO 函数用有关 Spy 驱动自身的数据填充调用者提供的 SPY_VERSION_INFO 结构。该功能不需要输入参数,需要使用 SpyOutputVersionInfo() 帮助函数。 列表 4-12 给出了该函数和 SPY_VERSION_INFO 结构,该函数很简单,它将 dVersion 成员设置为 SPY_VERSION 常量(当前是 100 ,表示 V1.00 ),该常量定义于 w2k_spy.h 中。然后复制驱动程序的符号化名称,即字符串常量 DRV_NAME (“ SBS Windows 2000 Spy Device ”)到 awName 成员。通过整除 dVersion 可获取主版本号,剩下的是次版本号。
typedef struct _SPY_VERSION_INFO
{
DWORD dVersion;
WORD awName [SPY_NAME];
}
SPY_VERSION_INFO, *PSPY_VERSION_INFO, **PPSPY_VERSION_INFO;
#define SPY_VERSION_INFO_ sizeof (SPY_VERSION_INFO)
NTSTATUS SpyOutputVersionInfo (PVOID pOutput,
DWORD dOutput,
PDWORD pdInfo)
{
SPY_VERSION_INFO svi;
svi.dVersion = SPY_VERSION;
wcscpyn (svi.awName, USTRING (CSTRING (DRV_NAME)), SPY_NAME);
return SpyOutputBinary (&svi, SPY_VERSION_INFO_,
pOutput, dOutput, pdInfo);
}
列表 4-12. 获取 Spy 驱动程序的版本信息
IOCTL 函数 SPY_IO_OS_INFO
该函数比上一个有趣的多。它是另一个只有输出的函数,不需要输入参数,使用几个操作系统的内部参数来填充调用者提供的 SPY_OS_INFO 结构。 列表 4-13 列出了该结构的定义,和 Dispatcher 调用的 SpyOutputOsInfo() 帮助函数。有些结构体成员只是被简单的设为定义于 DDK 头文件和 w2k_spy.h 中的常量;其他的将被设为从几个内部的内核变量和结构体中读取的当前值。在第二章中,你已经了解了变量 NtBuildNumber 和 NtGlobalFlag (由 ntoskrnl.exe 导出,参见 附录 B 中的 表 B-1 )。和其他的 Nt* 符号不同,这两个符号不指向 API 函数,而是指向位于内核的 .data section 中的变量。在 Win32 世界里,导出变量是十分罕见的。不过, Windows 2000 的几个内核模块都使用了这一技术。 Ntoskrnl.exe 导出了至少 55 个变量, ntdll.dll 提供了 4 个, hal.dll 提供了 1 个。 SpyOutputOsInfo() 将从 ntoskrnl.exe 导出的变量中复制 MmHighestUserAddress 、 MmUserProbeAddress 、 MmSystemRangeStart 、 NtGlobalFlag 、 KeI386MachineType 、 KeNumberProcessors 和 NtBuildNumber 到输出缓冲区中。
当一个模块从另一个模块中导入数据时,它需要使用 extern 关键字来通知编译器和链接器。这会使链接器生成一个进入模块导出节的入口,并会解析符号名以确定其地址。有些 extern 声明已经包含在 ntddk.h 。 列表 4-13 给出了缺失的那些 extern 声明。
extern PWORD NlsAnsiCodePage;
extern PWORD NlsOemCodePage;
extern PWORD NtBuildNumber;
extern PDWORD NtGlobalFlag;
extern PDWORD KeI386MachineType;
typedef struct _SPY_OS_INFO
{
DWORD dPageSize;
DWORD dPageShift;
DWORD dPtiShift;
DWORD dPdiShift;
DWORD dPageMask;
DWORD dPtiMask;
DWORD dPdiMask;
PX86_PE PteArray;
PX86_PE PdeArray;
PVOID pLowestUserAddress;
PVOID pThreadEnvironmentBlock;
PVOID pHighestUserAddress;
PVOID pUserProbeAddress;
PVOID pSystemRangeStart;
PVOID pLowestSystemAddress;
PVOID pSharedUserData;
PVOID pProcessorControlRegion;
PVOID pProcessorControlBlock;
DWORD dGlobalFlag;
DWORD dI386MachineType;
DWORD dNumberProcessors;
DWORD dProductType;
DWORD dBuildNumber;
DWORD dNtMajorVersion;
DWORD dNtMinorVersion;
WORD awNtSystemRoot [MAX_PATH];
}
SPY_OS_INFO, *PSPY_OS_INFO, **PPSPY_OS_INFO;
#define SPY_OS_INFO_ sizeof (SPY_OS_INFO)
NTSTATUS SpyOutputOsInfo (PVOID pOutput,
DWORD dOutput,
PDWORD pdInfo)
{
SPY_SEGMENT ss;
SPY_OS_INFO soi;
NT_PRODUCT_TYPE NtProductType;
PKPCR pkpcr;
NtProductType = (SharedUserData->ProductTypeIsValid
? SharedUserData->NtProductType
: 0);
SpySegment (X86_SEGMENT_FS, 0, &ss);
pkpcr = ss.pBase;
soi.dPageSize = PAGE_SIZE;
soi.dPageShift = PAGE_SHIFT;
soi.dPtiShift = PTI_SHIFT;
soi.dPdiShift = PDI_SHIFT;
soi.dPageMask = X86_PAGE_MASK;
soi.dPtiMask = X86_PTI_MASK;
soi.dPdiMask = X86_PDI_MASK;
soi.PteArray = X86_PTE_ARRAY;
soi.PdeArray = X86_PDE_ARRAY;
soi.pLowestUserAddress = MM_LOWEST_USER_ADDRESS;
soi.pThreadEnvironmentBlock = pkpcr->NtTib.Self;
soi.pHighestUserAddress = *MmHighestUserAddress;
soi.pUserProbeAddress = (PVOID) *MmUserProbeAddress;
soi.pSystemRangeStart = *MmSystemRangeStart;
soi.pLowestSystemAddress = MM_LOWEST_SYSTEM_ADDRESS;
soi.pSharedUserData = SharedUserData;
soi.pProcessorControlRegion = pkpcr;
soi.pProcessorControlBlock = pkpcr->Prcb;
soi.dGlobalFlag = *NtGlobalFlag;
soi.dI386MachineType = *KeI386MachineType;
soi.dNumberProcessors = *KeNumberProcessors;
soi.dProductType = NtProductType;
soi.dBuildNumber = *NtBuildNumber;
soi.dNtMajorVersion = SharedUserData->NtMajorVersion;
soi.dNtMinorVersion = SharedUserData->NtMinorVersion;
wcscpyn (soi.awNtSystemRoot, SharedUserData->NtSystemRoot,
MAX_PATH);
return SpyOutputBinary (&soi, SPY_OS_INFO_,
pOutput, dOutput, pdInfo);
}
列表 4-13. 获取有关操作系统的信息
SPY_OS_INFO 结构的剩余成员会由位于内存中的系统数据结构填充。例如, SpyOutputOsInfo() 将内核的进程控制区域( Kernel's Processor Control Region, KPCR )的基地址赋值给 pProcessorControlRegion 成员。 KPCR 是一个非常重要的数据结构,该结构包含很多线程相关的数据项,因此,它位于自己的内存段中,该内存段的地址由 CPU 的 FS 寄存器给出。 Windows NT 4.0 和 Windows 2000 都将 FS 指向处于内核模式的线性地址 0xFFDFF000 。 SpyOutputOsInfo() 调用 SpySegment() 函数(稍后讨论它)来查询 FS 段在线性地址空间中的基地址。这个段中还包含内核的进程控制块( Kernel's Processor Control Block, KPRCB ), KPCR 结构的 Prcb 成员指向 KPRCB 结构的首地址,紧随其后的是一个 CONTEXT 结构,该结构包含当前线程的底层 CPU 信息。 KPCR 、 KPRCB 和 CONTEXT 结构定义在 ntddk.h 头文件中。
列表 4-13 中引用的另一个内部数据结构是 SharedUserData 。该结构实际上是一个由一个“众所周知的地址”通过类型转化( TypeCast )得来的结构体指针。 列表 4-14 给出了它在 ntddk.h 中的定义。那个“众所周知的地址”位于线性地址空间中,它会在编译时被设置,因此不需要花费额外的时间或进行配置。显然, SharedUserData 是一个指向 KUSER_SHARED_DATA 结构的指针,该结构的基地址在 0xFFDF0000 (这是一个线性地址)。这个内存区域由系统和用户模式的应用程序共享,它包含像操作系统版本号这样的数据, SpyOutputOsInfo() 将该版本数据复制到 SPY_OS_INFO 结构(由调用者提供)的 dNtMajorVersion 和 dNtMinorVersion 成员。就像我稍后要展示的那样, KUSER_SHARED_DATA 结构将被映射到 0x7FFE0000 ,这样用户模式的代码就可以访问它了。
在对 Spy 设备的 IOCTL 函数的讲解之后还将提供了一个示例程序,该示例程序会把返回的数据显示在屏幕上。
#define KI_USER_SHARED_DATA 0xFFDF0000
#define SharedUserData ((KUSER_SHARED_DATA *const)KI_USER_SHARED_DATA)
列表 4-14. SharedUserData 结构定义
IOCTL 函数 SPY_IO_SEGMENT
到现在讨论以变得更加有趣了。 SPY_IO_SEGMENT 函数通过一些更底层的操作来查询指定段的属性,调用者需要首先给出一个选择器( selector )。 SpyDispatcher() 首先调用 SpyInputDword() 来获取由调用程序传入的选择器的值。你可能还记得选择器( selector )是一个 16 位的数。不过,只要可能,我就会尝试避免使用 16 位的数据类型,这是因为原生的 WORD 在 i386 CPU 的 32 位模式下是 32 位的 DWORD 类型。因此,我将选择器参数扩展为 DWORD ,不过其高 16 位总是 0 。如果 SpyInputDword() 报告操作成功,接下来就会调用 SpyOutputSegemnt() 函数( 列表 4-15 给出了此函数)。不管 SpySegment() 帮助函数如何, SpyOutputSegemnt() 总是返回到调用者。基本上来说, SpySegment() 将填充 SPY_SEGMENT 结构,该结构定义于 列表 4-15 的顶部。它以 X86_SELECTOR 结构(参见 列表 4-2 )的形式给出选择器的值,紧随其后的是 64 位的 X86_DESCRIPTOR ,以及相应的段基址,段的大小限制以及一个名为 fOk 的标志,该标志用来指出 SPY_SEGMENT 结构是否有效。在稍后的一些函数中需要一次返回多个段的属性,利用 fOk 成员,调用者就可以将无效的段信息从输出数据中筛选出来。
typedef struct _SPY_SEGMENT
{
X86_SELECTOR Selector;
X86_DESCRIPTOR Descriptor;
PVOID pBase;
DWORD dLimit;
BOOL fOk;
}
SPY_SEGMENT, *PSPY_SEGMENT, **PPSPY_SEGMENT;
#define SPY_SEGMENT_ sizeof (SPY_SEGMENT)
NTSTATUS SpyOutputSegment (DWORD dSelector,
PVOID pOutput,
DWORD dOutput,
PDWORD pdInfo)
{
SPY_SEGMENT ss;
SpySegment (X86_SEGMENT_OTHER, dSelector, &ss);
return SpyOutputBinary (&ss, SPY_SEGMENT_,
pOutput, dOutput, pdInfo);
}
BOOL SpySegment (DWORD dSegment,
DWORD dSelector,
PSPY_SEGMENT pSegment)
{
BOOL fOk = FALSE;
if (pSegment != NULL)
{
fOk = TRUE;
if (!SpySelector (dSegment, dSelector,
&pSegment->Selector))
{
fOk = FALSE;
}
if (!SpyDescriptor (&pSegment->Selector,
&pSegment->Descriptor))
{
fOk = FALSE;
}
pSegment->pBase =
SpyDescriptorBase (&pSegment->Descriptor);
pSegment->dLimit =
SpyDescriptorLimit (&pSegment->Descriptor);
pSegment->fOk = fOk;
}
return fOk;
}
列表 4-15. 查询段的属性
SpySegment() 函数依赖其他几个帮助函数,以构建 SPY_SEGMENT 结构的某些部分。首先, SpySelector() 复制一个选择器的值到传入的 X86_SELECTOR 结构中。如果 SpySelector() 函数的第一个参数 dSegment 被设置为 X86_SEGMENT_OTHER (即 0 ), dSelector 参数将假定已经指定了一个有效的选择器值,因此该值将被简单的附给输出结构 X86_SELECTOR 的 wValue 成员。否则, dSelector 将被忽略, dSegment 会被用于一个 switch/case 结构中以便选择一个段寄存器或任务寄存器 TR 。注意,这种请求需要少量的嵌入式汇编, C 语言没有提供标准的方法访问处理器相关的特性,如段寄存器。
#define X86_SEGMENT_OTHER 0
#define X86_SEGMENT_CS 1
#define X86_SEGMENT_DS 2
#define X86_SEGMENT_ES 3
#define X86_SEGMENT_FS 4
#define X86_SEGMENT_GS 5
#define X86_SEGMENT_SS 6
#define X86_SEGMENT_TSS 7
//---------------------------------------------------------------
BOOL SpySelector (DWORD dSegment,
DWORD dSelector,
PX86_SELECTOR pSelector)
{
X86_SELECTOR Selector = {0, 0};
BOOL fOk = FALSE;
if (pSelector != NULL)
{
fOk = TRUE;
switch (dSegment)
{
case X86_SEGMENT_OTHER:
{
if (fOk = ((dSelector >> X86_SELECTOR_SHIFT)
<= X86_SELECTOR_LIMIT))
{
Selector.wValue = (WORD) dSelector;
}
break;
}
case X86_SEGMENT_CS:
{
__asm mov Selector.wValue, cs
break;
}
case X86_SEGMENT_DS:
{
__asm mov Selector.wValue, ds
break;
}
case X86_SEGMENT_ES:
{
__asm mov Selector.wValue, es
break;
}
case X86_SEGMENT_FS:
{
__asm mov Selector.wValue, fs
break;
}
case X86_SEGMENT_GS:
{
__asm mov Selector.wValue, gs
break;
}
case X86_SEGMENT_SS:
{
__asm mov Selector.wValue, ss
break;
}
case X86_SEGMENT_TSS:
{
__asm str Selector.wValue
break;
}
default:
{
fOk = FALSE;
break;
}
}
RtlCopyMemory (pSelector, &Selector, X86_SELECTOR_);
}
return fOk;
}
列表 4-16. 获取选择器( selector )的值
SpyDispatcher() 将从一个 64 位的描述符中读取数据,段选择器指向该描述符(见 列表 4-17 )。像你记得的那样,所有的选择器都包含一个表指示符( Table Indicator, TI )位,以确定选择器引用的描述符是位于 GDT ( TI=0 )中还是 LDT ( TI=1 )中。 列表 4-17 的上半部分处理了是 LDT 的情况。首先,使用汇编指令 SLDT 和 SGDT 分别读取 LDT 选择器的值以及段的大小限制和 GDT 的基地址。还记得 GDT 的线性基地址是显示指定的,而 LDT 是由 GDT 中的选择器间接引用的吗?所以, SpyDispatcher() 会首先验证 LDT 选择器的值。如果段选择器不为空并且没有超过 GDT 的限制,就会调用 SpyDescriptorType() 、 SpyDescriptorLimit() 和 SpyDescriptorBase()( 列表 4-17 给出了这些函数 ) 来获取 LDT 的基本属性:
l SpyDescriptorType() 返回描述符的类型数据及其 S 位域(参见 列表 4-2 )。 LDT 选择器必须指向一个类型为 X86_DESCRIPTOR_SYS_LDT 的系统描述符。
l SpyDescriptorLimit() 从描述符的 Limit1 、 Limit2 这两个位域中汇总段的大小限制。根据描述符的 G 标志指定的内存分配粒度的不同,其处理方式也会不同。
l SpyDescriptorBase() 只是简单的通过适当的组织描述符的 Base1 、 Base2 和 Base3 位域以获取一个 32 位的线性地址。
BOOL SpyDescriptor (PX86_SELECTOR pSelector,
PX86_DESCRIPTOR pDescriptor)
{
X86_SELECTOR ldt;
X86_TABLE gdt;
DWORD dType, dLimit;
BOOL fSystem;
PX86_DESCRIPTOR pDescriptors = NULL;
BOOL fOk = FALSE;
if (pDescriptor != NULL)
{
if (pSelector != NULL)
{
if (pSelector->TI) // ldt descriptor
{
__asm
{
sldt ldt.wValue
sgdt gdt.wLimit
}
if ((!ldt.TI) && ldt.Index &&
((ldt.wValue & X86_SELECTOR_INDEX)
<= gdt.wLimit))
{
dType = SpyDescriptorType (gdt.pDescriptors +
ldt.Index,
&fSystem);
dLimit = SpyDescriptorLimit (gdt.pDescriptors +
ldt.Index);
if (fSystem && (dType == X86_DESCRIPTOR_SYS_LDT)
&&
((DWORD) (pSelector->wValue
& X86_SELECTOR_INDEX)
<= dLimit))
{
pDescriptors =
SpyDescriptorBase (gdt.pDescriptors +
ldt.Index);
}
}
}
else // gdt descriptor
{
if (pSelector->Index)
{
__asm
{
sgdt gdt.wLimit
}
if ((pSelector->wValue & X86_SELECTOR_INDEX)
<= gdt.wLimit)
{
pDescriptors = gdt.pDescriptors;
}
}
}
}
if (pDescriptors != NULL)
{
RtlCopyMemory (pDescriptor,
pDescriptors + pSelector->Index,
X86_DESCRIPTOR_);
fOk = TRUE;
}
else
{
RtlZeroMemory (pDescriptor,
X86_DESCRIPTOR_);
}
}
return fOk;
}
// -----------------------------------------------------------------
PVOID SpyDescriptorBase (PX86_DESCRIPTOR pDescriptor)
{
return (PVOID) ((pDescriptor->Base1 ) |
(pDescriptor->Base2 << 16) |
(pDescriptor->Base3 << 24));
}
// -----------------------------------------------------------------
DWORD SpyDescriptorLimit (PX86_DESCRIPTOR pDescriptor)
{
return (pDescriptor->G ? (pDescriptor->Limit1 << 12) |
(pDescriptor->Limit2 << 28) | 0xFFF
: (pDescriptor->Limit1 ) |
(pDescriptor->Limit2 << 16));
}
// -----------------------------------------------------------------
DWORD SpyDescriptorType (PX86_DESCRIPTOR pDescriptor,
PBOOL pfSystem)
{
if (pfSystem != NULL) *pfSystem = !pDescriptor->S;
return pDescriptor->Type;
}
列表 4-17. 获取描述符的值
如果选择器的 TI 位指定了一个 GDT 描述符,事情就简单了。再次使用 SGDT 指令来取出 GDT 在线性内存中的位置和大小,如果选择器指定的描述符索引位于适当的范围, pDescriptors 变量将被设置为指向 GDT 的基地址。对于 LDT 和 GDT 来说, pDescriptors 变量都不会为空。如果调用者传入的选择器是有效的, 64 位的描述符值将被复制到调用者提供的 X86_DESCRIPTOR 结构中。否则,该结构的所有成员都会被 RtlZeroMemory() 设为 0 。
我们仍然在讨论 列表 4-15 中的 SpySegment() 函数。 SpySelector() 和 SpyDescriptor() 调用已经解释了。只剩下最后的 SpyDescriptorBase() 和 SpyDescriptorLimit() 调用,不过你应该已经知道这些函数作了些什么(见 列表 4-17 )。如果 SpySelector() 和 SpyDescriptor() 成功,返回的 SPY_SEGMENT 结构将是有效的。 SpyDescriptorBase() 和 SpyDescriptorLimit() 不会返回出错标志。因为它们不可能失败,如果提供的描述符无效,只是会让它们返回错误的数据而已。
标签: