HOWTO:将 IOCTL 发送到筛选器驱动程序

HOWTO:将 IOCTL 发送到筛选器驱动程序

<script type="text/javascript">function loadTOCNode(){}</script>
文章编号:262305
最后修改:2003年10月23日
修订:1.4
本文的发布号曾为 CHS262305

概要

<script type="text/javascript">loadTOCNode(1, 'summary');</script>
本文说明如何通过创建独立的控件 deviceobject 而不是打开由筛选器驱动程序注册的专用设备接口 (IoRegisterDeviceInterface),将 IOCTL 请求发送到即插即用 (PNP) 筛选器驱动程序。

更多信息

<script type="text/javascript">loadTOCNode(1, 'moreinformation');</script>
正在编写筛选器驱动程序的开发人员可能需要将自定义设备 I/O 控制请求从应用程序或另一个驱动程序发送到它们的筛选器驱动程序,以便控制其行为。在基于 Windows 2000 的计算机上,Microsoft 建议驱动程序在 AddDevice 例程中为要与它们进行交互的其他应用程序或驱动程序注册设备接口。

按照此建议,筛选器驱动程序编写者通过在 PhysicalDeviceObject (PDO) 上注册他们自己的 GUID(在 AddDevice 例程中收到),编写自定义应用程序以枚举此接口 GUID 并将 IOCTL 发送到驱动程序。
status = IoRegisterDeviceInterface (
PhysicalDeviceObject,
(LPGUID) &MY_SPECIAL_INTERFACE_GUID,
NULL, 
&devExt->InterfaceName);

只要筛选器是堆栈中最上面的筛选器(类筛选器),此方法就会起作用,因为该筛选器首先处理请求。如果筛选器驱动程序位于堆栈中的其他任何位置,则 IOCTL 请求可能被其他筛选器或它上面的功能驱动程序拒绝。筛选器驱动程序拒绝它不知道的请求是不适当的;但是,功能驱动程序可能会这样做。因此,如果筛选器驱动程序是一个低层筛选器,功能驱动程序肯定会拒绝不知道的所有 IOCTL 请求。

避免此问题的唯一方法是创建另一个名为 controlobject 的独立控件和一个符号链接(如通常对 Microsoft Windows NT 4.0 驱动程序所做的那样),而不是在 PDO 上注册接口。应用程序可以打开符号链接并将 IOCTL 发送到筛选器。这些 I/O 请求会被直接发送到控件 deviceobject,而不是遍历整个堆栈,不管筛选器驱动程序位于堆栈中的什么位置。

以下代码显示如何为通用 toaster 筛选器示例 (Filter.c) 提供自定义 IOCTL 支持,该示例位于 Windows 2000 和 Windows XP DDK 中,在 <DDK 根目录>/Src/General/Toaster/Filter/ 下。提供 IOCTL 支持所需的所有代码都位于条件编译指令内。您必须在源文件中定义 IOCTL_INTERFACE(-DIOCTL_INTERFACE=1),以包括 IOCTL 接口支持。

与在 Windows 2000 DDK 中提供的示例相比,除了为演示 IOCTL 接口而添加的代码之外,在代码中还有一些增强和错误更正。
#include <ntddk.h>
#include "filter.h"

#ifdef ALLOC_PRAGMA
#pragma alloc_text (INIT, DriverEntry)
#pragma alloc_text (PAGE, FilterAddDevice)

#pragma alloc_text (PAGE, FilterDispatchPnp)
#pragma alloc_text (PAGE, FilterUnload)
#endif

// 
// Define IOCTL_INTERFACE in your sources file if  you want your
// app to have private interaction with the filter driver.Read KB Q262305
// for more information.
// 

#ifdef IOCTL_INTERFACE

#ifdef ALLOC_PRAGMA
#pragma alloc_text (PAGE, FilterCreateControlObject)
#pragma alloc_text (PAGE, FilterDeleteControlObject)
#pragma alloc_text (PAGE, FilterDispatchIo)
#endif
FAST_MUTEX ControlMutex;
ULONG InstanceCount = 0;
PDEVICE_OBJECT ControlDeviceObject;

#endif

