分层驱动程序

 作者:edwardlewiswe  来源:博客园  发布时间:2010-11-16

原文http://www.cnblogs.com/mydomain/archive/2010/11/16/1879041.html

 

把功能复杂的驱动分解成多个简单的驱动。多个分层驱动程序形成一个设备堆栈,IRP请求首先发送到设备堆栈的顶层,然后以次穿越每层的设备堆栈,最终完成IRP的请求。

1、相关概念

分层驱动是指两个或两个以上的驱动程序,他们分别创建设备对象,并且形成一个由高到低的设备对象栈。IRP请求一般送到设备栈最顶层的设备对象。顶层设备对象可以处理该IRP请求,也可以向下转发。IRP请求结束时,按原路返回。

1)_DEVICE_OBJECT结构体,需要注意3个参数:

DriverObject 设备对象对应的驱动对象

NextDevice 记录下一个设备对象的指针

AttachedDevice 记录当前设备对象挂载的设备对象。

WDM驱动程序就属于分层驱动程序。最简单的WDM驱动程序分为两层:一层是PDO(物理设备对象),一层是FDO(功能设备对象),FDO挂载在PDO之上。PDO实现即插即用的功能,FDO完成逻辑功能,而将一些硬件相关请求发往PDL。

挂载指的是将高一层的设备对象挂载在低一层的设备对象上,从而形成一个设备栈。

IoAttachDeviceToDeviceStack 挂载

IoDetachDevice 卸载

2)I/O 堆栈

用一个叫做IO_STACK_LOCATION的数据结构来保存;它和设备堆栈紧密结合。在IRP中,有一个指向IO_STACK_LOCATION数组的指针。调用IoAllocateIrp创建Irp时,有一个StackSize,就是IO_STACK_LOCATION数组的大小。

clip_image002

图示 P322

3)向下转发IRP

3种方法处理IRP:直接处理;调用StartIo,向下转发。

一个设备堆栈对应一个IO_STACK_LOCATION堆栈元素。IRP内部有一个指针指向当前正使用的IO_STACK_LOCATION。IoGetCurrentIrpStackLocation 获得。

每次调用IoCallDriver时,内核函数会将IRP的当前指针向下移一个单位。而IoSkipCurrentIrpStackLocation 用来将当前I/O堆栈往回(上)移一个单位。

