3.4 IRP处理

简要介绍IRP的生成、创建、发送等,然后介绍多种IRP处理示例。


3.4.1 简单的IRP流动图

应用程序打开磁盘上一个数据文件的基本流程:

1. Windows子系统使用系统服务函数NtCreateFile打开目标文件

2. I/O管理器调用对象管理器解析命名的文件,然后调用安全引用监视器检查子系统是否拥有对目标文件对象的访问权限

3. 如果目标文件所在的卷还没有被加载,I/O管理器将临时性地将此次打开请求挂起,然后调用文件系统识别器识别目标文件系统并加载,然后I/O管理器将恢复打开请求

4. I/O管理器为此次的文件打开请求创建并初始化一个IRP。对于驱动程序而言,文件打开请求就是Create操作。

5. I/O管理器将此IRP传递给目标文件系统。文件系统驱动程序访问IRP的I/O栈单元并根据访问到的信息判断需要实习的操作;检查参数信息;判断请求的文件是否在缓存中,如果不在,则为此IRP建立下层I/O栈单元。

6. 下层驱动程序使用I/O管理器及其他组件提供的支持函数完成此次请求操作。

7. 驱动程序将IRP返回给I/O管理器。通过IRP的I/O状态块指示此次操作是成功完成还是失败。

8. I/O管理器从IRP中获取I/O状态,然后将状态信息复制给原始的调用者。

9. I/O管理器删除此IRP

10.如果此次操作成功完成,I/O管理器将返回一个句柄给原始的调用者程序;否则,将返回相关失败信息。

IRP本质是一个数据结构,其作用是统一格式,封装各种I/O请求,并记录操作状态。


3.4.2 IRP的创建

IRP创建函数IoAllocateIRP,创建后的IRP只是一个空的“容器”,必须初始化才能使用。初始化工作主要包括两个部分:IRP头部分和IRP栈单元部分。

3.4.2.1 初始化IRP头部分

IRP头部分需要初始化的域包括MdlAddress、Flags、AssocaitedIrp.SystemBuffer、RequestMode、UserBuffer和Tail.Overlay.Thread

3.4.2.2 初始IRP栈单元部分

IRP栈单元需要初始的域包括MajorFunction、MiniorFunction、DeviceObject、FileObject等

例子:

NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
    NTSTATUS status;
    HANDLE hFile;
    PFILE_OBJECT pFileObj = NULL;
    UNICODE_STRING uFileName;
    PIRP pIrp = NULL;
    PDRIVER_OBJECT pFSDDevice = NULL;
    KEVENT kEvent;
    LARGE_INTEGER liOffset;
    PVOID pBuf = NULL;
    PMDL pMdl = NULL;
    IO_STATUS_BLOCK  ioStatus;
    PIO_STACK_LOCATION pSP = NULL;


    DriverObject->DriverUnload = ReadFileUnload;


    do 
    {
        //设置目标文件
        RtlInitUnicodeString(&uFileName,L"\\Device\\HarddiskVolume1\\test.txt");


        //获取目标文件的句柄,该函数实际是对IoCreateFile函数的封装
        hFile = KGetTargetFileHandle(&uFileName);
        if (INVALID_HANDLE_VALUE == hFile)
        {
            KdPrint(("[DriverEntry] KGetTargetFileHandle Failure!\n"));
            break;
        }


        //获取目标文件的对象地址
        status = ObReferenceObjectByHandle(hFile,GENERIC_READ,*IoFileObjectType,KernelMode,&pFileObj,NULL);
        if (!NT_SUCCESS(status))
        {
            KdPrint(("[DriverEntry] ObReferenceObjectByHandle Failure!err:0x%08x\n",status));
            ZwClose(hFile);
            break;
        }


        //获取目标文件关联的设备对象
        pFSDDevice = IoGetRelatedDeviceObject(pFileObj);
        KdPrint(("pFSDDevice:0x%08x\n",pFSDDevice));


        //分配一个IRP
        pIrp = IoAllocateIrp(pFSDDevice->StackSize,FALSE);
        if (NULL == pIrp)
        {
            KdPrint(("[DriverEntry] IoAllocateIrp Failure!\n"));
            break;
        }


        //初始一个事件对象,用于同步完成该IRP
        KeInitializeEvent(&kEvent,SynchronizationEvent,FALSE);


        //分配缓存,用于记录读信息
        pBuf = ExAllocatePoolWithTag(NonPagedPool,1024,'wjl');
        if (NULL == pBuf)
        {
            KdPrint(("[DriverEntry] ExAllocatePoolWithTag Failure!\n"));
            IoFreeIrp(pIrp);
            break;
        }


        //填写IRP头
        pIrp->UserBuffer = pBuf; //注意下层驱动程序的读写请求I/O方式为Neither
        pIrp->UserEvent = &kEvent;
        pIrp->UserIosb = &ioStatus;
        pIrp->Tail.Overlay.Thread = PsGetCurrentThread();
        pIrp->Tail.Overlay.OriginalFileObject = pFileObj;
        pIrp->RequestorMode = KernelMode;
        pIrp->Flags = IRP_READ_OPERATION;


        //为下层驱动程序填写栈单元信息
        pSP = IoGetNextIrpStackLocation(pIrp);


        pSP->MajorFunction = IRP_MJ_READ;
        pSP->MinorFunction = 0;
        pSP->DeviceObject = pFSDDevice;
        pSP->FileObject = pFileObj;


        //设置完成例程,用于获取读到的信息及释放资源
        IoSetCompletionRoutine(pIrp,ReadIoCompletion,0,TRUE,TRUE,TRUE);


        //设置读请求参数
        liOffset.QuadPart = 0;
        pSP->Parameters.Read.Length = 10;
        pSP->Parameters.Read.ByteOffset = liOffset;


        //下发该IRP
        IoCallDriver(pFSDDevice,pIrp);


        //同步等待该IRP
        KeWaitForSingleObject(&kEvent,Executive,KernelMode,TRUE,0);
        KdPrint(("[DriverEntry] Read Finish!\n"));


        //关闭该文件
        ObDereferenceObject(pFileObj);


        ZwClose(hFile);


    } while (FALSE);


    return STATUS_SUCCESS;
}


NTSTATUS ReadIoCompletion(PDEVICE_OBJECT DeviceObject, PIRP Irp, PVOID Context)
{
    PVOID pBuf;


    *Irp->UserIosb = Irp->IoStatus;


    KeSetEvent(Irp->UserEvent,0,FALSE);


    pBuf = Irp->UserBuffer;


    KdPrint(("[ReadIoCompletion] status:0x08x information:%d\n",
        Irp->IoStatus.Status, Irp->IoStatus.Information));


    KdPrint(("[ReadIoCompletion] Read Buffer:0x08x %s\n",pBuf,pBuf));


    ExFreePoolWithTag(pBuf,'wjl');


    IoFreeIrp(Irp);


    return STATUS_MORE_PROCESSING_REQUIRED;
}

上述代码分配IRP,填充了必要的参数后,下发到目标设备驱动。


3.4.3 IRP的发送

下发IRP的典型过程如下所示:

1. 为下层驱动程序安装I/O栈单元

复用:调用IoSkipCurrentIrpStackLocation函数

复制:调用IoCopyCurrentIrpStackLocatioToNext函数为下层驱动程序安装一个I/O栈单元,然后调用IoSetCompletionRoutine设置完成函数

手动安装:由I/O管理器完成

2. 设置完成函数

3. 调用IoCallDriver发送IRP

4. 返回状态值


3.4.4 为IRP设置完成函数

1. I/O栈单元介绍

I/O管理器为每一个IRP创建了一组I/O栈单元,I/O栈单元对应的数据结构是IO_STACK_LOCATION。

每个驱动程序都拥有一个I/O栈单元,通过调用IoGetCurrentIrpStackLocation函数可以获取对于的I/O栈单元指针。每个驱动程序都需要直接或间接调用IoGetNextIrpStack-Location(复用I/O栈单元除外)为下层驱动程序安装I/O栈单元,并可能设置一个完成函数。即本层驱动程序的I/O完成函数是被设置在下层驱动程序的I/O栈单元中的。