NTSTATUS
DriverEntry(
IN PDRIVER_OBJECT  DriverObject,
IN PUNICODE_STRING RegistryPath
    )
/*++

例程说明:

可安装的驱动程序初始化入口点。
此入口点由 I/O 系统直接调用。

参数:

DriverObject - 指向驱动程序对象的指针

RegistryPath - 指向表示路径的 unicode 字符串,该路径是
注册表中驱动程序特定的键的路径。

返回值:

STATUS_SUCCESS if successful,
STATUS_UNSUCCESSFUL otherwise.

--*/ 
{
NTSTATUS            status = STATUS_SUCCESS;
ULONG               ulIndex;
PDRIVER_DISPATCH  * dispatch;

UNREFERENCED_PARAMETER (RegistryPath);

DebugPrint (("Entered the Driver Entry/n"));


    // 
// Create dispatch points
    // 
for (ulIndex = 0, dispatch = DriverObject->MajorFunction;
ulIndex <= IRP_MJ_MAXIMUM_FUNCTION;
ulIndex++, dispatch++) {

*dispatch = FilterPass;
    }

DriverObject->MajorFunction[IRP_MJ_PNP]            = FilterDispatchPnp;
DriverObject->MajorFunction[IRP_MJ_POWER]          = FilterDispatchPower;
DriverObject->DriverExtension->AddDevice           = FilterAddDevice;
DriverObject->DriverUnload                         = FilterUnload;

#ifdef IOCTL_INTERFACE
    // 
// Set the following dispatch points as we will be doing
// something useful to these requests instead of just
// passing them down. 
    // 
    
DriverObject->MajorFunction[IRP_MJ_CREATE]     = 
DriverObject->MajorFunction[IRP_MJ_CLOSE]      = 
DriverObject->MajorFunction[IRP_MJ_CLEANUP]    = 
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = 
FilterDispatchIo;
    // 
// Mutex is to synchronize multiple threads creating & deleting 
// control deviceobjects.
    // 
ExInitializeFastMutex (&ControlMutex);
    
#endif

return status;
}


NTSTATUS
FilterAddDevice(
IN PDRIVER_OBJECT  DriverObject,
IN PDEVICE_OBJECT PhysicalDeviceObject
    )
/*++

例程说明:

"即插即用"子系统为我们提供了一个全新的 PDO,我们要
(通过 INF 注册的方法)为其提供驱动程序。

我们需要确定是否需要处于该设备的驱动程序堆栈中。
创建功能设备对象以连接到堆栈
初始化该设备对象
返回状态成功。

请记住:实际上我们不能将任何非 pnp IRPS 发送到给定的驱动程序
堆栈,除非已经接收到了 IRP_MN_START_DEVICE。

参数:

DeviceObject - 指向设备对象的指针。

PhysicalDeviceObject - 指向
最下层总线驱动程序创建的设备对象的指针。

返回值:

NT status code.

--*/ 
{
NTSTATUS            status = STATUS_SUCCESS;
PDEVICE_OBJECT          deviceObject = NULL;

PDEVICE_EXTENSION       deviceExtension;
ULONG                   deviceType = FILE_DEVICE_UNKNOWN;

PAGED_CODE ();


    // 
// IoIsWdmVersionAvailable(1, 0x20) returns TRUE on os after Windows 2000.
    // 
if (!IoIsWdmVersionAvailable(1, 0x20)) {
        // 
// Win2K system bugchecks if the filter attached to a storage device
// doesn't specify the same DeviceType as the device it's attaching
// to.This bugcheck happens in the filesystem when you disable
// the devicestack whose top level deviceobject doesn't have a VPB.
// To workaround we will get the toplevel object's DeviceType and
// specify that in IoCreateDevice.
        // 
deviceObject = IoGetAttachedDeviceReference(PhysicalDeviceObject);
deviceType = deviceObject->DeviceType;
ObDereferenceObject(deviceObject);
    }

    // 
// Create a filter device object.
    // 

status = IoCreateDevice (DriverObject,
sizeof (DEVICE_EXTENSION),
NULL,  // No Name
deviceType,
FILE_DEVICE_SECURE_OPEN,
FALSE,
&deviceObject);


if (!NT_SUCCESS (status)) {
        // 
// Returning failure here prevents the entire stack from functioning,
// but most likely the rest of the stack will not be able to create
// device objects either, so it is still OK.
        // 
return status;
    }

DebugPrint (("AddDevice PDO (0x%x) FDO (0x%x)/n",
PhysicalDeviceObject, deviceObject));

deviceExtension = (PDEVICE_EXTENSION) deviceObject->DeviceExtension;


deviceExtension->NextLowerDriver = IoAttachDeviceToDeviceStack (
deviceObject,
PhysicalDeviceObject);
    // 
// Failure for attachment is an indication of a broken plug & play system.
    // 

if(NULL == deviceExtension->NextLowerDriver) {

IoDeleteDevice(deviceObject);
return STATUS_UNSUCCESSFUL;
    }

deviceObject->Flags |= deviceExtension->NextLowerDriver->Flags &
(DO_BUFFERED_IO | DO_DIRECT_IO |
DO_POWER_PAGABLE );


deviceObject->DeviceType = deviceExtension->NextLowerDriver->DeviceType;

deviceObject->Characteristics =
deviceExtension->NextLowerDriver->Characteristics;

deviceExtension->Self = deviceObject;

    // 
// Set the initial state of the Filter DO
    // 

INITIALIZE_PNP_STATE(deviceExtension);

DebugPrint(("AddDevice:%x to %x->%x /n", deviceObject,
deviceExtension->NextLowerDriver,
PhysicalDeviceObject));

deviceObject->Flags &= ~DO_DEVICE_INITIALIZING;

return STATUS_SUCCESS;

}

