第六章 在用户模式下调用内核API函数
翻译:Kendiv( fcczj@263.net )
更新:Friday, May 13, 2005
声明:转载请注明出处,并保证文章的完整性,本人保留译文的所有权利。
将调用接口封装为DLL
尽管w2k_spy.sys已经导出了针对内核函数的IOCTL调用接口,但该接口在使用时多少有些不太方便。如果你想调用一个简单的函数,如MmGetPhysicalAddress()或MmIsAddressValid()。首先,你必须使用要调用的函数及其所需参数来构建一个SPY_CALL_INPUT结构,接下来,你必须发出一个Win32 DeviceIoControl()调用。如果该函数返回ERROR_SUCCESS,则你必须计算返回的SPY_CALL_OUTPUT结构,而且必须恰当的处理发生的错误。这看起来一点也不吸引人,不是吗?解决这一问题的方案就是将IOCTL机制隐藏到DLL中。这就是包含在本书光盘中的w2k_call.dll项目的目的所在。本节给出了摘自w2k_call.c和w2k_call.h中的代码片断,可以在光盘的\src\w2k_call\目录下找到这两个文件。
处理IOCTL函数调用
在开始其他工作之前,必须先以一种简便的方式将DeviceIoControl()调用封装起来,因为这是所有内核函数调用必须经过的一个瓶颈。列表6-18给出了外包函数w2kSpyControl(),该函数的核心部分还是调用DeviceIoControl()。总的来说,此外包函数将执行如下的任务:
l 验证输入/输出参数
l 如果还未加载Spy device,则加载Spy device driver,并打开Spy device。
l 调用DeviceIoControl()
l 测试输出数据是否为预期大小
l 设置恰当的Win32错误代码(Win32 last-error code)
如果成功的话,应用程序通过GetLastError()获取到的系统最后错误代码将被设置为ERROR_SUCCESS(其值为0)。否则,将按照如下策略来设置错误代码:
l 如果输入或输出参数无效,错误代码将被设为ERROR_INVALID_PARAMETER(87),表示“参数不正确”(依照SDK头文件winerror.h)。
l 如果Spy device无法初始化,错误代码将被设为ERROR_GEN_FAILURE(31),这表示“与系统相关的一个设备无法工作”。
l 如果spy device返回的数据的大小与调用者提供的缓冲区大小不一致,错误代码将被设为ERROR_DATATYPE_MISMATCH,这表示“提供的数据的类型有错误”。
l 对于其他的情况,w2kSpyControl()保留DeviceIoControl()设置的错误代码。这通常是spy device返回的NTSTATUS,不过该返回值已映射为适当的Win32状态代码。
BOOL WINAPI w2kSpyControl (DWORD dCode,
PVOID pInput,
DWORD dInput,
PVOID pOutput,
DWORD dOutput)
{
DWORD dInfo = 0;
BOOL fOk = FALSE;
SetLastError (ERROR_INVALID_PARAMETER);
if (((pInput != NULL) || (!dInput )) &&
((pOutput != NULL) || (!dOutput)))
{
if (w2kSpyStartup (FALSE, NULL))
{
if (DeviceIoControl (ghDevice, dCode,
pInput, dInput,
pOutput, dOutput,
&dInfo, NULL))
{
if (dInfo == dOutput)
{
SetLastError (ERROR_SUCCESS);
fOk = TRUE;
}
else
{
SetLastError (ERROR_DATATYPE_MISMATCH);
}
}
}
else
{
SetLastError (ERROR_GEN_FAILURE);
}
}
return fOk;
}
列表6-18. 基本的DeviceIoContorl外包函数
对于列表6-18中调用的w2kSpyStartup()(在调用DeviceIoControl之前),我们应更多的关注一下,因为w2k_call.dll依赖于内核模式的驱动程序所提供的服务,该驱动程序必须在第一个IOCTL调用之前被加载到内存中。而且该设备的句柄也必须被打开,以表示可通过DeviceIoControl()访问该目标设备。为了使DLL具有尽可能大的灵活性,我选择了混合模式,在此模式下,调用者可以完全的控制Spy device的加载/卸载、打开/关闭或者采用默认的机制(将设备的管理任务交给DLL来完成)。这种自动化机制非常简单:直道第一次请求IOCTL调用,才加载并打开Spy device。一旦DLL被卸载了,将自动关闭设备的句柄,但仍将内核模式的驱动程序保留在内存中。接下来,我们将制订一个预防性的策略。只要调用者不提供任何有关如何处理驱动程序的具体信息,w2k_call.dll就将假定其他客户还在使用驱动程序,此时它将不会卸载该驱动程序。产生问题的并不是仍然持有Spy device句柄的进程。Windows 2000服务控制器(Service Control Manager,SCM)仅在设备的所有句柄都关闭的情况下才会卸载驱动程序。现在的问题是,SCM不允许打开任何新的设备句柄。
w2k_call.dll的客户程序可以通过w2kSpyStartup()和w2kSpyCleanup()来控制Spy device的状态,列表6-19给出了这两个函数。由于这两个函数可能在多线程环境下被调用,因此它们使用了一个临界区对象来进行同步。同一时刻,只能有一个线程可以加载/打开或者关闭/卸载Spy device。例如,两个线程几乎同时调用了w2kStartup(),那么只有一个线程可以打开设备句柄。另一个将被暂停。
BOOL WINAPI w2kSpyLock (VOID)
{
BOOL fOk = FALSE;
if (gpcs != NULL)
{
EnterCriticalSection (gpcs);
fOk = TRUE;
}
return fOk;
}
// -----------------------------------------------------------------
BOOL WINAPI w2kSpyUnlock (VOID)
{
BOOL fOk = FALSE;
if (gpcs != NULL)
{
LeaveCriticalSection (gpcs);
fOk = TRUE;
}
return fOk;
}
// -----------------------------------------------------------------
BOOL WINAPI w2kSpyStartup (BOOL fUnload,
HINSTANCE hInstance)
{
HINSTANCE hInstance1;
SC_HANDLE hControl;
BOOL fOk = FALSE;
w2kSpyLock ();
hInstance1 = (hInstance != NULL ? hInstance : ghInstance);
if ((ghDevice == INVALID_HANDLE_VALUE) &&
w2kFilePath (hInstance1, awSpyFile, awDriver, MAX_PATH)
&&
((hControl = w2kServiceLoad (awSpyDevice, awSpyDisplay,
awDriver, TRUE))
!= NULL))
{
ghDevice = CreateFile (awSpyPath,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL);
if ((ghDevice == INVALID_HANDLE_VALUE) && fUnload)
{
w2kServiceUnload (awSpyDevice, hControl);
}
else
{
w2kServiceDisconnect (hControl);
}
}
fOk = (ghDevice != INVALID_HANDLE_VALUE);
w2kSpyUnlock ();
return fOk;
}
// -----------------------------------------------------------------
BOOL WINAPI w2kSpyCleanup (BOOL fUnload)
{
BOOL fOk = FALSE;
w2kSpyLock ();
if (ghDevice != INVALID_HANDLE_VALUE)
{
CloseHandle (ghDevice);
ghDevice = INVALID_HANDLE_VALUE;
}
if (fUnload)
{
w2kServiceUnload (awSpyDevice, NULL);
}
w2kSpyUnlock ();
return fOk;
}
列表6-19. Spy device的管理函数
针对特定类型(type-specific)的调用接口函数
对DeviceIoControl()调用以及Spy device的自动管理机制已被整理为一组相关的函数,w2kSpyControl()是这组函数的主入口点。接下来我们将提供一个函数来执行Spy device的SPY_IO_CALL调用。列表6-20给出了内核调用接口在用户模式下的基本实现方式,这由w2kCallExecute()、w2kCall()和w2kCallV()组成。它们所需的输入参数,在形式上等价于用户模式下的SpyCallEx()(参见列表6-3)。事实上,从w2kCallExecute()的实现方式上可以看出,它首先确认在传入的控制块中是否包含一个符号名或者一个入口地址,然后通过w2kSpyControl()调用Spy device的SPY_IO_CALL函数。从列表6-12中,我们可看出SPY_IO_CALL是由SpyOutputCall()(见列表6-17)实现的,SpyOutputCall()依赖于SpyModuleSymbolEx()和SpyCallEx()。
BOOL WINAPI w2kCallExecute (PSPY_CALL_INPUT psci,
PSPY_CALL_OUTPUT psco)
{
BOOL fOk = FALSE;
SetLastError (ERROR_INVALID_PARAMETER);
if (psco != NULL)
{
psco->uliResult.QuadPart = 0;
if ((psci != NULL)
&&
((psci->pbSymbol != NULL) ||
(psci->pEntryPoint != NULL)))
{
fOk = w2kSpyControl (SPY_IO_CALL,
psci, SPY_CALL_INPUT_,
psco, SPY_CALL_OUTPUT_);
}
}
return fOk;
}
// -----------------------------------------------------------------
BOOL WINAPI w2kCall (PULARGE_INTEGER puliResult,
PBYTE pbSymbol,
PVOID pEntryPoint,
BOOL fFastCall,
DWORD dArgumentBytes,
PVOID pArguments)
{
SPY_CALL_INPUT sci;
SPY_CALL_OUTPUT sco;
BOOL fOk = FALSE;
sci.fFastCall = fFastCall;
sci.dArgumentBytes = dArgumentBytes;
sci.pArguments = pArguments;
sci.pbSymbol = pbSymbol;
sci.pEntryPoint = pEntryPoint;
fOk = w2kCallExecute (&sci, &sco);
if (puliResult != NULL) *puliResult = sco.uliResult;
return fOk;
}
// -----------------------------------------------------------------
BOOL WINAPI w2kCallV (PULARGE_INTEGER puliResult,
PBYTE pbSymbol,
BOOL fFastCall,
DWORD dArgumentBytes,
...)
{
return w2kCall (puliResult, pbSymbol, NULL, fFastCall,
dArgumentBytes, &dArgumentBytes + 1);
}
列表6-20. 基本的内核调用接口函数
列表6-20中的SpyCall()和w2kCallV()是内核调用接口的核心函数,它们位于w2k_call.dll中。这两个函数为几个特定的函数提供基本服务。w2kCall()的主要目标是在调用w2kCallExecute()之前将它的参数放入SPY_CALL_INPUT结构中,并将w2kCallExecute()返回的ULARGE_INTEGER作为自己的返回值。就像前面解释过的那样,返回值的所有位并不必须都是有效的,这依赖于被调用内核函数的返回值类型。w2kCallV()只是w2kCall()的一个简单的外包函数,它的参数列表是可变的(这也是函数名末尾的V的含义)。由于w2kCall()的参数列表是针对通用的内核API调用来设计的,因此它并不适合大多数常见的函数类型。比如,最常见的函数类型是__stdcall(或者NTAPI),这些函数将返回一个NTSTATUS。在这种情况下,fFastCall参数将总为FALSE,并且返回的64位ULARGE_INTEGER中仅有低32位包含有效的数据。因此,列表6-21提供了w2kCallNT()函数来更好的完成这一工作。请注意w2kCallNT()是如何控制w2kCall()产生的错误的。如果w2kCall()返回FALSE,这意味着w2kSpyControl()调用失败,函数的返回值是无效的。此时,就没有必要取出uliResult结构中的低32位值。因此,w2kCallNT()默认将返回STATUS_IO_DEVICE_ERROR(0xC0000185)。在这一切都结束后,调用者必须准备好处理返回值不是STATUS_SUCESS的情况,对于非STATUS_SUCESS的返回值报告一个错误代码将是一个合理的决定。对于其他不返回NTSTATUS的内核函数,当它们失败时,必须小心的选择默认的返回值。
NTSTATUS WINAPI w2kCallNT (PBYTE pbSymbol,
DWORD dArgumentBytes,
...)
{
ULARGE_INTEGER uliResult;
return (w2kCall (&uliResult, pbSymbol, NULL, FALSE,
dArgumentBytes, &dArgumentBytes + 1)
? uliResult.LowPart
: STATUS_IO_DEVICE_ERROR);
}
列表6-21. 针对NTAPI/NTSTATUS函数类型的简单接口
列表6-22给出了针对__stdcall类型的API函数的5个附加接口函数,这些函数将返回基本的数据类型:BTYE、WORD、DWORD、DWORDLOG和PVOID。函数名末尾的数字表示返回值中的有效位的个数。w2kCallP()基本上等价于w2kCall32(),不同之处在于w2kCallP()将返回的32位值转型为一个指针。没有必要针对返回有符号数据类型或任意类型的指针而提供单独的函数。
编译器可以针对这些微小的不同自动进行类型转化(typecasting)。这里需要注意的是,列表6-22中所有函数的第一个参数都将作为其默认的返回值。这样做是必须的,因为调用接口无法知道当对内核函数的调用失败后,最好返回什么样的值,所以调用者应该负起这一责任。
BYTE WINAPI w2kCall08 (BYTE bDefault,
PBYTE pbSymbol,
BOOL fFastCall,
DWORD dArgumentBytes,
...)
{
ULARGE_INTEGER uliResult;
return (w2kCall (&uliResult, pbSymbol, NULL, fFastCall,
dArgumentBytes, &dArgumentBytes + 1)
? (BYTE) uliResult.LowPart
: bDefault);
}
// -----------------------------------------------------------------
WORD WINAPI w2kCall16 (WORD wDefault,
PBYTE pbSymbol,
BOOL fFastCall,
DWORD dArgumentBytes,
...)
{
ULARGE_INTEGER uliResult;
return (w2kCall (&uliResult, pbSymbol, NULL, fFastCall,
dArgumentBytes, &dArgumentBytes + 1)
? (WORD) uliResult.LowPart
: wDefault);
}
// -----------------------------------------------------------------
DWORD WINAPI w2kCall32 (DWORD dDefault,
PBYTE pbSymbol,
BOOL fFastCall,
DWORD dArgumentBytes,
...)
{
ULARGE_INTEGER uliResult;
return (w2kCall (&uliResult, pbSymbol, NULL, fFastCall,
dArgumentBytes, &dArgumentBytes + 1)
? uliResult.LowPart
: dDefault);
}
// -----------------------------------------------------------------
QWORD WINAPI w2kCall64 (QWORD qDefault,
PBYTE pbSymbol,
BOOL fFastCall,
DWORD dArgumentBytes,
...)
{
ULARGE_INTEGER uliResult;
return (w2kCall (&uliResult, pbSymbol, NULL, fFastCall,
dArgumentBytes, &dArgumentBytes + 1)
? uliResult.QuadPart
: qDefault);
}
// -----------------------------------------------------------------
PVOID WINAPI w2kCallP (PVOID pDefault,
PBYTE pbSymbol,
BOOL fFastCall,
DWORD dArgumentBytes,
...)
{
ULARGE_INTEGER uliResult;
return (w2kCall (&uliResult, pbSymbol, NULL, fFastCall,
dArgumentBytes, &dArgumentBytes + 1)
? (PVOID) uliResult.LowPart
: pDefault);
}
列表6-22. 针对常见的函数类型的接口函数
用于数据复制的接口函数
我在前面提到过Spy device的内核调用接口还可以处理内核模块导出的公开变量(public variable)。在列表6-2中给出了SpyCall()函数,其中针对参数堆栈大小的负值(通过SPY_CALL_INPUT结构的dArgumentBytes成员传入)的补码将被解释为导出变量的大小。此时,SpyCall()将不会调用指定的入口点(entry point),而是从该入口点处复制适当的字节到结果缓冲区中。如果dArgumentBytes被设为-1,则意味着将入口点自身的地址复制到缓冲区中(-1的补码为0)。
列表6-23给出了由w2k_call.dll导出的数据复制函数。这组函数与列表6-22中的函数非常相似。不过,列表6-23中的函数只需要很少的输入参数。复制导出变量的值,只需要提供变量的名字即可,不需要多余的输入参数以及调用约定。
BOOL WINAPI w2kCopy (PULARGE_INTEGER puliResult,
PBYTE pbSymbol,
PVOID pEntryPoint,
DWORD dBytes)
{
return w2kCall (puliResult, pbSymbol, pEntryPoint, FALSE,
0xFFFFFFFF - dBytes, NULL);
}
// -----------------------------------------------------------------
BYTE WINAPI w2kCopy08 (BYTE bDefault,
PBYTE pbSymbol)
{
ULARGE_INTEGER uliResult;
return (w2kCopy (&uliResult, pbSymbol, NULL, 1)
? (BYTE) uliResult.LowPart
: bDefault);
}
// -----------------------------------------------------------------
WORD WINAPI w2kCopy16 (WORD wDefault,
PBYTE pbSymbol)
{
ULARGE_INTEGER uliResult;
return (w2kCopy (&uliResult, pbSymbol, NULL, 2)
? (WORD) uliResult.LowPart
: wDefault);
}
// -----------------------------------------------------------------
DWORD WINAPI w2kCopy32 (DWORD dDefault,
PBYTE pbSymbol)
{
ULARGE_INTEGER uliResult;
return (w2kCopy (&uliResult, pbSymbol, NULL, 4)
? uliResult.LowPart
: dDefault);
}
// -----------------------------------------------------------------
QWORD WINAPI w2kCopy64 (QWORD qDefault,
PBYTE pbSymbol)
{
ULARGE_INTEGER uliResult;
return (w2kCopy (&uliResult, pbSymbol, NULL, 8)
? uliResult.QuadPart
: qDefault);
}
// -----------------------------------------------------------------
PVOID WINAPI w2kCopyP (PVOID pDefault,
PBYTE pbSymbol)
{
ULARGE_INTEGER uliResult;
return (w2kCopy (&uliResult, pbSymbol, NULL, 4)
? (PVOID) uliResult.LowPart
: pDefault);
}
// -----------------------------------------------------------------
PVOID WINAPI w2kCopyEP (PVOID pDefault,
PBYTE pbSymbol)
{
ULARGE_INTEGER uliResult;
return (w2kCopy (&uliResult, pbSymbol, NULL, 0)
? (PVOID) uliResult.LowPart
: pDefault);
}
列表6-23. 针对基本数据类型的数据复制接口函数
在列表6-23中,w2kCopy()将完成大多数的工作,这很像用来处理函数调用的w2kCall()。再次强调,w2k_call.dll为基本的数据类型:BTYE、WORD、DWORD、DWORDLOG和PVOID,提供了单独的函数。其函数名末尾的数字表示返回值中有效位的个数。w2kCpyP()返回一个指针,w2kCopyEP()用来处理查询一个入口地址。调用w2kCopyEP()等价于调用Spy device的SPY_IO_PE_SYMBOL函数。是的,这个函数是有些多余,不过,有两条可以回家的路总比一条没有的好,不是吗?
……………..待续……………