2. 使用IoSetCompletionRoutine设置完成函数

完成函数可以帮忙驱动程序重新获取执行控制,从而有能力访问甚至修改IRP的完成结果。

3. 自定义设置完成函数

在本层驱动程序对对应的I/O栈单元为本层驱动程序设置完成函数

4. 完成函数介绍

完成函数运行在DISPATCH_LEVEL,因此完成函数不能访问分页文件。

典型的IRP完成函数

NTSTATS CompletionRoutine_1 (IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp, IN PVOID Context)

{if (Irp->PendingReturned) {IoMarkIrpPending(Irp);} return STATUS_CONTINUE_COMPLETION};

NTSTATS CompletionRoutine_2 (IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp, IN PVOID Contect)

{if (Irp->PendingReturned = TRUE) {KeSetEvent((PKEVENT Contect, IO_NO_INCREMENT,FALSE);} return STATUS_MORE_PROCESSING_REQUIRED;}


3.4.5 IRP的完成

以下情况驱动程序应该完成IRP:

驱动程序检测到IRP中携带非法参数、驱动程序不需要下发IRP直接在分发函数中完成该请求、IRP被取消

完成IRP分为两个阶段:

 驱动程序完成IRP:驱动程序调用IoCompleteRequest函数,逐层调用各级驱动程序注册的完成函数

I/O管理器完成IRP:将以完成的IRP信息复制给应用程序

IRP完成分为同步完成和异步完成。驱动程序提供同步或异步支持,I/O管理器根据应用程序的标记决定IRP最终被同步或异步完成。

IRP的同步完成:指的是应用程序或者上层驱动程序发送请求后,I/O管理器一直等待该IRP被最终完成。

IRP的异步完成:指的是应用程序或上层驱动程序发送请求后,I/O管理器立即返回,返回结果可能是未决(Pending),也可能是成功。如果驱动程序提供的是异步支持,I/O管理器将立即返回未决状态,当驱动程序完成IRP时,I/O管理器将会投递一个APC到目标线程中,并通知应用程序;如果驱动程序提供的是同步支持,那么该请求将会被立即完成,I/O管理器也立即返回所有信息给应用程序,请求结束。


IRP的同步完成:

IRP的同步完成指的是应用程序或者上层驱动程序发送请求后,I/O管理器一直等待该IRP被最终完成。

//
//
//
#include <ntddk.h>

NTSTATUS SyncRead(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
    NTSTATUS status = STATUS_SUCCESS;
    PIO_STACK_LOCATION pSP = IoGetCurrentIrpStackLocation(Irp);
    PVOID pBuf = NULL;
    ULONG uBuf = 0;


    KdPrint(("[SyncRead]\n"));


    do 
    {
        //读写请求使用的是直接I/O方式
        pBuf = MmGetSystemAddressForMdl(Irp->MdlAddress);
        if (NULL == pBuf)
        {
            status = STATUS_UNSUCCESSFUL;
            break;
        }


        uBuf = pSP->Parameters.Read.Length;


        KdPrint(("Read Len: %d\n",uBuf));


        //最大支持5字节请求
        uBuf = uBuf>=5?5:uBuf;


        //简单地向读请求缓冲区中写入"hello"
        RtlCopyMemory(pBuf,"hello",uBuf);
    } while (FALSE);


    //填写返回状态及返回大小
    Irp->IoStatus.Status = status;
    Irp->IoStatus.Information = uBuf;


    //完成该IRP
    IoCompleteRequest(Irp,IO_NO_INCREMENT);


    return status;
}

应用程序同步读请求操作相关代码如下:

int main()
{
    HANDLE hFile;
    char Buf[10] = {0};
    DWORD dwRet = 0;
    BOOL bRet;


    hFile = CreateFileA("\\\\.\\Sync0",GENERIC_READ,FILE_SHARE_READ,
        NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
    if (INVALID_HANDLE_VALUE == hFile)
    {
        return 0;
    }


    bRet = ReadFile(hFile,Buf,10,&dwRet,NULL)
    if (!bRet)
    {
        CloseHandle(hFile);
        return 0;
    }


    printf("SyncUser--Read Buf: %s Read Len: %d\n",Buf,dwRet);


    return 0;
}


IRP的异步完成:

//
//
//
#include <ntddk.h>




//设备自定义扩展
typedef struct _Device_Extension_
{
    LIST_ENTRY IrpList;


    KTIMER timer;
    LARGE_INTEGER liDueTime;
    KDPC dpc;


}DevExt;




NTSTATUS AsyncRead(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
    NTSTATUS status;
    PIO_STACK_LOCATION pSP = IoGetCurrentIrpStackLocation(Irp);
    DevExt *pDevExt = (DevExt*)DeviceObject->DeviceExtension;


    //设置IRP为Pending,将在以后完成该IRP
    IoMarkIrpPending(Irp);


    //将IRP插入自定义链表中
    InsertTailList(&pDevExt->IrpList,&Irp->Tail.Overlay.ListEntry);


    //返回Pending,注意返回给I/O管理器的值必须和IRP的Pending标志位一致
    //即调用IoMarkIrpPending和返回值要一致
    return STATUS_PENDING;
}




NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
    UNICODE_STRING DeviceName,Win32Device;
    PDEVICE_OBJECT DeviceObject = NULL;
    NTSTATUS status;
    unsigned i;
    DevExt *pDevExt = NULL;
    HANDLE hThread;
    OBJECT_ATTRIBUTES ObjectAttributes;
    CLIENT_ID CID;


    RtlInitUnicodeString(&DeviceName,L"\\Device\\Async0");
    RtlInitUnicodeString(&Win32Device,L"\\DosDevices\\Async0");


    for (i=0; i<=IRP_MJ_MAXIMUM_FUNCTION; i++)
    {
        DriverObject->MajorFunction[i] = AsyncDefaultHandler;
    }


    DriverObject->MajorFunction[IRP_MJ_CREATE] = AsyncCreateClose;
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = AsyncCreateClose;
    DriverObject->MajorFunction[IRP_MJ_READ] = AsyncRead;


    DriverObject->DriverUnload = AsyncUnload;


    //注意分配一个自定义扩展,大小为sizeof(DevExt)
    status = IoCreateDevice(DriverObject, sizeof(DevExt), &DeviceName,
                            FILE_DEVICE_UNKNOWN,0,FALSE,&DeviceObject);
    if (!NT_SUCCESS(status))
    {
        return status;
    }
    if (!DeviceObject)
    {
        return STATUS_UNEXPECTED_IO_ERROR;
    }


    DeviceObject->Flags |= DO_DIRECT_IO;
    DeviceObject->AlignmentRequirement = FILE_WORD_ALIGNMENT;
    status = IoCreateSymbolicLink(&Win32Device,&DeviceName);


    DeviceObject->Flags &= ~DO_DEVICE_INITIALIZING;


    pDevExt = (DevExt *)DeviceObject->DeviceExtension;


    //初始化Irp链表
    InitializeListHead(&pDevExt->IrpList);
    //初始化定时器
    KeInitializeTimer(&pDevExt->timer);
    //初始化DPC
    KeInitializeDpc(&pDevExt->dpc,(PKDEFERRED_ROUTINE)CustomDpc,pDevExt);


    //设置定时时间为3s
    pDevExt->liDueTime = RtlConvertLongToLargeInteger(-30000000);


    //启动定时器
    KeSetTimer(&pDevExt->timer,pDevExt->liDueTime,*pDevExt->dpc);status
    
    return STATUS_SUCCESS;
}




VOID CustomDpc(__in struct _KDPC *Dpc,
               __in_opt PVOID DeferredContext,
               __in_opt PVOID SystemArgument1,
               __in_opt PVOID SystemArgument2)
{
    PIRP pIrp;
    DevExt *pDevExt = (DevExt*)DeferredContext;
    PVOID pBuf = NULL;
    ULONG uBuf = 0;
    PIO_STACK_LOCATION pSP = NULL;


    KdPrint(("[CustomDpc]\n"));


    do 
    {
        if (!pDevExt)
        {
            KdPrint(("!pDevExt\n"));
            break;
        }


        //检查未决IRP链表是否为空
        if (IsListEmpty(&pDevExt->IrpList))
        {
            KdPrint(("IsListEmpty\n"));
            break;
        }


        //
        //从IRP链表中取出一个IRP并完成该IRP
        //
        KdPrint(("[CustomDpc] Dequeue one irp from IrpList and complete it!\n"));


        pIrp = (PIRP)RemoveHeadList(&pDevExt->IrpList);
        if (!pIrp)
        {
            break;
        }


        pIrp = (PIRP)CONTAINING_RECORD(pIrp,IRP,Tail.Overlay.LIST_ENTRY);
        pSP = IoGetCurrentIrpStackLocation(pIrp);


        KdPrint(("CustomDpc irp:0x%08x\n",pIrp));


        //驱动程序的读写I/O方式为直接I/O
        pBuf = MmGetSystemAddressForMdl(pIrp->MdlAddress);
        if (NULL == pBuf)
        {
            pIrp->IoStatus.Status = STATUS_UNSUCCESSFUL;
            pIrp->IoStatus.Information = 0;
            IoCompleteRequest(pIrp,IO_NO_INCREMENT);


            break;
        }


        uBuf = pSP->Parameters.Read.Length;


        KdPrint(("CustomDpc Read uBuf:%d\n",uBuf));


        //支持5字节以下的读请求
        uBuf = uBuf>5?5:uBuf


        //复制请求内容
        RtlCopyMemory(pBuf,"hello",uBuf);


        pIrp->IoStatus.Status = STATUS_SUCCESS;
        pIrp->IoStatus.Information = uBuf;


        //完成该IRP
        IoCompleteRequest(pIrp,IO_NO_INCREMENT);


        KdPrint(("[CustomDpc] finish complete!\n"));


    } while ();


    KdPrint(("Set Next Timer.\n"));


    //重新设置定时器
    KeSetTimer(&pDevExt->timer,pDevExt->liDueTime,&pDevExt->dpc);
}




int main()
{
    HANDLE hFile;
    char Buf[3][10] = {0};
    DWORD dwRet[3] = {0};
    OVERLAPPED ol[3] = {0};
    HANDLE hEvent[3] = {0};


    hFile = CreateFileA("\\\\.\\Async0",GENERIC_READ,FILE_SHARE_READ,
        NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL|FILE_FLAG_OVERLAPPED,NULL);
    if (INVALID_HANDLE_VALUE == hFile))
    {
        return 0;
    }


    hEvent[0] = CreateEvent(NULL,TRUE,FALSE,NULL);
    ol[0].hEvent = hEvent[0];


    hEvent[1] = CreateEvent(NULL,TRUE,FALSE,NULL);
    ol[1].hEvent = hEvent[1];


    hEvent[2] = CreateEvent(NULL,TRUE,FALSE,NULL);
    ol[2].hEvent = hEvent[2];


    ReadFile(HFILE,Buf[0],10,&dwRet[0],&ol[0]);
    ReadFile(HFILE,Buf[1],10,&dwRet[1],&ol[1]);
    ReadFile(HFILE,Buf[2],10,&dwRet[2],&ol[2]);


    //do some other work here


    WaitForMultipleObjects(3,hEvent,TRUE,INFINITE);


    printf("AsyncUser--Read Buf1:%s\n\
           Read Buf1:%s\n\
           Read Buf1:%s\n",
           Buf[0],Buf[1],Buf[2]);


    CloseHandle(hFile);
    
    return 0;
}