NTSTATUS
FilterPass (
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp
    )
/*++

例程说明:

默认的调度例程。如果此驱动程序不识别
IRP,那么它应该将它不经修改就向下发送。
如果该设备正在处理其他一些 IRP,此 IRP 必须在设备扩展中排队
不需要完成例程。

只是为了进行演示,我们才会将所有(非"即插即用")Irp 向下传递
到堆栈上(因为我们是筛选器驱动程序)。实际的驱动程序可能会选择
为这些 Irp 中的一部分提供服务。

由于我们不清楚正在传递哪个函数,所以,我们不能假定自己是否在比较高的 IRQL 上被调用。
因此,此函数必须置于非分页的池
(也称默认位置)中。

参数:

DeviceObject - 指向设备对象的指针。

Irp - 指向 I/O 请求数据包的指针。

返回值:

NT 状态代码

--*/ 
{
PDEVICE_EXTENSION       deviceExtension;

deviceExtension = (PDEVICE_EXTENSION) DeviceObject->DeviceExtension;

IoSkipCurrentIrpStackLocation (Irp);
return IoCallDriver (deviceExtension->NextLowerDriver, Irp);
}

NTSTATUS
FilterDispatchPnp (
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp
    )
/*++

例程说明:

"即插即用"调度例程。

这些驱动程序中,多数都将会完全忽略。
在所有情况下,它都必须把 IRP 传递到较低级别的驱动程序。

参数:

DeviceObject - 指向设备对象的指针。

Irp - 指向 I/O 请求数据包的指针。

返回值:

NT 状态代码

--*/ 
{
PDEVICE_EXTENSION       deviceExtension;
PIO_STACK_LOCATION          irpStack;
NTSTATUS                    status;

PAGED_CODE ();

deviceExtension = (PDEVICE_EXTENSION) DeviceObject->DeviceExtension;
irpStack = IoGetCurrentIrpStackLocation(Irp);

DebugPrint(("FilterDO %s IRP:0x%x /n",
PnPMinorFunctionString(irpStack->MinorFunction), Irp));

switch (irpStack->MinorFunction) {
case IRP_MN_START_DEVICE:

        // 
// The device is starting.
        // 
// We cannot touch the device (send it any non pnp irps) until a
// start device has been passed down to the lower drivers.
        // 
IoCopyCurrentIrpStackLocationToNext(Irp);
IoSetCompletionRoutine(Irp,
(PIO_COMPLETION_ROUTINE) FilterStartCompletionRoutine,
NULL, 
TRUE,
TRUE,
TRUE);

return IoCallDriver (deviceExtension->NextLowerDriver, Irp);

case IRP_MN_REMOVE_DEVICE:

IoSkipCurrentIrpStackLocation (Irp);

status = IoCallDriver(deviceExtension->NextLowerDriver, Irp);

SET_NEW_PNP_STATE(deviceExtension, Deleted);
        
#ifdef IOCTL_INTERFACE
FilterDeleteControlObject();
#endif
IoDetachDevice(deviceExtension->NextLowerDriver);
IoDeleteDevice(DeviceObject);
return status;


case IRP_MN_QUERY_STOP_DEVICE:
SET_NEW_PNP_STATE(deviceExtension, StopPending);
status = STATUS_SUCCESS;
break;

case IRP_MN_CANCEL_STOP_DEVICE:

        // 
// Check to see whether you have received cancel-stop
// without first receiving a query-stop.This could happen if someone
// above us fails a query-stop and passes down the subsequent
// cancel-stop.
        // 

if(StopPending == deviceExtension->DevicePnPState)
        {
            // 
// We did receive a query-stop, so restore.
            // 
RESTORE_PREVIOUS_PNP_STATE(deviceExtension);
        }
status = STATUS_SUCCESS; // We must not fail this IRP.
break;

case IRP_MN_STOP_DEVICE:
SET_NEW_PNP_STATE(deviceExtension, Stopped);
status = STATUS_SUCCESS;
break;

case IRP_MN_QUERY_REMOVE_DEVICE:

SET_NEW_PNP_STATE(deviceExtension, RemovePending);
status = STATUS_SUCCESS;
break;

case IRP_MN_SURPRISE_REMOVAL:

SET_NEW_PNP_STATE(deviceExtension, SurpriseRemovePending);
status = STATUS_SUCCESS;
break;

case IRP_MN_CANCEL_REMOVE_DEVICE:

        // 
// Check to see whether you have received cancel-remove
// without first receiving a query-remove.This could happen if
// someone above us fails a query-remove and passes down the
// subsequent cancel-remove.
        // 

if(RemovePending == deviceExtension->DevicePnPState)
        {
            // 
// We did receive a query-remove, so restore.
            // 
RESTORE_PREVIOUS_PNP_STATE(deviceExtension);
        }

status = STATUS_SUCCESS; // We must not fail this IRP.
break;

case IRP_MN_DEVICE_USAGE_NOTIFICATION:

        // 
// On the way down, pagable might become set.Mimic the driver
// above us.If no one is above us, just set pagable.
        // 
if ((DeviceObject->AttachedDevice == NULL) ||
(DeviceObject->AttachedDevice->Flags & DO_POWER_PAGABLE)) {

DeviceObject->Flags |= DO_POWER_PAGABLE;
        }

IoCopyCurrentIrpStackLocationToNext(Irp);

IoSetCompletionRoutine(
Irp,
FilterDeviceUsageNotificationCompletionRoutine,
NULL, 
TRUE,
TRUE,
TRUE
            );

return IoCallDriver (deviceExtension->NextLowerDriver, Irp);

default:
        // 
// If you don't handle any IRP you must leave the
// status as is.
        // 
status = Irp->IoStatus.Status;

break;
    }

    // 
// Pass the IRP down and forget it.
    // 
Irp->IoStatus.Status = status;
return FilterPass(DeviceObject, Irp);
}