When sending an IRP to the next-lower driver, your driver can call IoSkipCurrentIrpStackLocation if you do not intend to provide an IoCompletion routine (the address of which is stored in the driver's IO_STACK_LOCATION structure). If you call IoSkipCurrentIrpStackLocation before calling IoCallDriver, the next-lower driver receives the same IO_STACK_LOCATION that your driver received.

If you intend to provide an IoCompletion routine for the IRP, your driver should call IoCopyCurrentIrpStackLocationToNext instead of IoSkipCurrentIrpStackLocation.

 代码

1 /************************************************************************
2 * 函数名称:DriverEntry
3 * 功能描述:初始化驱动程序,定位和申请硬件资源,创建内核对象
4 * 参数列表:
5 pDriverObject:从I/O管理器中传进来的驱动对象
6 pRegistryPath:驱动程序在注册表的中的路径
7 * 返回 值:返回初始化驱动状态
8 *************************************************************************/
9  #pragma INITCODE
10  extern"C" NTSTATUS DriverEntry (
11 IN PDRIVER_OBJECT pDriverObject,
12 IN PUNICODE_STRING pRegistryPath )
13 {
14 NTSTATUS ntStatus;
15 KdPrint(("DriverB:Enter B DriverEntry\n"));
16
17 //注册其他驱动调用函数入口
18   pDriverObject->DriverUnload= HelloDDKUnload;
19 pDriverObject->MajorFunction[IRP_MJ_CREATE]= HelloDDKCreate;
20 pDriverObject->MajorFunction[IRP_MJ_CLOSE]= HelloDDKClose;
21 pDriverObject->MajorFunction[IRP_MJ_WRITE]= HelloDDKDispatchRoutine;
22 pDriverObject->MajorFunction[IRP_MJ_READ]= HelloDDKRead;
23
24 UNICODE_STRING DeviceName;
25 RtlInitUnicodeString(&DeviceName, L"\\Device\\MyDDKDeviceA" );
26
27 PDEVICE_OBJECT DeviceObject= NULL;
28 PFILE_OBJECT FileObject= NULL;
29 //寻找DriverA创建的设备对象
30   ntStatus= IoGetDeviceObjectPointer(&DeviceName,FILE_ALL_ACCESS,&FileObject,&DeviceObject);
31
32 if (!NT_SUCCESS(ntStatus))
33 {
34 KdPrint(("DriverB:IoGetDeviceObjectPointer() 0x%x\n", ntStatus ));
35 return ntStatus;
36 }
37
38 //创建自己的驱动设备对象
39   ntStatus= CreateDevice(pDriverObject);
40
41 if (!NT_SUCCESS( ntStatus ) )
42 {
43 ObDereferenceObject( FileObject );
44 DbgPrint("IoCreateDevice() 0x%x!\n", ntStatus );
45 return ntStatus;
46 }
47
48 PDEVICE_EXTENSION pdx= (PDEVICE_EXTENSION) pDriverObject->DeviceObject->DeviceExtension;
49
50 PDEVICE_OBJECT FilterDeviceObject= pdx->pDevice;
51
52 //将自己的设备对象挂载在DriverA的设备对象上
53 PDEVICE_OBJECT TargetDevice= IoAttachDeviceToDeviceStack( FilterDeviceObject,
54 DeviceObject );
55 //将底层设备对象记录下来
56 pdx->TargetDevice= TargetDevice;
57
58 if (!TargetDevice )
59 {
60 ObDereferenceObject( FileObject );
61 IoDeleteDevice( FilterDeviceObject );
62 DbgPrint("IoAttachDeviceToDeviceStack() 0x%x!\n", ntStatus );
63 return STATUS_INSUFFICIENT_RESOURCES;
64 }
65
66 FilterDeviceObject->DeviceType= TargetDevice->DeviceType;
67 FilterDeviceObject->Characteristics= TargetDevice->Characteristics;
68 FilterDeviceObject->Flags&=~DO_DEVICE_INITIALIZING;
69 FilterDeviceObject->Flags|= ( TargetDevice->Flags& ( DO_DIRECT_IO|
70 DO_BUFFERED_IO ) );
71 ObDereferenceObject( FileObject );
72
73 KdPrint(("DriverB:B attached A successfully!\n"));
74
75 KdPrint(("DriverB:Leave B DriverEntry\n"));
76 return ntStatus;
77 }
78 /************************************************************************
79 * 函数名称:HelloDDKUnload
80 * 功能描述:负责驱动程序的卸载操作
81 * 参数列表:
82 pDriverObject:驱动对象
83 * 返回 值:返回状态
84 *************************************************************************/
85 #pragma PAGEDCODE
86 VOID HelloDDKUnload (IN PDRIVER_OBJECT pDriverObject)
87 {
88 PDEVICE_OBJECT pNextObj;
89 KdPrint(("DriverB:Enter B DriverUnload\n"));
90 pNextObj= pDriverObject->DeviceObject;
91
92 while (pNextObj!= NULL)
93 {
94 PDEVICE_EXTENSION pDevExt= (PDEVICE_EXTENSION)
95 pNextObj->DeviceExtension;
96 pNextObj= pNextObj->NextDevice;
97 //从设备栈中弹出
98 IoDetachDevice( pDevExt->TargetDevice);
99 //删除该设备对象
100 IoDeleteDevice( pDevExt->pDevice );
101 }
102 KdPrint(("DriverB:Enter B DriverUnload\n"));
103 }
104
105 /************************************************************************
106 * 函数名称:HelloDDKDispatchRoutine
107 * 功能描述:对读IRP进行处理
108 * 参数列表:
109 pDevObj:功能设备对象
110 pIrp:从IO请求包
111 * 返回 值:返回状态
112 *************************************************************************/
113 #pragma PAGEDCODE
114 NTSTATUS HelloDDKDispatchRoutine(IN PDEVICE_OBJECT pDevObj,
115 IN PIRP pIrp)
116 {
117 KdPrint(("DriverB:Enter B HelloDDKDispatchRoutine\n"));
118 NTSTATUS ntStatus= STATUS_SUCCESS;
119 // 完成IRP
120 pIrp->IoStatus.Status= ntStatus;
121 pIrp->IoStatus.Information=0;// bytes xfered
122 IoCompleteRequest( pIrp, IO_NO_INCREMENT );
123 KdPrint(("DriverB:Leave B HelloDDKDispatchRoutine\n"));
124 return ntStatus;
125 }
126
127 #pragma PAGEDCODE
128 NTSTATUS HelloDDKCreate(IN PDEVICE_OBJECT pDevObj,
129 IN PIRP pIrp)
130 {
131 KdPrint(("DriverB:Enter B HelloDDKCreate\n"));
132 NTSTATUS ntStatus= STATUS_SUCCESS;
133 //
134 //// 完成IRP
135 // pIrp->IoStatus.Status = ntStatus;
136 // pIrp->IoStatus.Information = 0;// bytes xfered
137 // IoCompleteRequest( pIrp, IO_NO_INCREMENT );
138
139 PDEVICE_EXTENSION pdx= (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
140
141 IoSkipCurrentIrpStackLocation (pIrp);
142
143 ntStatus= IoCallDriver(pdx->TargetDevice, pIrp);
144
145 KdPrint(("DriverB:Leave B HelloDDKCreate\n"));
146
147 return ntStatus;
148 }
149
150 #pragma PAGEDCODE
151 NTSTATUS HelloDDKRead(IN PDEVICE_OBJECT pDevObj,
152 IN PIRP pIrp)
153 {
154 KdPrint(("DriverB:Enter B HelloDDKCreate\n"));
155 NTSTATUS ntStatus= STATUS_SUCCESS;
156 //将自己完成IRP,改成由底层驱动负责
157
158 PDEVICE_EXTENSION pdx= (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
159
160 //调用底层驱动
161 IoSkipCurrentIrpStackLocation (pIrp);
162
163 ntStatus= IoCallDriver(pdx->TargetDevice, pIrp);
164
165 KdPrint(("DriverB:Leave B HelloDDKCreate\n"));
166
167 return ntStatus;
168 }
169
170 #pragma PAGEDCODE
171 NTSTATUS HelloDDKClose(IN PDEVICE_OBJECT pDevObj,
172 IN PIRP pIrp)
173 {
174 KdPrint(("DriverB:Enter B HelloDDKClose\n"));
175 NTSTATUS ntStatus= STATUS_SUCCESS;
176
177 PDEVICE_EXTENSION pdx= (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
178
179 IoSkipCurrentIrpStackLocation (pIrp);
180
181 ntStatus= IoCallDriver(pdx->TargetDevice, pIrp);
182
183 KdPrint(("DriverB:Leave B HelloDDKClose\n"));
184
185 return ntStatus;
186 }

载设备对象代码示例 P324

转发IRP代码示例 P326 如上代码示例Read

2、完成例程

在将IRP发送给低层驱动或者其他驱动之前,可以对IRP设置一个完成例程。一旦底层驱动将IRP完成后,IRP完成例程将被触发,可以通过这个原理来获得通知。

IoSetCompletionRoutine ,如果参数CompletionRoutine 为NULL,则意味着完成例程取消。IRP被IoCompleteRequest 完成时,会一层层出栈,如果遇到完成例程,则调用完成例程。

当调用IoCallDriver后,当前驱动就失去了对IRP的控制;如果此时设置IRP的属性,会引起系统崩溃。完成例程返回两种状态,STATUS_SUCCESS,STATUS_MORE_PROCESSING_REQUIRED。如果返回是 STATUS_MORE_PROCESSING_REQUIRED,则本层设备堆栈重新获得IRP的控制权,并且设备栈不会向上弹出,也就是向上“回卷” 设备栈停止,此时可以再次向底层发送IRP。

1)传播Pending位

IRP的堆栈向上回卷时,底层I/O堆栈的Control域的SL_PENDING_RETURNED位必须传播到上一层。如果本层没有设备完成例程,传播是自动的;如果设备了完成例程,则需要程序员自己实现。

注意:只能在完成例程中设置。

2)返回STATUS_SUCCESS

如果是STATUS_SUCCESS,则继续回卷。

代码

1 NTSTATUS
2 MyIoCompletion(
3 IN PDEVICE_OBJECT DeviceObject,
4 IN PIRP Irp,
5 IN PVOID Context
6 )
7 {
8 //进入此函数标志底层驱动设备将IRP完成
9 KdPrint(("Enter MyIoCompletion\n"));
10 if (Irp->PendingReturned)
11 {
12 //传播pending位
13 IoMarkIrpPending( Irp );
14 }
15 return STATUS_SUCCESS;//同STATUS_CONTINUE_COMPLETION
16 }
17
18 #pragma PAGEDCODE
19 NTSTATUS HelloDDKRead(IN PDEVICE_OBJECT pDevObj,
20 IN PIRP pIrp)
21 {
22 KdPrint(("DriverB:Enter B HelloDDKRead\n"));
23 NTSTATUS ntStatus = STATUS_SUCCESS;
24 //将自己完成IRP,改成由底层驱动负责
25
26 PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
27
28 //将当前IRP堆栈拷贝底层堆栈
29 IoCopyCurrentIrpStackLocationToNext(pIrp);
30
31 //设置完成例程
32 IoSetCompletionRoutine(pIrp,MyIoCompletion,NULL,TRUE,TRUE,TRUE);
33
34 //调用底层驱动
35 ntStatus = IoCallDriver(pdx->TargetDevice, pIrp);
36
37 //当IoCallDriver后,并且完成例程返回的是STATUS_SUCCESS
38 //IRP就不在属于派遣函数了,就不能对IRP进行操作了
39 if (ntStatus == STATUS_PENDING)
40 {
41 KdPrint(("STATUS_PENDING\n"));
42 }
43 ntStatus = STATUS_PENDING;
44
45 KdPrint(("DriverB:Leave B HelloDDKRead\n"));
46
47 return ntStatus;
48 }

 

示例代码 P333

3)STATUS_MORE_PROCESSING_REQUIRED

如果是STATUS_MORE_PROCESSING_REQUIRED,则本层堆栈重新获得控制,并且该IRP从完成状态变成了未完成状态,需要再次完成,即执行IoCompleteRequest 。

重新获得的IRP可以再次传下底层,也可以标志完成。

代码 

 1 NTSTATUS
2   MyIoCompletion(
3     IN PDEVICE_OBJECT  DeviceObject,
4     IN PIRP  Irp,
5     IN PVOID  Context
6     )
7 {
8
9     if (Irp->PendingReturned == TRUE)
10     {
11         //设置事件
12         KeSetEvent((PKEVENT)Context,IO_NO_INCREMENT,FALSE);
13     }
14
15     return STATUS_MORE_PROCESSING_REQUIRED;
16 }
17
18 #pragma PAGEDCODE
19 NTSTATUS HelloDDKRead(IN PDEVICE_OBJECT pDevObj,
20                                  IN PIRP pIrp)
21 {
22     KdPrint(("DriverB:Enter B HelloDDKRead\n"));
23     NTSTATUS ntStatus = STATUS_SUCCESS;
24     //将自己完成IRP,改成由底层驱动负责
25
26     PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
27
28     //将本层的IRP堆栈拷贝到底层堆栈
29     IoCopyCurrentIrpStackLocationToNext(pIrp);
30
31     KEVENT event;
32     //初始化事件
33     KeInitializeEvent(&event, NotificationEvent, FALSE);
34
35     //设置完成例程
36     IoSetCompletionRoutine(pIrp,MyIoCompletion,&event,TRUE,TRUE,TRUE);
37
38     //调用底层驱动
39     ntStatus = IoCallDriver(pdx->TargetDevice, pIrp);
40
41     if (ntStatus == STATUS_PENDING)
42     {
43         KdPrint(("IoCallDriver return STATUS_PENDING,Waiting ...\n"));
44         KeWaitForSingleObject(&event,Executive,KernelMode ,FALSE,NULL);
45         ntStatus = pIrp->IoStatus.Status;
46     }
47
48     //虽然在底层驱动已经将IRP完成了,但是由于完成例程返回的是
49     //STATUS_MORE_PROCESSING_REQUIRED,因此需要再次调用IoCompleteRequest!
50     IoCompleteRequest (pIrp, IO_NO_INCREMENT);
51
52     KdPrint(("DriverB:Leave B HelloDDKRead\n"));
53
54     return ntStatus;
55 }

示例代码 P335

3)将IRP分成多个IRP

