首页
社区
课程
招聘
[原创]驱动与应用程序之间的通信
2010-11-7 20:08 12197

[原创]驱动与应用程序之间的通信

2010-11-7 20:08
12197
最近比较忙,其实也就是瞎忙,今天抽出一点时间来给大家补补驱动的课,呵呵~~如果您已经会了,就直接跳过啦,真没什么技术含量,让大哥们笑话了~~但我想对于一些想进军驱动和刚踏入驱动的朋友们,如果能给大家一点小小的帮助我想就足够了!(小弟,也是刚踏入驱动的大门,有误之处,望各位大哥指出
看到论坛上有人问起驱动与应用层之间通信的问题,今天我给大家讲讲驱动与应用程序之间是怎么通信的吧~~
首先说说为什么大部分的驱动程序都需要与应用层程序之间进行通信,其实原因很简单,就是为了用应用层软件去控制电脑底层~~不过如果某些特殊功能的驱动,比如病毒中的某个驱动程序进行某种隐蔽的操作(病毒或木马之类的),也可以不用与用户通信,因为它的安装就是为了某项工作,完成就行了~~不过我想大部分的驱动都需要与应用程序进行通信吧~~个人观点!

那么怎么样才能使我们的驱动与应用程序之间进行通信呢?
目前比较流行的两种方法是:
第一种:使用WriteFile和ReadFile分别从驱动中读取和写入数据,然后用不同的IRP来传递信息。
第二种:使用DeviceIoControl通信

编程工具:RadASM
编程语言:Win32汇编
使用WriteFile和ReadFile进行通信
首先用RadASM建立一个Driver工程Connection
Connection.asm的代码如下:
.386
.model flat,stdcall
option casemap:none

include D:\RadASM\masm32\include\w2k\ntstatus.inc        ;根据自己的安装路径进行配置
include D:\RadASM\masm32\include\w2k\ntddk.inc
include D:\RadASM\masm32\include\w2k\ntoskrnl.inc

includelib D:\RadASM\masm32\lib\w2k\ntoskrnl.lib

include D:\RadASM\masm32\macros\Strings.mac

include Connection.inc

.const
CCOUNTED_UNICODE_STRING "\\Device\\Connection",g_usDeviceName,4
CCOUNTED_UNICODE_STRING "\\DosDevice\\Connection",g_usSymbolicLinkName,4

.code
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;DispatchCreateClose
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
DispatchCreateClose proc pDeviceObject:PDEVICE_OBJECT,pIrp:PIRP
       
       
        mov eax,pIrp
        assume eax:PTR _IRP
        mov [eax].IoStatus.Status,STATUS_SUCCESS
        and [eax].IoStatus.Information,0
        assume eax:nothing
       
        fastcall IofCompleteRequest,pIrp,IO_NO_INCREMENT
       
        mov eax,STATUS_SUCCESS
        ret

DispatchCreateClose endp

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;DriverUnload
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
DriverUnload proc pDriverObject:PDRIVER_OBJECT
       
        ;删除符号符连接
        invoke IoDeleteSymbolicLink,addr g_usSymbolicLinkName
       
        mov eax,pDriverObject
       
        ;删除设备对象
        invoke IoDeleteDevice,(DRIVER_OBJECT PTR[eax]).DeviceObject
        ret

DriverUnload endp

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;DispatchWrite
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
DispatchWrite proc uses esi edi pDeviceObject:PDEVICE_OBJECT,pIrp:PIRP
       
        LOCAL status:NTSTATUS
       
        mov esi,pIrp
        assume esi:PTR _IRP
       
        IoGetCurrentIrpStackLocation esi
       
        mov edi,eax
        assume edi:PTR IO_STACK_LOCATION
       
        invoke DbgPrint,$CTA0("[Test]%d"),[edi].Parameters.Write._Length
        invoke DbgPrint,$CTA0("[Test]%s"),[esi].AssociatedIrp.SystemBuffer
       
        push status
        pop  [esi].IoStatus.Status
       
        push [edi].Parameters.Write._Length
        pop  [esi].IoStatus.Information
       
        assume edi:nothing
        assume esi:nothing
       
        fastcall IofCompleteRequest,esi,IO_NO_INCREMENT
       
        mov eax,status
        ret

DispatchWrite endp

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;DispatchRead
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
DispatchRead proc pDeviceObject:PDEVICE_OBJECT,pIrp:PIRP
       
       
        LOCAL status:NTSTATUS
       
        mov esi,pIrp
        assume esi:PTR _IRP
       
        IoGetCurrentIrpStackLocation esi
       
        mov edi,eax
        assume edi:PTR IO_STACK_LOCATION
       
        invoke DbgPrint,$CTA0("[Test]%d"),[edi].Parameters.Read._Length
        invoke DbgPrint,$CTA0("[Test]%s"),[esi].AssociatedIrp.SystemBuffer
       
        push status
        pop  [esi].IoStatus.Status
       
        push [edi].Parameters.Read._Length
        pop  [esi].IoStatus.Information
       
        assume edi:nothing
        assume esi:nothing
       
        fastcall IofCompleteRequest,esi,IO_NO_INCREMENT
       
        mov eax,status
       
        ret

DispatchRead endp

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;DriverEntry
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
DriverEntry proc pDriverObject:PDRIVER_OBJECT,pusRegistryPath:PUNICODE_STRING
       
        LOCAL status:NTSTATUS
        LOCAL pDeviceObject:PDEVICE_OBJECT
       
        mov status,STATUS_DEVICE_CONFIGURATION_ERROR
       
        ;创建设备对象
        invoke IoCreateDevice,pDriverObject,0,addr g_usDeviceName,FILE_DEVICE_UNKNOWN,0,FALSE,addr pDeviceObject
       
        .if eax == STATUS_SUCCESS
       

                ;创建符号连接
                invoke IoCreateSymbolicLink,addr g_usSymbolicLinkName,addr g_usDeviceName
                .if eax == STATUS_SUCCESS
                        mov eax,pDriverObject
                        assume eax:PTR DRIVER_OBJECT
                        or  [eax].Flags,DO_BUFFERED_IO
                        mov [eax].DriverUnload,offset DriverUnload
                        mov [eax].MajorFunction[IRP_MJ_CREATE*(sizeof PVOID)],offset DispatchCreateClose
                        mov [eax].MajorFunction[IRP_MJ_CLOSE*(sizeof PVOID)],offset DispatchCreateClose
                        mov [eax].MajorFunction[IRP_MJ_WRITE*(sizeof PVOID)],offset DispatchWrite
                        mov [eax].MajorFunction[IRP_MJ_READ*(sizeof PVOID)],offset DispatchRead
                        assume eax:nothing
                       
                        mov status,STATUS_SUCCESS
                .else
                        invoke IoDeleteDevice,pDeviceObject
                .endif
        .endif
       
        mov eax,status
        ret
DriverEntry endp
end DriverEntry

代码很简单,应用层的例子就不用多说了吧,只需要在应用层代码创建一个文件,然后调用WriteFile和ReadFile进行文件操作,驱动层就会响应应用层的操作进行相关处理,我这里的代码很简单只是输出了缓冲区中的内容,其实可以在其中加入更加复杂的操作,请读者自行研究~~

使用DeviceIoControl进行通信
前面我们使用的方法,使得我们每建行一次操作都要分别调用ReadFile和WriteFile进行操作,很麻烦,在这里我们就用一个比较通用的方法DeviceIoControl函数。
DeviceIoControl函数会使操作系统产生一个IRP_MJ_DEVICE_CONTROL类型的IRP,然后这个IRP会被分发到相应的派遣例程中
先来看看DeviceIoControl函数的原型声明:
BOOL DeviceIoControl(
        HANDLE        hDevice,
        DWORD         dwIoControlCode,           //进行的操作代码
        LPVOID                 lpInBuffer,
        DWORD                nInBufferSize,
        LPVOID                lpOutBuffer,
        DWORD                nOutBufferSize,
        LPDWORD        lpBytesReturned,
        LPOVERLAPPED        lpOverlapped
        )
主要看第二个参数,它是一个I/O控制码,即IOCTL值,是一个32位的无符号整型数值。
什么是IOCTL?
IO控制指令(IOCTLs)主要用于用户态应用程序和驱动之间的沟通或者设备栈内驱动之间的沟通,这种指令通过IRP来进行传送。对DeviceIoControl的调用会促使I/O管理器产生一个IRP_MJ_DEVICE_CONTROL请求并且发送给当前设备栈最顶端的驱动程序。另外,上层驱动程序可以通过产生和发送IRP_MJ_DEVICE_CONTROL或者IRP_MJ_INTERNAL_DEVICE_CONTROL请求的方式向下层驱动程序发送IO控制指令。驱动程序在DispatchDeviceControl和DispatchInternalDeviceControl这两个例程中处理这些请求。
首先我们用RadASM建立一个名为DeviceControl的Driver工程
DeviceControl.inc的代码如下:
IOCTL_DEVICEIOCONTROL equ CTL_CODE(FILE_DEVICE_UNKNOWN,800h,METHOD_BUFFERED,FILE_READ_ACCESS + FILE_WRITE_ACCESS)

DeviceControl.asm的代码如下:
.386
.model flat,stdcall
option casemap:none

include D:\RadASM\masm32\include\w2k\ntstatus.inc        ;根据自己的安装路径进行配置
include D:\RadASM\masm32\include\w2k\ntddk.inc
include D:\RadASM\masm32\include\w2k\ntoskrnl.inc

includelib D:\RadASM\masm32\lib\w2k\ntoskrnl.lib

include D:\RadASM\masm32\macros\Strings.mac

include DeviceControl.inc

.const
CCOUNTED_UNICODE_STRING "\\Device\\DeviceControl",g_usDeviceName,4
CCOUNTED_UNICODE_STRING "\\??\\DeviceControl",g_usSymbolicLinkName,4

.code
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;DispatchCreateClose
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
DispatchCreateClose proc pDeviceObject:PDEVICE_OBJECT,pIrp:PIRP
       
       
        mov eax,pIrp
        assume eax:PTR _IRP
        mov [eax].IoStatus.Status,STATUS_SUCCESS
        and [eax].IoStatus.Information,0
        assume eax:nothing
       
        fastcall IofCompleteRequest,pIrp,IO_NO_INCREMENT
       
        mov eax,STATUS_SUCCESS
        ret

DispatchCreateClose endp

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;DispatchControl
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
DispatchControl proc pDeviceObject:PDEVICE_OBJECT,pIrp:PIRP
       
        LOCAL status:NTSTATUS
       
        mov esi,pIrp
        assume esi:PTR _IRP
       
        IoGetCurrentIrpStackLocation esi
        mov edi,eax
        assume edi:PTR IO_STACK_LOCATION
       
        .if [edi].Parameters.DeviceIoControl.IoControlCode == IOCTL_DEVICEIOCONTROL
                invoke DbgPrint,$CTA0("IOCTL_DEVICEIOCONTROL IS CALL")    ;演示之用,请读者自行扩展
        ;.elseif [edi].Parameters.DeviceIoControl.IoControlCode == ....   如果还有其它的请求进行处理
        .else
                mov status,STATUS_INVALID_DEVICE_REQUEST
        .endif
       
        assume edi:nothing
       
        push status
        pop [esi].IoStatus.Status
       
        push 0
        pop [esi].IoStatus.Information
        assume esi:nothing
       
        fastcall IofCompleteRequest,pIrp,IO_NO_INCREMENT
       
        mov eax,status
        ret

DispatchControl endp

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;DriverUnload
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
DriverUnload proc pDriverObject:PDRIVER_OBJECT
       
        ;删除符号符连接
        invoke IoDeleteSymbolicLink,addr g_usSymbolicLinkName
       
        mov eax,pDriverObject
       
        ;删除设备对象
        invoke IoDeleteDevice,(DRIVER_OBJECT PTR[eax]).DeviceObject
        ret

DriverUnload endp

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;DriverEntry
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

DriverEntry proc pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING
        LOCAL status:NTSTATUS
        LOCAL pDeviceObject:PDEVICE_OBJECT
       
        mov status,STATUS_DEVICE_CONFIGURATION_ERROR
       
        invoke IoCreateDevice,pDriverObject,0,addr g_usDeviceName,FILE_DEVICE_UNKNOWN,0,FALSE,addr pDeviceObject
       
        .if eax == STATUS_SUCCESS
                invoke IoCreateSymbolicLink,addr g_usSymbolicLinkName,addr g_usDeviceName
                .if eax == STATUS_SUCCESS
                        mov eax,pDriverObject
                        assume eax:PTR DRIVER_OBJECT
                        mov [eax].DriverUnload,offset DriverUnload
                        mov [eax].MajorFunction[IRP_MJ_CREATE*(sizeof PVOID)],offset DispatchCreateClose
                        mov [eax].MajorFunction[IRP_MJ_CLOSE*(sizeof PVOID)],offset DispatchCreateClose
                        mov [eax].MajorFunction[IRP_MJ_DEVICE_CONTROL*(sizeof PVOID)],offset DispatchControl
                        assume eax:nothing
                       
                        mov status,STATUS_SUCCESS
                .else
                        invoke IoDeleteDevice,pDeviceObject
                .endif
        .endif
       
        mov eax,status
        ret

DriverEntry endp
end DriverEntry

好了,代码很简单,我就不多讲了,现在我们来看看应用层的代码吧!
我们用RadASM建立一个应用程序DeviceControlExe
DeviceControlExe.inc的代码如下:
IOCTL_DEVICEIOCONTROL equ CTL_CODE(FILE_DEVICE_UNKNOWN,800h,METHOD_BUFFERED,FILE_READ_ACCESS + FILE_WRITE_ACCESS)

DeviceControlExe.asm的代码如下:
.386
.model flat,stdcall
option casemap:none

include windows.inc
include user32.inc
include kernel32.inc
include advapi32.inc

includelib user32.lib
includelib kernel32.lib
includelib advapi32.lib

include D:\RadASM\masm32\include\winioctl.inc       ;当使用IOCTL时必须包含这个头文伯

include D:\RadASM\masm32\macros\Strings.mac

include DeviceControlExe.inc

.code
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;ControlConnection
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ControlConnection proc uses esi edi
       
        LOCAL hDevice:HANDLE
        LOCAL dwBytesReturned:DWORD
        invoke CreateFile,$CTA0("\\\\.\\DeviceControl"),GENERIC_READ + GENERIC_WRITE,\
                0,NULL,OPEN_EXISTING,0,NULL
               
        .if eax != INVALID_HANDLE_VALUE
               
                mov hDevice,eax
                               
                invoke DeviceIoControl,hDevice,IOCTL_DEVICEIOCONTROL,\
                         NULL,NULL,NULL,NULL,addr dwBytesReturned,NULL
                .if (eax != 0 ) && (dwBytesReturned == 0)
                        invoke MessageBox,NULL,$CTA0("Send Control Code is Success"),$CTA0("Success"),MB_OK
                .else
                        invoke MessageBox,NULL,$CTA0("Send Control Code is Failed"),$CTA0("Failed"),MB_OK
                .endif
        .else
                invoke MessageBox,NULL,$CTA0("Device Connection failed"),$CTA0("Failed"),MB_OK + MB_ICONSTOP
        .endif
       
        ret

ControlConnection endp

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;InstallDriver
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
InstallDriver proc
       
        LOCAL hSCManager:HANDLE
        LOCAL hService:HANDLE
        LOCAL acModulePath[MAX_PATH]:CHAR
        LOCAL _ss:SERVICE_STATUS
       
        invoke OpenSCManager,NULL,NULL,SC_MANAGER_ALL_ACCESS
       
        .if eax != NULL
                mov hSCManager,eax
               
                push eax
               
                invoke GetFullPathName,$CTA0("DeviceControl.sys"),sizeof acModulePath,addr acModulePath,esp
               
                pop eax
               
                invoke CreateService,hSCManager,$CTA0("DeviceControl"),$CTA0("DeviceControl"),\
                        SERVICE_START + SERVICE_STOP + DELETE,SERVICE_KERNEL_DRIVER,SERVICE_DEMAND_START,\
                        SERVICE_ERROR_IGNORE,addr acModulePath,NULL,NULL,NULL,NULL,NULL
               
                .if eax != NULL
                        mov hService,eax
                       
                        invoke StartService,hService,0,NULL
                       
                        .if eax != 0
                                invoke ControlConnection
                                invoke ControlService,hService,SERVICE_CONTROL_STOP,addr _ss
                        .else
                                invoke MessageBox,NULL,$CTA0("Cann't start driver"),$CTA0("Failed"),MB_OK + MB_ICONSTOP
                        .endif
                .else
                        invoke MessageBox,NULL,$CTA0("Can't register driver"),$CTA0("Failed"),MB_OK + MB_ICONSTOP
                .endif
               
                invoke DeleteService,hService
                invoke CloseServiceHandle,hSCManager
        .else
                invoke MessageBox,NULL,$CTA0("Cann't connect to Service Control Manager"),$CTA0("Failed"),MB_OK + MB_ICONSTOP
        .endif
        ret

InstallDriver endp

start proc
       
        invoke InstallDriver
        invoke ExitProcess,NULL
        ret

start endp
end start
呵呵,这样我们就完成了应用层的代码了,代码很简单,仅作为演示之用,如您有其它的需要自行添加其它功能~~
程序运行如图所示(将程序与驱动放在同一个目录下)
图1

好了,就先讲到这里,争取在过年之前,将驱动的相关编程全部讲完,申明:我只会讲方法,至于具体你要实现什么功能,由你自己完成,有句话说的好“师傅领进门,修行在各人”没有师傅也行,不过我想有的话可能会更快点,虽然我不算“师傅”级的人,仅是想和大家一起学习,才有这个想法,下次再见~~
(本来想把附件也传上,不过想想还是让大家多上机操作吧,熟能生巧,不要总是CTRL+C加CTRL+V的~~)

[培训]《安卓高级研修班(网课)》月薪三万计划

上传的附件:
收藏
点赞6
打赏
分享
最新回复 (9)
雪    币: 37
活跃值: (25)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
defddr 2010-11-7 21:54
2
0
第一种:使用WriteFile和ReadFile分别从驱动中读取和写入数据,然后用不同的IRP来传递信息。
不能保证信息传递的次序和效应速度  用处不大
一般都是用第二种方法吧

不过论坛可以针对某个问题进行讨论

类似楼主这种在某段时间将驱动的相关编程讲完 我觉得很艰难的
不如来本书  张帆的驱动技术开发详解 入门不错
雪    币: 2320
活跃值: (3971)
能力值: ( LV12,RANK:530 )
在线值:
发帖
回帖
粉丝
熊猫正正 9 2010-11-8 09:49
3
0
尽力,尽力~~
雪    币: 110
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
anyueyue 2010-11-9 20:57
4
0
学习......
雪    币: 329
活跃值: (1628)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
sungy 1 2010-12-20 22:30
5
0
汇编写驱动啊,厉害
雪    币: 157
活跃值: (376)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
likecrack 2010-12-29 00:55
6
0
[esi].AssociatedIrp.SystemBuffer

一直没搞懂。这个系统缓冲区的地址。如果一个控制码有输入和输出的话如何确定是传入的还是传出来的地址
雪    币: 88
活跃值: (11)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
gezz 2011-5-16 15:40
7
0
[QUOTE=likecrack;908845][esi].AssociatedIrp.SystemBuffer

一直没搞懂。这个系统缓冲区的地址。如果一个控制码有输入和输出的话如何确定是传入的还是传出来的地址[/QUOTE]

不同的IRP请求,对应着输入输出
雪    币: 1259
活跃值: (18)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
stu 2011-5-18 16:33
8
0
不错啊,不过用汇编还是不大习惯。
雪    币: 349
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
jerrylhj 2011-5-31 14:26
9
0
还是用C顺手。
雪    币: 183
活跃值: (998)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
yy大雄 2011-5-31 14:52
10
0
用C易读点吧   建议就C 了
游客
登录 | 注册 方可回帖
返回