NTSTATUS
FilterStartCompletionRoutine(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP             Irp,
IN PVOID            Context
    )
/*++
例程说明:
一个完成例程,在调用我们的筛选器设备对象所附加的低层设备对象时
使用该例程。

参数:

DeviceObject - 指向 deviceobject 的指针
Irp          - 指向 PnP Irp 的指针。
上下文      - NULL
返回值:

返回 NT 状态。

--*/ 

{
PDEVICE_EXTENSION       deviceExtension;

UNREFERENCED_PARAMETER(Context);

deviceExtension = (PDEVICE_EXTENSION) DeviceObject->DeviceExtension;

if (Irp->PendingReturned) {

IoMarkIrpPending(Irp);
    }

if (NT_SUCCESS (Irp->IoStatus.Status)) {
 
        // 
// As we are successfully now back, we will
// first set our state to Started.

        // 

SET_NEW_PNP_STATE(deviceExtension, Started);

        // 
// On the way up inherit FILE_REMOVABLE_MEDIA during Start.
// This characteristic is available only after the driver stack is started!.
        // 
if (deviceExtension->NextLowerDriver->Characteristics & FILE_REMOVABLE_MEDIA) {

DeviceObject->Characteristics |= FILE_REMOVABLE_MEDIA;
        }

#ifdef IOCTL_INTERFACE

        // 
// If the PreviousPnPState is stopped then we are being stopped temporarily
// and restarted for resource rebalance. 
        // 
if(Stopped != deviceExtension->PreviousPnPState) {
            // 
// Device is started for the first time.
            // 
FilterCreateControlObject(DeviceObject);
        }
#endif

    }

return STATUS_SUCCESS;

}