3.4.6 多种典型的IRP处理示例

//
//
//
#include <ntddk.h>




//1.仅下发
//
//如果驱动程序仅需要下发IRP而不需要额外的操作,那么驱动程序就没必要设置完成函数,连I/O栈单元也可以复用
//
NTSTATUS DispatchRoutine_1(
    IN PDEVICE_OBJECT DeviceObject,
    IN PIRP Irp)
{
    //
    //不需要设置完成函数,所以可以复用I/O栈单元
    //这样可以提高性能
    //
    IoSkipCurrentIrpStackLocation(Irp);
    return IoCallDriver(TopOfDeviceStack, Irp);
}




//2.下发并同步等待完成
//
//驱动程序下发IRP并需要等待其完成
//
NTSTATUS DispatchRoutine_2(
    IN PDEVICE_OBJECT DeviceObject,
    IN PIRP Irp)
{
    KEVENT event;
    NTSTATUS status;


    KeInitializeEvent(&event,NotificationEvent,FALSE);


    //
    //因为需要设置一个完成函数,所以必须要复制当前的I/O栈单元给下层驱动而不能复用I/O栈单元
    //
    IoCopyCurrentIrpStackLocationToNext(Irp);


    IoSetCompletionRoutine(Irp, CompletionRoutine_2, &event,
                           TRUE, TRUE, TRUE);


    status = IoCallDriver(TopOfDeviceStack, Irp);


    if (status == STATUS_PENDING)
    {
        KeWaitForSingleObject(&event, Executive, KernelMode,
                              FALSE, NULL);
        status = Irp->IoStatus.Status;
    }


    // <----Do your own work here.
    
    //
    //因为在完成函数中返回了STATUS_MORE_PROCESSING_REQUIRED
    //所以这里必须要调用IoCompleteRequest重新完成Irp
    //
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    return status;
}


