




已阅读5页,还剩44页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
Windows文件系统过滤驱动开发教程0. 作者,楚狂人自述我感觉Windows文件系统驱动的开发能找到的资料比较少。为了让技术经验不至于遗忘和引起大家交流的兴趣我以我的工作经验撰写本教程。我的理解未必正确,有错误的地方望多多指教。有问题欢迎与我联系。我们也乐于接受各种驱动项目的开发。邮箱为MFC_Tan_W,QQ为16191935。作者是杭州楚狂人,首先在驱动开发网连载.此版本比驱网连载版本稍有修改.除了0外,已发1-8节.后继的章节将在不久后整理推出.您可以随时发邮件向我索取更新的版本.最后更新日期是11月2日.1. 概述,钻研目的和准备我经常在网上碰到同行请求开发文件系统驱动。windows的pc机上以过滤驱动居多。其目的不外乎有以下几种:一是用于防病毒引擎。希望在系统读写文件的时候,捕获读写的数据内容,然后检测其中是否含有病毒代码。二是用于加密文件系统,希望在文件写过程中对数据进行加密,在读的过程中进行解密。三是设计透明的文件系统加速。读写磁盘的时候,合适的cache算法是可以大大提高磁盘的工作效率。windows本身的cache算法未必适合一些特殊的读写磁盘操作(如流媒体服务器上读流媒体文件)。设计自己的cache算法的效果,我已在工作中有所感受。如果你刚好有以上此类的要求,你可以阅读本教程。文件系统驱动是windows系统中最复杂的驱动种类之一。不能对ifsddk中的帮助抱太多希望,以我的经验看来,文件系统相关的ddk帮助极其简略,很多重要的部分仅仅轻描淡写的带过。如果安装了ifsddk,应该阅读srcfilesysOSR_docs下的文档。而不仅仅是ddk帮助。文件系统驱动开发方面的书籍很少。中文资料我仅仅见过侯捷翻译过的一本驱动开发的书上有两三章涉及,也仅仅是只能用于9x的vxd驱动。NT文件系统我见过一本英文书。我都不记得这两本书的书名了。如果您打算开发9x或者nt文件系统驱动,建议你去网上下载上文提及的书。那两本书都有免费的电子版本下载。如果你打算开发Windows2000WindowsXPWindow2003的文件系统驱动,你可以阅读本教程。虽然本教程仅仅讲述文件系统过滤驱动。但是如果您要开发一个全新的文件系统驱动的话,本教程依然对你有很大的帮助。学习文件系统驱动开发之前,应该在机器上安装ifsddk。ddk版本越高级,其中头文件中提供的系统调用也越多。经常有人询问如xpddk编译的驱动能不能在2000上运行等等的问题。我想可以这样解释:高级版本的ddk应该总是可以编译低级驱动的代码,而且得到的二进制版本也总是可以在低级系统上运行。但是反过来就未必可以了。如果在高级系统上编写用于低级系统上的驱动,要非常认真的注意仅仅调用低级系统上有的系统调用。ifsddk可以在某些ftp上免费下载。我的使用的是ifs ddk for xp,但是我实际用来开发的两台机器有一台是windows 2000,另一台是windows 2003.我尽量使我编译出来的驱动,可以在2000xp2003三种系统上都通过测试。安装配置ddk和在vc中开发驱动的方法网上有很多的介绍。ifsddk安装之后,src目录下的filesys目录下有文件系统驱动的示例。阅读这些代码你就可以快速的学会文件系统驱动开发。filter目录下的sfilter是一个文件系统过滤驱动的例子。另一个filespy完全是用这个例子的代码加工得更复杂而已。如何用ddk编译这个例子请自己查看相关的资料。文件系统过滤驱动编译出来后你得到的是一个扩展名为sys的文件。同时你需要写一个.inf文件来实现这个驱动的安装。我这里不讨论.inf文件的细节,你可以直接用sfilter目录下的inf文件修改。对inf文件点鼠标右键弹出菜单选择“安装”,即可安装这个过滤驱动。但是必须重新启动系统才生效。如果重启后蓝屏无法启动,可以用其他方式引导系统后到system32drivers目录下删除你的.sys文件再重启即可。我尝试这种情况下用安全模式结果还是蓝屏。所以我后来不得不在机器上装了两个2000系统。双系统情况下,一个系统崩溃了用另一个系统启动,删除原来的驱动即可。如果要调试代码,请安装softice.打开sfilter目录下的文件sources(这个文件没有扩展名),加入一行BROWSER_INFO=1然后打开Symbol Loader,File-Open选中你编译出来的xxx.sys,Modul-Load,Modul-Translate,然后就可以调试了。打开softice,输入file *就可以看见代码。如果准备好了,我们就可以开始琢磨windows文件系统过滤驱动的开发了。2.hello world,驱动对象与设备对象这里所说的驱动对象是一种数据结构,在DDK中名为DRIVER_OBJECT。任何驱动程序都对应一个DRIVER_OBJECT.如何获得本人所写的驱动对应的DRIVER_OBJECT呢?驱动程序的入口函数为DriverEntry,因此,当你写一个驱动的开始,你会写下如下的代码:NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath )这个函数就相当与喜欢c语言的你所常用的main().IN是无意义的宏,仅仅表明后边的参数是一种输入,而对应的OUT则代表这个参数是一种返回。这里没有使用引用,因此如果想在参数中返回结果,一律传入指针。DriverObject就是你所写的驱动对应的DRIVER_OBJECT,是系统在加载你的驱动时候所分配的。RegisteryPath是专用于你记录你的驱动相关参数的注册表路径。DriverObject重要之处,在于它拥有一组函数指针,称为dispatch functions.开发驱动的主要任务就是亲手撰写这些dispatch functions.当系统用到你的驱动,会向你的DO发送IRP(这是windows所有驱动的共同工作方式)。你的任务是在dispatch function中处理这些请求。你可以让irp失败,也可以成功返回,也可以修改这些irp,甚至可以自己发出irp。设备对象则是指DEVICE_OBJECT.下边简称DO.但是实际上每个irp都是针对DO发出的。只有针对由该驱动所生成的DO的IRP,才会发给该驱动来处理。当一个应用程序打开文件并读写文件的时候,windows系统将这些请求变成irp发送给文件系统驱动。文件系统过滤驱动将可以过滤这些irp.这样,你就拥有了捕获和改变文件系统操作的能力。象Fat32,NTFS这样的文件系统(File System,简称FS),可能生成好几种设备。首先文件系统驱动本身往往生成一个控制设备(CDO).这个设备的主要任务是修改整个驱动的内部配置。因此一个Driver只对应一个CDO.另一种设备是被这个文件系统Mount的Volume。一个FS可能有多个Volume,也可能一个都没有。解释一下,如果你有C:,D:,E:,F:四个分区。C:,D:为NTFS,E:,F:为Fat32.那么C:,D:则是NTFS的两个Volume设备对象.实际上C:是该设备的符号连接(Symbolic Link)名。而不是真正的设备名。可以打开Symbolic Links Viewer,能看到:C: “DeviceHarddiskVolume1”因此该设备的设备名为“DeviceHarddiskVolume1”.这里也看出来,文件系统驱动是针对每个Volume来生成一个DeviceObject,而不是针对每个文件的。实际上对文件的读写的irp,都发到Volume设备对象上去了。并不会生成一个“文件设备对象”。掌握了这些概念的话,我们现在用简单的代码来生成我们的CDO,作为我们开发文件系统驱动的第一步牛刀小试。我不喜欢用微软风格的代码。太长而且难看。我对大部分数据结构和函数进行了重定义。为此我写了一个名为wdf.h的头文件帮助我转换。有兴趣的读者可以发邮件向索取这个文件。没有也没有关系,我总是会写出wd_xxx系列的东西在DDK中的原形。/ -wdf_filter.c中的内容-#include wdf.hwd_stat wdff_cdo_create(in wd_drv *driver,in wd_size exten_len,in wd_ustr *name,out wd_dev *device) return wd_dev_create( driver, exten_len, name, wd_dev_disk_fs, wdf_dev_secure_open, wd_false, device);wd_stat wd_main(in wd_drv* driver,in wd_ustr* reg_path)wd_ustr name;wd_stat status = wd_stat_suc;/ 然后我生成控制设备,虽然现在我的控制设备什么都不干wd_ustr_init(&name,LFileSystemFiltersour_fs_filter);status = wdff_cdo_create(driver,0,&name,&g_cdo);if(!wd_suc(status)if(status = wd_stat_path_not_found)/ 这种情况发生于FileSystemFilters路径不存在。这个路径是/ 在xp上才加上的。所以2000下会运行到这里wd_ustr_init(&name,LFileSystemour_fs_filter);status = wdff_cdo_create(driver,0,&name,&g_cdo);if(!wd_suc(status)wd_printf0(error: create cdo failed.rn);return status;wd_printf0(success: create cdo ok.rn);return status;为了让代码看起来象上边的那样,我不得不做了很多转换。如#define wd_main DriverEntry 一种爽的感觉,终于可以在写看起来更象是main()的函数中工作了。 wd_dev_create 这个函数内部调用的是IoCreateDevice.而wd_suc实际上是SUCCESS()这样的宏。/ -wdf.h中的内容-#include ntifs.h#define inIN#define outOUT#define optionalOPTIONAL#define wd_ustrUNICODE_STRING#define wdp_ustrPUNICODE_STRING#define wd_mainDriverEntry/ 设备、驱动对象类型typedef DRIVER_OBJECTwd_drv;typedef DEVICE_OBJECTwd_dev;typedef PDRIVER_OBJECTwd_pdrv;typedef PDEVICE_OBJECTwd_pdev;enum wd_dev_disk_fs = FILE_DEVICE_DISK_FILE_SYSTEM,wd_dev_cdrom_fs = FILE_DEVICE_CD_ROM_FILE_SYSTEM,wd_dev_network_fs = FILE_DEVICE_NETWORK_FILE_SYSTEM;/ 状态相关的类型和宏typedef NTSTATUS wd_stat;enum wd_stat_suc = STATUS_SUCCESS,wd_stat_path_not_found = STATUS_OBJECT_PATH_NOT_FOUND,wd_stat_insufficient_res = STATUS_INSUFFICIENT_RESOURCES,wd_stat_invalid_dev_req = STATUS_INVALID_DEVICE_REQUEST,wd_stat_no_such_dev = STATUS_NO_SUCH_DEVICE,wd_stat_image_already_loaded = STATUS_IMAGE_ALREADY_LOADED,wd_stat_more_processing = STATUS_MORE_PROCESSING_REQUIRED,wd_stat_pending = STATUS_PENDING;_inline wd_bool wd_suc(wd_stat state)return NT_SUCCESS(state);#define wd_printf0DbgPrint_inline wd_void wd_ustr_init(in out wd_ustr* str, in const wd_wchar* chars)RtlInitUnicodeString(str,chars);_inline wd_void wd_ustr_init_em(in out wd_ustr*str,in wd_wchar *chars,in wd_size size)RtlInitEmptyUnicodeString(str,chars,size);wdf.h这个文件我仅仅节选了需要的部分。以上您已经拥有了一个简单的“驱动”的完整的代码。它甚至可以编译,安装(请修改sfilter.inf文件,其方法不过是将多处sfilter改为our_fs_filter,希望这个过程中您不会出现问题)。然后把wdf.h和wdf_filter.c放在您新建立的目录下,这个目录下还应该有另两个文件。一个是Makefile,请从sfilter目录下拷贝。另一个是SOURCES,请输入如下内容:TARGETNAME=our_fs_filterTARGETPATH=objTARGETTYPE=DRIVERDRIVERTYPE=FSBROWSER_INFO=1SOURCES=wdf_filter.c使用ddk编译之后您将得到our_fs_filter.sys.把这个文件与前所描述的inf文件同一目录,按上节所叙述方法安装。这个驱动不起任何作用,但是你已经成功的完成了hello world.3.分发例程,fast io上一节仅仅生成了控制设备对象。但是不要忘记,驱动开发的主要工作是撰写分发例程(dispatch functions.).接上一接,我们已经知道自己的DriverObject保存在上文代码的driver中。现在我写如下一个函数来指定一个默认的dispatch function给它。/-wdf.h中的代码-typedef PDRIVER_DISPATCH wd_disp_fuc;_inline wd_void wd_drv_set_dispatch(in wd_drv* driver, in wd_disp_fuc disp)wd_size i; for (i = 0; i MajorFunctioni = disp;在前边的wd_main中,我只要加wd_drv_set_dispatch(driver,my_dispatch_func);就为这个驱动指定了一个默认的Dispatch Function.所有的irp请求,都会被发送到这个函数。但是,我可能不希望这个函数处理过于复杂,而希望把一些常见的请求独立出来,如Read,Write,Create,Close,那我又写了几个函数专门用来设置这几个Dispatch Functions./-wdf.h中的代码-_inline wd_void wd_drv_set_read(in wd_drv* driver,in wd_disp_fuc read)driver-MajorFunctionIRP_MJ_READ = read;_inline wd_void wd_drv_set_write(in wd_drv* driver,in wd_disp_fuc write)driver-MajorFunctionIRP_MJ_WRITE = write;wd_void wd_drv_set_create(in wd_drv* driver, in wd_disp_fuc create) driver-MajorFunctionIRP_MJ_CREATE = create; driver-MajorFunctionIRP_MJ_CREATE_NAMED_PIPE = create; driver-MajorFunctionIRP_MJ_CREATE_MAILSLOT = create;wd_void wd_drv_set_file_sys_control(in wd_drv* driver,in wd_disp_fuc control)driver-MajorFunctionIRP_MJ_FILE_SYSTEM_CONTROL = control;wd_void wd_drv_set_clean_up(in wd_drv* driver, in wd_disp_fuc clean_up)driver-MajorFunctionIRP_MJ_CLEANUP = clean_up;wd_void wd_drv_set_close(in wd_drv* driver, in wd_disp_fuc close)driver-MajorFunctionIRP_MJ_CLOSE = close; 别看我罗列n多代码,其实就是在设置driver-MajorFunction这个数组而已。因此在wd_main对dispatch functions的设置,就变成了下边这样的:/ 开始设置几个分发例程wd_drv_set_dispatch(driver,my_disp_default);wd_drv_set_create(driver,my_disp_create);wd_drv_set_clean_up(driver,my_disp_clean_up);wd_drv_set_file_sys_control(driver,my_disp_file_sys_ctl);wd_drv_set_close(driver,my_disp_close);wd_drv_set_read(driver,my_disp_read);wd_drv_set_write(driver,my_disp_write);下面的任务都在写my_xxx系列的这些函数了。但是对于这个DriverObject的设置,还并不是仅仅这么简单。由于你的驱动将要绑定到文件系统驱动的上边,文件系统除了处理正常的IRP之外,还要处理所谓的FastIo.FastIo是Cache Manager调用所引发的一种没有irp的请求。换句话说,除了正常的Dispatch Functions之外,你还得为DriverObject撰写另一组Fast Io Functions.这组函数的指针在driver-FastIoDispatch.我不知道这个指针留空会不会导致系统崩溃。在这里本来是没有空间的,所以为了保存这一组指针,你必须自己分配空间。下面是我常用的内存分配函数。/-wdf.h中的代码-/ 最简单的分配内存的函数,可以指定分页非分页_inline wd_pvoid wd_malloc(wd_bool paged,wd_size size)if(paged)return ExAllocatePool(PagedPool,size);elsereturn ExAllocatePool(NonPagedPool,size);/ 释放内存_inline wd_void wd_free(wd_pvoid point)ExFreePool(point);_inline wd_void wd_memzero(wd_pvoid point,wd_size size)RtlZeroMemory(point,size);有了上边的基础,我就可以自己写一个初始化FastIoDispatch指针的函数。/-wdf.h中的代码-wd_bool wd_fio_disp_init(wd_drv *driver,wd_ulong size)wd_fio_disp *disp = wd_malloc(wd_false,size);if(disp = wd_null)return wd_false;wd_memzero(wd_pvoid)disp,size);driver-FastIoDispatch = disp;driver-FastIoDispatch-SizeOfFastIoDispatch = size;return wd_true;这个函数为FastIoDispacth指针分配足够的空间并填写它的大小。下面是再写一系列的函数来设置这个函数指针数组。实际上,FastIo接口函数实在太多了,所以我仅仅写出这些设置函数的几个作为例子:/-wdf.h中的代码-_inline wd_void wd_fio_disp_set_query_standard(wd_drv *driver,wd_fio_query_standard_func func) driver-FastIoDispatch-FastIoQueryStandardInfo = func;_inline wd_void wd_fio_disp_set_io_lock(wd_drv *driver,wd_fio_io_lock_func func) driver-FastIoDispatch-FastIoLock = func;_inline wd_void wd_fio_disp_set_io_unlock_s(wd_drv *driver,wd_fio_unlock_single_func func) driver-FastIoDispatch-FastIoUnlockSingle = func;.好,如果你坚持读到了这里,应该表示祝贺了。我们回顾一下,wd_main中,应该做哪些工作。a.生成一个控制设备。当然此前你必须给控制设置指定名称。b.设置Dispatch Functions.c.设置Fast Io Functions./ -wd_main 的近况-.wd_dev*g_cdo = NULL;wd_stat wd_main(in wd_drv* driver, in wd_ustr* reg_path)wd_ustr name;wd_stat status = wd_stat_suc;/ 然后我生成控制设备,虽然现在我的控制设备什么都不干wd_ustr_init(&name,LFileSystemFiltersour_fs_filters);status = wdff_cdo_create(driver,0,&name,&g_cdo);if(!wd_suc(status)if(status = wd_stat_path_not_found)/ 这种情况发生于FileSystemFilters路径不存在。这个路径是/ 在xp上才加上的。所以2000下可能会运行到这里wd_ustr_init(&name,LFileSystemour_fs_filters);status = wdff_cdo_create(driver,0,&name,&g_cdo);if(!wd_suc(status)wd_printf0(error: create cdo failed.rn);return status;wd_printf0(success: create cdo ok.rn);/ 开始设置几个分发例程wd_drv_set_dispatch(driver,my_disp_default);wd_drv_set_create(driver,my_disp_create);wd_drv_set_clean_up(driver,my_disp_clean_up);wd_drv_set_file_sys_control(driver,my_disp_file_sys_ctl);wd_drv_set_close(driver,my_disp_close);wd_drv_set_read(driver,my_disp_read);wd_drv_set_write(driver,my_disp_write);/ 指定fast io处理函数if(!wd_fio_disp_init(driver,sizeof(wd_fio_disp)wd_dev_del(g_cdo);wd_printf0(error: fast io disp init failed.rn);return wd_stat_insufficient_res;/ 下面指定的这些函数都定义在wdf_filter_fio.h中,其实这些函数都统/ 一的返回了falsewd_fio_disp_set_check(driver,my_fio_check);wd_fio_disp_set_read(driver,my_fio_read);wd_fio_disp_set_write(driver,my_fio_write);wd_fio_disp_set_query_basic(driver,my_fio_query_basic_info);.FastIo函数个数数量不明,我只觉得很多。因此不打算全部罗列,以.敷衍之。某些读者可能会认为这些代码无法调试安装。其实您可以参考sfilter中的示例自己完成这些代码。现在我们的my_xxx系列的函数还没有开始写,因此驱动也不能编译通过。在后边的内容中再逐步介绍。4.设备栈,过滤,文件系统的感知前边都在介绍文件系统驱动的结构,却还没讲到我们的过滤驱动如何能捕获所有发给文件系统驱动的irp,让我们自己来处理?前面已经解释过了设备对象。现在来解释一下设备栈。任何设备对象都存在于某个设备栈中。设备栈自然是一组设备对象。这些设备对象是互相关联的,也就是说,如果得到一个DO指针,你就可以知道它所处的设备栈。任何来自应用的请求,最终被windows io mgr翻译成irp的,总是发送给设备栈的顶端那个设备。原始irp irp irp irp- - - - DevTop Dev2 . DevVolumne . ?- - - DeviceType;文件系统的CDO的设备类型有下边的几种可能,你的过滤驱动可能只对其中某些感兴趣。enum wd_dev_disk_fs = FILE_DEVICE_DISK_FILE_SYSTEM,wd_dev_cdrom_fs = FILE_DEVICE_CD_ROM_FILE_SYSTEM,wd_dev_network_fs = FILE_DEVICE_NETWORK_FILE_SYSTEM;你应该自己写一个函数来判断该fs是否你所关心的。/ -一个函数,判断是否我所关心的fs-wd_bool my_care(wd_ulong type) return (type) = wd_dev_disk_fs) |(type) = wd_dev_cdrom_fs) |(type) = wd_dev_network_fs);下一个问题是我打算跳过文件系统识别器。文件系统识别器是文件系统驱动的一个很小的替身。为了避免没有使用到的文件系统驱动占据内核内存,windows系统不加载这些大驱动,而代替以该文件系统驱动对应的文件系统识别器。当新的物理存储媒介进入系统,io管理器会依次的尝试各种文件系统对它进行“识别”。识别成功,立刻加载真正的文件系统驱动,对应的文件系统识别器则被卸载掉。对我们来说,文件系统识别器的控制设备看起来就像一个文件系统控制设备。但我们不打算绑定它。分辨的方法是通过驱动的名字。凡是文件系统识别器的驱动对象的名字(注意是DriverObject而不是DeviceObject!)都为“FileSystemFs_Rec”./-用这些代码来跳过文件系统识别器-wd_wchar name_bufwd_dev_name_max_len;wd_ustr name,tmp;wd_ustr_init_em(&name,name_buf,wd_dev_name_max_len);wd_ustr_init(&tmp,LFileSystemFs_Rec);/ 我不绑定识别器。所以如果是识别器,我直接返回成功。查看是否是识别/ 器的办法是看是否是FileSystemFs_Rec的设备。wd_obj_get_name(wd_dev_drv(fs_dev),&name);if(wd_ustr_cmp(&name,&tmp,wd_true) = 0)wd_printf0(attach fs dev:is a recogonizer.rn);return wd_stat_suc;wd_printf0(attach fs dev: not a recogonizer.rn);接下来我将要生成我的设备。这里要提到设备扩展的概念。设备对象是一个数据结构,为了表示不同的设备,里边将有一片自定义的空间,用来给你记录这个设备的特有信息。我们为我们所生成的设备确定设备扩展如下:/ 文件过滤系统驱动的设备扩展typedef struct _my_dev_ext / 我们绑定的文件系统驱动 wd_dev * attached_to;/ 上边这个设备的设备名。wd_ustr dev_name;/ 这是上边的unicode字符串的缓冲区wd_wchar name_bufwd_dev_name_max_len; my_dev_ext;之所以如此简单,是因为我们现在还没有多少东西要记录。只要记得自己绑定在哪个设备上就好了。如果以后需要更多的信息,再增加不迟。扩展空间的大小是在wdf_dev_create(也就是这个设备生成)的时候指定的。得到设备对象指针后,我用下面这个函数来获取设备扩展指针:/ -wdf.h中的内容-_inline wd_void * wd_dev_ext(wd_dev *dev) return (dev-DeviceExtension);生成设备
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 油墨在艺术品复制印刷中的精细度考核试卷
- 污水处理中的微量有机物处理技术考核试卷
- 淀粉在食品干燥剂中的性能考核试卷
- 生物基纤维在汽车内饰中的应用考核试卷
- 碱水管施工方案
- 火力发电厂变压器安装与调试考核试卷
- 滚动轴承在精密加工中的应用考核试卷
- 服装零售店铺业绩提升策略与实践考核试卷
- 信用服务在供应链金融中的作用考核试卷
- 2025年通风网络机柜项目可行性研究报告
- 防洪防汛主题安全教育
- 外研版英语八年级下Module4-Unit1课件(共31张ppt)
- 左宗棠课件完整版
- 中药学电子版教材
- 市政道路电力、照明、通信管道工程施工方案方案
- 球的体积和表面积说课稿
- GB/T 30726-2014固体生物质燃料灰熔融性测定方法
- 可吸收丝素修复膜(CQZ1900597)
- 凯莱通综合版
- 步行功能训练详解课件
- 物理讲义纳米光子学
评论
0/150
提交评论