NTSTATUS
FilterDeviceUsageNotificationCompletionRoutine(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP             Irp,
IN PVOID            Context
    )
/*++
例程说明:
一个完成例程,在调用我们的筛选器 deviceobject 所附加的低层设备对象时
使用该例程。

参数:

DeviceObject - 指向 deviceobject 的指针
Irp          - 指向 PnP Irp 的指针。
上下文      - NULL
返回值:

返回 NT 状态。

--*/ 

{
PDEVICE_EXTENSION       deviceExtension;

UNREFERENCED_PARAMETER(Context);

deviceExtension = (PDEVICE_EXTENSION) DeviceObject->DeviceExtension;


if (Irp->PendingReturned) {

IoMarkIrpPending(Irp);
    }

    // 
// On the way up, pagable might become clear.Mimic the driver below us.
    // 
if (!(deviceExtension->NextLowerDriver->Flags & DO_POWER_PAGABLE)) {

DeviceObject->Flags &= ~DO_POWER_PAGABLE;
    }

return STATUS_SUCCESS;

}

NTSTATUS
FilterDispatchPower(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp
    )
/*++

例程说明:

此例程是 power irp 的调度例程。

参数:

DeviceObject - 指向设备对象的指针。

Irp - 指向请求数据包的指针。

返回值:

NT 状态代码
--*/ 
{
PDEVICE_EXTENSION       deviceExtension;

deviceExtension = (PDEVICE_EXTENSION) DeviceObject->DeviceExtension;
PoStartNextPowerIrp(Irp);
IoSkipCurrentIrpStackLocation (Irp);
return PoCallDriver(deviceExtension->NextLowerDriver, Irp);
}



VOID
FilterUnload(
IN PDRIVER_OBJECT DriverObject
    )
/*++

例程说明:

释放 DriverEntry 等中所有已分配的资源。

参数:

DriverObject - 指向驱动程序对象的指针。

返回值:

VOID.

--*/ 
{
PAGED_CODE ();

    // 
// The device object(s) should be NULL now
// (since we unload, all the devices objects associated with this
// driver must be deleted.
    // 
ASSERT(DriverObject->DeviceObject == NULL);

    // 
// We should not be unloaded until all the devices we control
// have been removed from our queue.
    // 
DebugPrint (("unload/n"));

return;
}

#ifdef IOCTL_INTERFACE
NTSTATUS
FilterCreateControlObject(
IN PDEVICE_OBJECT    DeviceObject
)
{
UNICODE_STRING      ntDeviceName;
UNICODE_STRING      symbolicLinkName;
PCONTROL_DEVICE_EXTENSION   deviceExtension;
NTSTATUS                    status;
    
ExAcquireFastMutex (&ControlMutex);

    // 
// If this is a first instance of the device, then create a controlobject
// and register dispatch points to handle ioctls.
    // 
if(1 == ++InstanceCount)
    {

        // 
// Initialize the unicode strings
        // 
RtlInitUnicodeString(&ntDeviceName, NTDEVICE_NAME_STRING);
RtlInitUnicodeString(&symbolicLinkName, SYMBOLIC_NAME_STRING);

        // 
// Create a named deviceobject so that applications or drivers
// can directly talk to us without going throuhg the entire stack.
// This call could fail if there are not enough resources or
// another deviceobject of same name exists (name collision).
        // 
        
status = IoCreateDevice(DeviceObject->DriverObject,
sizeof(CONTROL_DEVICE_EXTENSION),
&ntDeviceName,
FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN,
FALSE,
&ControlDeviceObject);

if (NT_SUCCESS( status )) {

ControlDeviceObject->Flags |= DO_BUFFERED_IO;

status = IoCreateSymbolicLink( &symbolicLinkName, &ntDeviceName );

if (!NT_SUCCESS (status)) {
IoDeleteDevice(ControlDeviceObject);
DebugPrint(("IoCreateSymbolicLink failed %x/n", status));
goto End;
            }

deviceExtension = ControlDeviceObject->DeviceExtension;
deviceExtension->ControlData = NULL;
            
ControlDeviceObject->Flags &= ~DO_DEVICE_INITIALIZING;
            
}else {
DebugPrint(("IoCreateDevice failed %x/n", status));
        }
    }

End:
    
ExReleaseFastMutex (&ControlMutex); 
return status;
    
}