通过一个例子来说明。

clip_image003

图示分层驱动模型 P336

代码
 1 #pragma PAGEDCODE
2 NTSTATUS
3 HelloDDKReadCompletion(
4     IN PDEVICE_OBJECT DeviceObject,
5     IN PIRP           Irp,
6     IN PVOID          Context
7     )
8 {
9     KdPrint(("DriverB:Enter B HelloDDKReadCompletion\n"));
10
11     PMYDRIVER_RW_CONTEXT rwContext = (PMYDRIVER_RW_CONTEXT) Context;
12     NTSTATUS ntStatus = Irp->IoStatus.Status;
13
14     ULONG stageLength;
15
16     if(rwContext && NT_SUCCESS(ntStatus))
17     {
18         //已经传送了多少字节
19         rwContext->Numxfer += Irp->IoStatus.Information;
20
21        if(rwContext->Length)
22        {
23            //设定下一阶段读取字节数
24             if(rwContext->Length > MAX_PACKAGE_SIZE)
25             {
26                 stageLength = MAX_PACKAGE_SIZE;
27             }
28             else
29             {
30                 stageLength = rwContext->Length;
31             }
32             //重新利用MDL
33             MmPrepareMdlForReuse(rwContext->NewMdl);
34
35             IoBuildPartialMdl(Irp->MdlAddress,
36                               rwContext->NewMdl,
37                               (PVOID) rwContext->VirtualAddress,
38                               stageLength);
39        
40             rwContext->VirtualAddress += stageLength;
41             rwContext->Length -= stageLength;
42
43             IoCopyCurrentIrpStackLocationToNext(Irp);
44             PIO_STACK_LOCATION nextStack = IoGetNextIrpStackLocation(Irp);
45
46             nextStack->Parameters.Read.Length = stageLength;
47
48             IoSetCompletionRoutine(Irp,
49                                    HelloDDKReadCompletion,
50                                    rwContext,
51                                    TRUE,
52                                    TRUE,
53                                    TRUE);
54
55             IoCallDriver(rwContext->DeviceExtension->TargetDevice,
56                          Irp);
57
58             return STATUS_MORE_PROCESSING_REQUIRED;
59         }
60         else
61         {
62             //最后一次传输
63             Irp->IoStatus.Information = rwContext->Numxfer;
64         }
65     }
66
67     KdPrint(("DriverB:Leave B HelloDDKReadCompletion\n"));
68     return STATUS_MORE_PROCESSING_REQUIRED; 
69 }
70

 