NTSTATUS CompletionRoutine_2(
                             IN PDEVICE_OBJECT DeviceObject,
                             IN PIRP Irp,
                             IN PVOID Context)
{
    if (Irp->PendingReturned == TRUE)
    {
        //如果下层驱动程序返回未决,那么需要设置一下事件,因为分发函数还在等待该事件
        KeSetEvent((PKEVENT)Context, IO_NO_INCREMENT, FALSE);
    }


    //必须返回STSTUS_MORE_PROCESSING_REQUIRED将控制权交给驱动程序的分发函数
    return STATUS_MORE_PROCESSING_REQUIRED;
}


//3.下发IRP并设置一个完成函数
//
//驱动程序的分发函数设置一个完成函数,然后下发该IRP,最后返回IoCallDriver的返回值,使用完成函数的目的是可以修改IRP的内容
//
NTSTATUS DispatchRoutine_3(
                           IN PDEVICE_OBJECT Device_Object,
                           IN PIRP Irp)
{
    NTSTATUS ststus;


    IoCopyCurrentIrpStackLocationToNext(Irp);


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


    return IoCallDriver(TopOfDeviceStack, Irp);
}


NTSTATUS CompletionRoutine_3(
                             IN PDEVICE_OBJECT Device_Object,
                             IN PIRP Irp,
                             IN PVOID Context);
{
    //因为驱动的分发函数直接返回了IoCallDriver的返回值
    //所以这里必须要检测IRP的Pending位,以确保传播Pending位和
    //返回给I/O管理器的返回值的一致性
    if (Irp->PendingReturned)
    {
        IoMarkIrpPending(Irp);
    }


    return STATUS_CONTINUE_COMPLETION;
}




//4.排队IRP,在合适的情况下完成该IRP
//
NTSTATUS DispatchRoutine_4(
                           IN PDEVICE_OBJECT DeviceObject,
                           IN PIRP Irp)
{
    NTSTATUS ststus;


    //标记IRP为未决的
    IoMarkIrpPending(Irp);


    MyQueueIrp(Irp,DeviceObject);


    return STATUS_PENDING;
}




//5.立即完成Irp
NTSTATUS DispatchRoutine_5(
                           IN PDEVICE_OBJECT DeviceObject,
                           IN PIRP Irp)
{
    //
    // <-- Process the IRP here.
    //
    Irp->IoStatus.Status = STATUS_XXX;
    Irp->IoStatus.Information = YYY;
    IoCompleteRequest(Irp,IO_NO_INCREMENT);
    return STSTUS_XXX;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值