VOID
FilterDeleteControlObject(
)
{
UNICODE_STRING      symbolicLinkName;

ExAcquireFastMutex (&ControlMutex);

    // 
// If this is the last instance of the device then delete the controlobject
// and symbolic link to enable the pnp manager to unload the driver.
    // 
    
if(!(--InstanceCount) && ControlDeviceObject)
    {
RtlInitUnicodeString(&symbolicLinkName, SYMBOLIC_NAME_STRING);

IoDeleteSymbolicLink(&symbolicLinkName);
IoDeleteDevice(ControlDeviceObject);
ControlDeviceObject= NULL;
    }

ExReleaseFastMutex (&ControlMutex); 

}


NTSTATUS
FilterDispatchIo(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp
    )
/*++

例程说明:

此例程是非 passthru irp 的调度例程。
我们将检查输入设备对象以了解请求是否
要用于控制设备对象。如果是,我们将
处理并完成 IRP,如果不是,我们会将它向下传递到
较低级别的驱动程序。
    
参数:

DeviceObject - 指向设备对象的指针。

Irp - 指向请求数据包的指针。

返回值:

NT 状态代码
--*/ 
{
PIO_STACK_LOCATION          irpStack;
NTSTATUS                    status;

PAGED_CODE ();

    // 
// Please note that this is a common dispatch point for controlobject and
// filter deviceobject attached to the pnp stack. 
    // 
if(DeviceObject != ControlDeviceObject) {
        // 
// We will just  the request down as we are not interested in handling
// requests that come on the PnP stack.
        // 
return FilterPass(DeviceObject, Irp);
    }
    
    // 
// Else this is targeted at our control deviceobject so let's handle it.
// Here we will handle the IOCTl requests that come from the app.
    //    
status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
irpStack = IoGetCurrentIrpStackLocation(Irp);

switch (irpStack->MajorFunction) {
case IRP_MJ_CREATE:
DebugPrint(("Create /n"));
break;
            
case IRP_MJ_CLOSE:
DebugPrint(("Close /n"));
break;
            
case IRP_MJ_CLEANUP:
DebugPrint(("Cleanup /n"));
break;
            
case  IRP_MJ_DEVICE_CONTROL:
DebugPrint(("DeviceIoControl/n"));
switch (irpStack->Parameters.DeviceIoControl.IoControlCode) {
                // 
//case IOCTL_CUSTOM_CODE: 
                // 
default:
status = STATUS_INVALID_PARAMETER;
break;
            }
default:
break;
    }
 
Irp->IoStatus.Status = status;
IoCompleteRequest (Irp, IO_NO_INCREMENT);
return status;
}

#endif 

对于附加了筛选器的设备的所有实例,该控件设备对象是公用的。因此,当启动设备的第一个实例时,会创建它 (FilterCreateControlObject);当删除设备的最后一个实例时,会删除它 (FilterDeleteControlObject)。如果在移除设备的最后一个实例之前没有删除控件对象,则控件对象将阻止筛选器动态卸载。此示例使用了一个互斥体,还使用一个全局计数器来跟踪实例。之所以将互斥体用于同步而不是使用自旋锁,是因为访问全局计数器的所有例程都在 IRQL PASSIVE_LEVEL 上运行。 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值