底层驱动示例代码 P337 见DriverA示例部分

代码

 1 #pragma PAGEDCODE
  2 NTSTATUS HelloDDKRead(IN PDEVICE_OBJECT pDevObj,
  3                                  IN PIRP pIrp)
  4 {
  5     KdPrint(("DriverB:Enter B HelloDDKRead\n"));
  6     NTSTATUS status = STATUS_SUCCESS;
  7
  8     PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)
  9         pDevObj->DeviceExtension;
10
11     PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);
12    
13     ULONG totalLength;
14     ULONG stageLength;
15     PMDL mdl;
16     PVOID virtualAddress;
17     PMYDRIVER_RW_CONTEXT rwContext = NULL;
18     PIO_STACK_LOCATION nextStack;
19
20     if (!pIrp->MdlAddress)
21     {
22         status = STATUS_UNSUCCESSFUL;
23         totalLength = 0;
24         goto HelloDDKRead_EXIT;
25     }
26
27     //获取MDL的虚拟地址
28     virtualAddress = MmGetMdlVirtualAddress(pIrp->MdlAddress);
29     //获取MDL的长度
30     totalLength = MmGetMdlByteCount(pIrp->MdlAddress);
31
32     KdPrint(("DriverB:(pIrp->MdlAddress)MmGetMdlVirtualAddress:%08X\n",MmGetMdlVirtualAddress(pIrp->MdlAddress)));
33     KdPrint(("DriverB:(pIrp->MdlAddress)MmGetMdlByteCount:%d\n",MmGetMdlByteCount(pIrp->MdlAddress)));
34
35     //将总的传输,分成几个阶段,这里设定每次阶段的长度
36     if(totalLength > MAX_PACKAGE_SIZE)
37     {
38         stageLength = MAX_PACKAGE_SIZE;
39     }else
40     {
41         stageLength = totalLength;
42     }
43
44     //创建新的MDL
45     mdl = IoAllocateMdl((PVOID) virtualAddress,
46                         totalLength,
47                         FALSE,
48                         FALSE,
49                         NULL);
50
51     KdPrint(("DriverB:(new mdl)MmGetMdlVirtualAddress:%08X\n",MmGetMdlVirtualAddress(mdl)));
52     KdPrint(("DriverB:(new mdl)MmGetMdlByteCount:%d\n",MmGetMdlByteCount(mdl)));
53
54     if(mdl == NULL)
55     {
56         KdPrint(("DriverB:Failed to alloc mem for mdl\n"));
57         status = STATUS_INSUFFICIENT_RESOURCES;
58         goto HelloDDKRead_EXIT;
59     }
60
61     //将IRP的MDL做重新映射
62     IoBuildPartialMdl(pIrp->MdlAddress,
63                       mdl,
64                       (PVOID) virtualAddress,
65                       stageLength);
66     KdPrint(("DriverB:(new mdl)MmGetMdlVirtualAddress:%08X\n",MmGetMdlVirtualAddress(mdl)));
67     KdPrint(("DriverB:(new mdl)MmGetMdlByteCount:%d\n",MmGetMdlByteCount(mdl)));
68
69     rwContext = (PMYDRIVER_RW_CONTEXT)
70                 ExAllocatePool(NonPagedPool,sizeof(MYDRIVER_RW_CONTEXT));
71
72     rwContext->NewMdl            = mdl;
73     rwContext->PreviousMdl        = pIrp->MdlAddress;
74     rwContext->Length            = totalLength - stageLength;//还剩下多少没读取
75     rwContext->Numxfer            = 0;                        //读了多少字节
76     rwContext->VirtualAddress    = ((ULONG_PTR)virtualAddress + stageLength);//下一阶段开始读取的地址
77     rwContext->DeviceExtension    = pDevExt;
78
79     //拷贝到底层堆栈
80     IoCopyCurrentIrpStackLocationToNext(pIrp);
81
82     nextStack = IoGetNextIrpStackLocation(pIrp);
83     //根据底层驱动的实现,底层驱动有可能读取这个数值,也有可能读取mdl的length。
84     nextStack->Parameters.Read.Length = stageLength;
85
86     pIrp->MdlAddress = mdl;
87    
88     //设定完成例程
89     IoSetCompletionRoutine(pIrp,
90                            (PIO_COMPLETION_ROUTINE)HelloDDKReadCompletion,
91                            rwContext,
92                            TRUE,
93                            TRUE,
94                            TRUE);
95
96     IoCallDriver(pDevExt->TargetDevice,pIrp);
97    
98     pIrp->MdlAddress = rwContext->PreviousMdl;
99     IoFreeMdl(rwContext->NewMdl);
100
101 HelloDDKRead_EXIT:
102     // 完成IRP
103     pIrp->IoStatus.Status = status;
104     pIrp->IoStatus.Information = totalLength;    // bytes xfered
105     IoCompleteRequest( pIrp, IO_NO_INCREMENT );
106     KdPrint(("DriverB:Leave B HelloDDKRead\n"));
107     return status;
108 }

 

中间层驱动,读派遣函数示例代码 P339

clip_image004

图示完成例程与派遣例程 P338

完全例程 示例代码 P342  如上代码示例中

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值