32位和64位Windows驱动有什么不同?
怎么兼容32位和64位Windows驱动?
在编程的时候,32位系统跟64位系统数据类型是有些不一样的。
在64位环境下,指针、句柄是64位的, 也就是8个字节长度的。
Windows为64位环境提供了一些指针类型的数据类型。
固定精度的数据类型
类型 定义
DWORD32 32位无符号整数
DWORD64 64位无符号整数
INT32 32位有符号整数
INT64 64位有符号整数
LONG32 32位有符号整数
LONG64 64位有符号整数
UINT32 无符号INT32
UINT64 无符号INT64
LONG32 无符号LONG32
LONG64 无符号LONG64
当需要数据类型的精度随着处理器位数大小变化时,
请使用指针精度类型。这些类型称为”多态“数据类型。
这些类型通常以”_PTR”后缀结尾。
指针精度的数据类型
类型 定义
DWORD_PTR 指针精度的无符号长类型
HALF_PTR 指针大小的一半。用于包含一个指针和两个小型字段的结构
INT_PTR 指针精度的有符号整型
LONG_PTR 指针精度的有符号长类型
SIZE_T 指针可以引用的最大字节数,用于必须跨指针的整个范围
SSIZE_T 有符号SIZE_T
UHALF_PTR 无符号HALF_PTR
UINT_PTR 无符号INT_PTR
ULONG_PTR 无符号LONG_PTR
LPARAM 与LONG_PTR同义词(在Wtypes.h里定义)
WPARAM 与UINT_PTR同义词(在Wtypes.h里定义)
注意:在使用数据保存指针或句柄时,请尽量使用同类型变量,如果因为程序原因不能使用同类型变量时,使用指针精度变量。
编写驱动时,除了要考虑移植驱动本身的问题外,还有一个问题要解决:移植完驱动后,上层应用程序怎么办?一般可以把上层应用改成64位。但是有时不行,比如调用了一个没有源码的第三方库,这时把应用程序移植到64位是不现实的。但需要支持64位操作系统,怎么办?
需要解决32位应用程序与64位驱动配合协作的问题。
1、驱动要支持32位IOCTL
某些IOCTL包含指针的结构,要特别小心地区别对待它,根据被调用者解析结构或者输出结构。
有三种办法可以解决这个问题:
1. 尽量避免使用IOCTL传递包含指针的结构。
2. 通过APIOIs32bitProcess()来判断上层调用者的程序类型。
3. 在64位程序里采用新的IOCTL命令。
/*IOCTL structure in header file*/
typedef struct _IOCTL_PARAMETERS {
PVOID Addr;
SIZE_T Length;
HANDLE handle;
}IOCTL_PARAMETERS, *PIOCTL_PARAMETERS;
/*32-bit IOCTL stucture*/
/*This structure is defined
inside the driver source code*/
typedef struct _IOCTL_PARMETERS_32 {
VOID * POINTER_32 Addr;
INT32 Length;
VOID * POINTER_32 Handle;
}IOCTL_PARAMETERS_32, *PIOCTL_PARAMETERS_32;
#ifdef _WIN64
/*32-Bit and 64-Bit IOCTL*/
case IOCTL_GEGISTER:
if(IoIs32bitProcess(Irp)) {
/*If this is a 32 bit process.*/
params32 = (PIOCTL_PARAMETERS_32)(Irp>AssociatedIrp.SystemBuffer);
if(irpSp->Parameters.DeviceIoControl.InputBufferLength < sizeof(IOCTL_PARAMETERS_32))
{
status = STATUS_INVALID_PARAMETER;
}else{
LocalParam.Addr = params32->Addr;
LocalParam.Handle = params32->Handle;
LocalParam.Length = params32->Length;
/*Handle the ioctl here*/
status = STATUS_SUCCESS;
Irp->IoStatus.Information = sizeof(IOCTL_PARAMETERS);
}
}else {
/*64bit process IOCTL*/
params = (PIOCTL_PARAMETERS)(Irp->AssociatedIrp.SystemBuffer);
if(irpSp->Parameters.DeviceIoControl.InputBufferLength < sizeof(IOCTL_PARAMETERS)){
status = STATUS_INVALID_PARAMETER;
}else{
RtlCopyMemory(&LocalParam, params, sizeof(IOCTL_PARAMETERS));
/*Handle the ioctl here*/
stastus = STATUS_INVALID_PARAMETER;
}
Irp->IoStatus.Information = sizeof(IOCTL_PARAMETERS);
}
}
break;
#endif
使用固定精度的数据类型代替指针精度的数据类型,
为了解决因数据类型导致32位和64位数据长度不一致的问题,我们可以采用固定精度的数据类型来代替指针精度的数据类型处理数据。
例如:
struct example
{
Void *p;
HANDLE h;
}
上面的指针和句柄都是指针精度的数据类型,根据32位或64位指令的情况进行变化。无法让一个驱动兼容所有的应用程序,为了让它可以兼容64位和32位应用程序,可以改成固定精度的数据类型:
struct example
{
ULONG64 P;
ULONG64 h;
}
这样子数据长度固定为64位,就不会出现由于指针变化引起的故障。当然,这样处理会牺牲效率。
避免固定精度的数据类型不对齐。
可以用固定精度的数据类型代替指针精度的数据类型来解决32位和64位驱动兼容问题,不过在跨CPU类型时会出现其它问题。
在32位和64位混合编程时,会发生数据长度一致,但对齐长度不一样的情况。不是所有的IOCTL/FSCTL缓冲对齐的问题可以通过修改指针精度的数据类型为固定精度的数据类型可以解决的。这意味着内核模式驱动的IOCTL和FSCTL请求中传递的固定精度的数据类型或者指针精度数据类型需要进行转换(thunked),所谓thunked就是在不同长度或类型之间转换。前面提到的32位和64位之间的数据传递方式就是一种转换,因为长度不一致,只能赋值不能直接进行内存拷贝。
哪些数据类型会被影响?
结构类型的固定精度数据会被影响。
因为在不同的平台上结构的对齐长度不一样。
要知道指定数据类型在特定平台上对齐长度,可以使用TYPE_ALIGNMENT宏。
怎么解决这个问题呢?
下面的例子中使用METHOD_NEITHER类型的IOCTL请求,因此Irp->UserBuffer指针是直接从用户模式的应用程序传递到内核驱动的。IOCTL和FSCTL传递的指针无法保证有效性,有可能传递下来的地址当前无效,因此需要调用ProbeForRead或ProbeForWrite来检查缓冲区的有效性。假如32位的应用程序用Irp->UserBuffer传递一个有效的缓冲区地址,p->DeviceTime指向一个LARGE_INTEGER结构,它是4字节对齐的。ProbeForRead通过Alignment参数传人的值检查对齐情况,Alignment参数的值其实就是TYPE_ALIGNMENT(LARGE_INTEGER)的结果。在x86平台上,这个宏返回4,在安腾平台返回8,导致ProbeForRead出错,返回STATUS_DATATYPE_MISALIGNMENT。如果在代码里去掉ProbeForRead的调用并不能解决问题,只会导致硬件诊断错误,出现蓝屏死机。
typedef struct _IOCTL_PARAMETERS2 {
LARGE_INTEGER DeviceTime;
}IOCTL_PARAMETERS2, *PIOCTL_PARAMETERS2;
#define SETTIME_FUNCTION 1
#define IOCTL_SETTIME CTL_CODE(FILE_DEVICE_UNKNOWN, \
SETTIME_FUNCTION, METHOD_NEITHER, FILE_ANY_ACCESS)
...
case IOCTL_SETTIME:
PIOCTL_PARAMETERS2 P = (PIOCTL_PARAMETERS2)Irp->UserBuffer;
try {
if (Irp->RequestorMode != KernelMode) {
ProbeForRead(p->DeviceTime,
sizeof(LARGE_INTEGER),
TYPE_ALIGNMENT(LARGE_INTEGER));
}
}
status = DoSomeWork(p->DeviceTime);
}except(EXEPTION_EXECUTE_HANDLER){
下面通过简单示例讲述怎么解决上面描述的问题。
方法一,拷贝缓冲区:
避免缓冲区对不齐的安全方法是访问缓冲区内容之前进行拷贝操作:
case IOCTL_SETTIME:
PIOCTL_PARAMETERS2 P = (PIOCTL_PARAMETERS2)Irp->UserBuffer;
#if _WIN64
PIOCTL_PARAMETERS2 LocalParams2;
RtlCopyMemory(&LocalParams2, p, sizeof(PIOCTL_PARAMETERS2));
p = &LocalParams2;
#endif
status = DoSomeWork(p->DeviceTime);
break;
这个方法可以进行一些性能优化,先检查缓冲区是否对齐,如果对齐就直接用,否则驱动程序进行缓冲区拷贝。
case IOCTL_SETTIME:{
PIOCTL_PARAMETERS2 P = (PIOCTL_PARAMETERS2)Irp->UserBuffer;
#if _WIN64
PIOCTL_PARAMETERS2 LocalParams2;
if ((ULONG_PTR)p & (TYPE_ALIGNMENT(IOCTL_PARAMETERS2) - 1)){
/*The buffer contents are not correctly aligned for this
platform, so copy them into a property aligned local buffer*/
RtlCopyMemory(&LocalParams2, p, sizeof(PIOCTL_PARAMETERS2));
p = &LocalParams2;
}
#endif
status = DoSomeWork(p->DeviceTime);
break;
方法二,使用宏
UNALIGNED宏告诉c编译器生成可以无故障的DeviceTime域的代码。注意:在安腾平台上使用这个宏,会导致驱动程序体积变大,速度变慢。
typedef struct _IOCTL_PARAMETERS2 {
LARGE_INTEGER DeviceTime;
}IOCTL_PARAMETERS2;
typedef IOCTL_PARAMETERS2 UNALIGNED *PIOCTL_PARAMETERS2;
指针类型一样会被影响。
前面讲的不对齐的问题一样会发生在缓冲型的I/O请求里。
下面例子里,IOCTL缓冲区包含一个指向LARGE_INTEGER结构的指针。
typedef struct _IOCTL_PARAMETERS3 {
LARGE_INTEGER *pDeviceCount;
}IOCTL_PARAMETERS3, *PIOCTL_PARAMETERS3;
#define COUNT_FUNCTION 1
#define IOCTL_SETTIME CTL_CODE(FILE_DEVICE_UNKNOWN, \
COUNT_FUNCTION, METHOD_BUFFERED, FILE_ANY_ACCESS)
就像前面讲的基于METHOD_NEITHER的IOCTL和FSCTL请求缓冲区一样,嵌入到缓冲类型的I/O请求中的指针是直接从应用层程序中传递到内核驱动中的。在这些指针上没有进行检验,因此在可以安全地访问这些内嵌的指针之前,需要在一个try/catch块里调用ProbeForRead或ProbeForWrite进行缓冲区指针检验。
在例子里,假设32位应用程序传递一个有效的PdeviceCount,pDeviceCount指向的LARGE_INTEGER结构按4字节对齐了。ProbeForRead或ProbeForWrite函数通过参数Alignment传人的值进行对齐检查,在这种情况下,它的值是TYPE_ALIGNMENT(LARGE_INTEGER)。在x86平台(包括x86-64平台)它返回4;在安腾平台返回8,导致ProbeForRead或ProbeForWrite函数发生STATUS_DATATYPE_MISALIGNMENT异常。
解决这个问题的办法是进行适当的以对齐为目的的LARGE_INTEGER结构的缓冲区拷贝,或者对下面的结构使用UNALIGNED宏。
typedef struct _IOCTL_PARAMETERS3 {
LARGE_INTEGER UNALIGNED *pDeviceCount;
}IOCTL_PARAMETERS3, *PIOCTL_PARAMETERS3;