copy_{to,from}_user

发布时间:2021-12-07 13:01:12

转载:https://blog.csdn.net/juS3Ve/article/details/100787869


引言
我们对copy_{to,from}_user()接口的使用应该是再熟悉不过吧。基本Linux书籍都会介绍它的作用。毕竟它是kernel space和user space沟通的桥梁。所有的数据交互都应该使用类似这种接口。所以,我们没有理由不知道接口的作用。但是,我也曾经有过以下疑问。


为什么需要copy_{to,from}_user(),它究竟在背后为我们做了什么?


copy_{to,from}_user()和memcpy()的区别是什么,直接使用memcpy()可以吗?


memcpy()替代copy_{to,from}_user()是不是一定会有问题?


一下子找回了当年困惑的自己。我所提出的每个问题,曾经我也思考过。还不止一次的思考,每一次都有不同的想法。当然是因为从一开始就我就没有完全理解。现在又重新回到这个沉重的话题,继续思考这曾经的问题。


温馨提示:文章代码分析基于Linux-4.18.0,部分架构相关代码以ARM64为代表。


百家争鸣
针对以上问题当然是先百度。百度对于该问题的博客也是很多,足以看出这个问题肯定困惑着一大批Linux的爱好者。对于我的查阅结果来说,观点主要分成以下两种:


copy_{to,from}user()比memcpy()多了传入地址合法性校验。例如是否属于用户空间地址范围。理论上说,内核空间可以直接使用用户空间传过来的指针,即使要做数据拷贝的动作,也可以直接使用memcpy(),事实上在没有MMU的体系架构上,copy{to,from}_user()最终的实现就是利用了mencpy()。但是对于大多数有MMU的*台,情况就有了些变化:用户空间传过来的指针是在虚拟地址空间上的,它所指向的虚拟地址空间很可能还没有真正映射到实际的物理页面上。但是这又能怎样呢?缺页导致的异常会很透明地被内核予以修复(为缺页的地址空间提交新的物理页面),访问到缺页的指令会继续运行仿佛什么都没有发生一样。但这只是用户空间缺页异常的行为,在内核空间这种缺页异常必须被显式地修复,这是由内核提供的缺页异常处理函数的设计模式决定的。其背后的思想是:在内核态,如果程序试图访问一个尚未被提交物理页面的用户空间地址,内核必须对此保持警惕而不能像用户空间那样毫无察觉。


如果我们确保用户态传递的指针的正确性,我们完全可以用memcpy()函数替代copy_{to,from}_user()。经过一些试验测试,发现使用memcpy(),程序的运行上并没有问题。因此在确保用户态指针安全的情况下,二者可以替换。


从各家博客上,观点主要集中在第一点。看起来第一点受到大家的广泛认可。但是,注重实践的人又得出了第二种观点,毕竟是实践出真知。真理究竟是是掌握在少数人手里呢?还是群众的眼睛是雪亮的呢?当然,我不否定以上任何一种观点。也不能向你保证哪种观点正确。因为,我相信即使是曾经无懈可击的理论,随着时间的推移或者特定情况的改变理论也可能不再正确。比如,牛顿的经典力学理论(好像扯得有点远)。如果要我说人话,就是:随着时间的推移,Linux的代码在不断的变化。或许以上的观点在曾经正确。当然,也可能现在还正确。下面的分析就是我的观点了。同样,大家也是需要保持怀疑的态度。下面我就抛砖引玉。


抛砖引玉
首先我们看下memcpy()和copy_{to,from}_user()的函数定义。参数几乎没有差别,都包含目的地址,源地址和需要复制的字节size。


static __always_inline unsigned long __must_check
copy_to_user(void __user *to, const void *from, unsigned long n);
static __always_inline unsigned long __must_check
copy_from_user(void *to, const void _user *from, unsigned long n);
void *memcpy(void *dest, const void *src, size_t len);
但是,有一点我们肯定是知道的。那就是memcpy()没有传入地址合法性校验。而copy
{to,from}_user()针对传入地址进行类似下面的合法性校验(简单说点,更多校验详情可以参考代码)。


如果从用户空间copy数据到内核空间,用户空间地址to及to加上copy的字节长度n必须位于用户空间地址空间。


如果从内核空间copy数据到用户空间,当然也需要检查地址的合法性。例如,是否越界访问或者是不是代码段的数据等等。总之一切不合法地操作都需要立刻杜绝。


经过简单的对比之后,我们再看看其他的差异以及一起探讨下上面提出的2个观点。我们先从第2个观点说起。涉及实践,我还是有点相信实践出真知。从我测试的结果来说,实现结果分成两种情况。


第一种情况的结果是:使用memcpy()测试,没有出现问题,代码正常运行。测试代码如下(仅仅展示proc文件系统下file_operations对应的read接口函数):


static ssize_t test_read(struct file *file, char __user *buf,
size_t len, loff_t offset)
{
memcpy(buf, “test
”, 5); /
copy_to_user(buf, “test
”, 5) */
return 5;
}
我们使用cat命令读取文件内容,cat会通过系统调用read调用test_read,并且传递的buf大小是4k。测试很顺利,结果很喜人。成功地读到了“test”字符串。看起来,第2点观点是没毛病的。但是,我们还需要继续验证和探究下去。因为第1个观点提到,“在内核空间这种缺页异常必须被显式地修复”。因此我们还需要验证的情况是:如果buf在用户空间已经分配虚拟地址空间,但是并没有建立和物理内存的具体映射关系,这种情况下会出现内核态page fault。我们首先需要创建这种条件,找到符合的buf,然后测试。这里我当然没测啦。因为有测试结论(主要是因为我懒,构造这个条件我觉得比较麻烦)。这个测试是我的一个朋友,人称宋老师的“阿助教”阿克曼大牛。他曾经做个这个实验,并且得到的结论是:即使是没有建立和物理内存的具体映射关系的buf,代码也可以正常运行。在内核态发生page fault,并被其修复(分配具体物理内存,填充页表,建立映射关系)。同时,我从代码的角度分析,结论也是如此。


经过上面的分析,看起来好像是memcpy()也可以正常使用,鉴于安全地考虑建议使用copy_{to,from}_user()等接口。


第二种情况的结果是:以上的测试代码并没有正常运行,并且会触发kernel oops。当然本次测试和上次测试的kernel配置选项是不一样的。这个配置项是 CONFIG_ARM64_SW_TTBR0_PAN或者 CONFIG_ARM64_PAN(针对ARM64*台)。两个配置选项的功能都是阻止内核态直接访问用户地址空间。只不过CONFIG_ARM64_SW_TTBR0_PAN是软件仿真实现这种功能,而CONFIG_ARM64_PAN是硬件实现功能(ARMv8.1扩展功能)。我们以CONFIG_ARM64_SW_TTBR0_PAN作为分析对象(软件仿真才有代码提供分析)。BTW,如果硬件不支持,即使配置CONFIG_ARM64_PAN也没用,只能使用软件仿真的方法。如果需要访问用户空间地址需要通过类似copy_{to,from}_user()的接口,否则会导致kernel oops。


在打开CONFIG_ARM64_SW_TTBR0_PAN的选项后,测试以上代码就会导致kernel oops。原因就是内核态直接访问了用户空间地址。因此,在这种情况我们就不可以使用memcpy()。我们别无选择,只能使用copy_{to,from}_user()。


为什么我们需要PAN(Privileged Access Never)功能呢?原因可能是用户空间和内核空间数据交互上容易引入安全问题,所以我们就不让内核空间轻易访问用户空间,如果非要这么做,就必须通过特定的接口关闭PAN。另一方面,PAN功能可以更加规范化内核态和用户态数据交互的接口使用。在使能PAN功能的情况下,可以迫使内核或者驱动开发者使用copy_{to,from}_user()等安全接口,提升系统的安全性。类似memcpy()非规范操作,kernel就oops给你看。


由于编程的不规范而引入安全漏洞。例如:Linux内核漏洞CVE-2017-5123可以提升权限。该漏洞的引入原因就是是缺少access_ok()检查用户传递地址的合法性。因此,为了避免自己编写的代码引入安全问题,针对内核空间和用户空间数据交互上,我们要格外当心。


刨根问底
既然提到了CONFIG_ARM64_SW_TTBR0_PAN的配置选项。当然我也希望了解其背后设计的原理。由于ARM64的硬件特殊设计,我们使用两个页表基地址寄存器ttbr0_el1和ttbr1_el1。处理器根据64 bit地址的高16 bit判断访问的地址属于用户空间还是内核空间。如果是用户空间地址则使用ttbr0_el1,反之使用ttbr1_el1。因此,ARM64进程切换的时候,只需要改变ttbr0_el1的值即可。ttbr1_el1可以选择不需要改变,因为所有的进程共享相同的内核空间地址。


当进程切换到内核态(中断,异常,系统调用等)后,如何才能避免内核态访问用户态地址空间呢?其实不难想出,改变ttbr0_el1的值即可,指向一段非法的映射即可。因此,我们为此准备了一份特殊的页表,该页表大小4k内存,其值全是0。当进程切换到内核态后,修改ttbr0_el1的值为该页表的地址即可保证访问用户空间地址是非法访问。因为页表的值是非法的。这个特殊的页表内存通过链接脚本分配。


#define RESERVED_TTBR0_SIZE (PAGE_SIZE)
SECTIONS
{
reserved_ttbr0 = .;
. += RESERVED_TTBR0_SIZE;
swapper_pg_dir = .;
. += SWAPPER_DIR_SIZE;
swapper_pg_end = .;
}
这个特殊的页表和内核页表在一起。和swapper_pg_dir仅仅差4k大小。reserved_ttbr0地址开始的4k内存空间的内容会被清零。


当我们进入内核态后会通过__uaccess_ttbr0_disable切换ttbr0_el1以关闭用户空间地址访问,在需要访问的时候通过_uaccess_ttbr0_enable打开用户空间地址访问。这两个宏定义也不复杂,就以_uaccess_ttbr0_disable为例说明原理。其定义如下:


.macro __uaccess_ttbr0_disable, tmp1
mrs mp1, ttbr1_el1 // swapper_pg_dir (1)
bic mp1, mp1, #TTBR_ASID_MASK
sub mp1, mp1, #RESERVED_TTBR0_SIZE // reserved_ttbr0 just before
// swapper_pg_dir (2)
msr ttbr0_el1, mp1 // set reserved TTBR0_EL1 (3)
isb
add mp1, mp1, #RESERVED_TTBR0_SIZE
msr ttbr1_el1, mp1 // set reserved ASID
isb
.endm
ttbr1_el1存储的是内核页表基地址,因此其值就是swapper_pg_dir。


swapper_pg_dir减去RESERVED_TTBR0_SIZE就是上面描述的特殊页表。


将ttbr0_el1修改指向这个特殊的页表基地址,当然可以保证后续访问用户地址都是非法的。


_uaccess_ttbr0_disable对应的C语言实现可以参考这里。如何允许内核态访问用户空间地址呢?也很简单,就是__uaccess_ttbr0_disable的反操作,给ttbr0_el1赋予合法的页表基地址。这里就不必重复了。我们现在需要知道的事实就是,在配置CONFIG_ARM64_SW_TTBR0_PAN的情况下,copy{to,from}user()接口会在copy之前允许内核态访问用户空间,并在copy结束之后关闭内核态访问用户空间的能力。因此,使用copy{to,from}_user()才是正统做法。主要体现在安全性检查及安全访问处理。这里是其比memcpy()多的第一个特性,后面还会介绍另一个重要特性。


现在我们可以解答上一节中遗留的问题。怎样才能继续使用memcpy()?现在就很简单了,在memcpy()调用之前通过uaccess_enable_not_uao()允许内核态访问用户空间地址,调用memcpy(),最后通过uaccess_disable_not_uao()关闭内核态访问用户空间的能力。


未雨绸缪
以上的测试用例都是建立在用户空间传递合法地址的基础上测试的,何为合法的用户空间地址?用户空间通过系统调用申请的虚拟地址空间包含的地址范围,即是合法的地址(不论是否分配物理页面建立映射关系)。既然要写一个接口程序,当然也要考虑程序的健壮性,我们不能假设所有的用户传递的参数都是合法的。我们应该预判非法传参情况的发生,并提前做好准备,这就是未雨绸缪。


我们首先使用memcpy()的测试用例,随机传递一个非法的地址。经过测试发现:会触发kernel oops。继续使用copy_{to,from}_user()替代memcpy()测试。测试发现:read()仅仅是返回错误,但不会触发kernel oops。这才是我们想要的结果。毕竟,一个应用程序不应该触发kernel oops。这种机制的实现原理是什么呢?


我们以copy_to_user()为例分析。函数调用流程是:


copy_to_user()->_copy_to_user()->raw_copy_to_user()->__arch_copy_to_user()
_arch_copy_to_user()在ARM64*台是汇编代码实现,这部分代码很关键。


end .req x5
ENTRY(__arch_copy_to_user)
uaccess_enable_not_uao x3, x4, x5
add end, x0, x2
#include “copy_template.S”
uaccess_disable_not_uao x3, x4
mov x0, #0
ret
ENDPROC(__arch_copy_to_user)
.section .fixup,“ax”
.align 2
9998: sub x0, end, dst // bytes not copied
ret
.previous
uaccess_enable_not_uao和uaccess_disable_not_uao是上面说到的内核态访问用户空间的开关。


copy_template.S文件是汇编实现的memcpy()的功能,稍后看看memcpy()的实现代码就清楚了。


.section.fixup,“ax”定义一个section,名为“.fixup”,权限是ax(‘a’可重定位的段,‘x’可执行段)。 9998标号处的指令就是“未雨绸缪”的善后处理工作。还记得copy_{to,from}user()返回值的意义吗?返回0代表copy成功,否则返回剩余没有copy的字节数。这行代码就是计算剩余没有copy的字节数。当我们访问非法的用户空间地址的时候,就一定会触发page fault。这种情况下,内核态发生的page fault并返回的时候并没有修复异常,所以肯定不能返回发生异常的地址继续运行。所以,系统可以有2个选择:第1个选择是kernel oops,并给当前进程发送SIGSEGV信号;第2个选择是不返回出现异常的地址运行,而是选择一个已经修复的地址返回。如果使用的是memcpy()就只有第1个选择。但是copy{to,from}_user()可以有第2个选择。 .fixup段就是为了实现这个修复功能。当copy过程中出现访问非法用户空间地址的时候,do_page_fault()返回的地址变成 9998标号处,此时可以计算剩余未copy的字节长度,程序还可以继续执行。


对比前面分析的结果,其实_arch_copy_to_user()可以*似等效如下关系。


uaccess_enable_not_uao();
memcpy(ubuf, kbuf, size); == __arch_copy_to_user(ubuf, kbuf, size);
uaccess_disable_not_uao();
先插播一条消息,解释copy_template.S为何是memcpy()。memcpy()在ARM64*台是由汇编代码实现。其定义在arch/arm64/lib/memcpy.S文件。


.weak memcpy
ENTRY(__memcpy)
ENTRY(memcpy)
#include “copy_template.S”
ret
ENDPIPROC(memcpy)
ENDPROC(__memcpy)
所以很明显,memcpy()和__memcpy()函数定义是一样的。并且memcpy()函数声明是weak,因此可以重写memcpy()函数(扯得有点远)。再扯一点,为何使用汇编呢?为何不使用lib/string.c文件的memcpy()函数呢?当然是为了优化memcpy() 的执行速度。lib/string.c文件的memcpy()函数是按照字节为单位进行copy(再好的硬件也会被粗糙的代码毁掉)。但是现在的处理器基本都是32或者64位,完全可以4 bytes或者8 bytes甚至16 bytes copy(考虑地址对齐的情况下)。可以明显提升执行速度。所以,ARM64*台使用汇编实现。这部分知识可以参考这篇博客《ARM64 的 memcpy 优化与实现》。


下面继续进入正题,再重复一遍:内核态访问用户空间地址,如果触发page fault,只要用户空间地址合法,内核态也会像什么也没有发生一样修复异常(分配物理内存,建立页表映射关系)。但是如果访问非法用户空间地址,就选择第2条路,尝试救赎自己。这条路就是利用 .fixup和 __ex_table段。如果无力回天只能给当前进程发送SIGSEGV信号。并且,轻则kernel oops,重则panic(取决于kernel配置选项CONFIG_PANIC_ON_OOPS)。在内核态访问非法用户空间地址的情况下,do_page_fault()最终会跳转 no_context标号处的do_kernel_fault()。


static void __do_kernel_fault(unsigned long addr, unsigned int esr,
struct pt_regs regs)
{
/

* Are we prepared to handle this kernel fault?
* We are almost certainly not prepared to handle instruction faults.
/
if (!is_el1_instruction_abort(esr) && fixup_exception(regs))
return;
/
… */
}
fixup_exception()继续调用search_exception_tables(),其通过查找_extable段。__extable段存储exception table,每个entry存储着异常地址及其对应修复的地址。例如上述的 9998:subx0,end,dst指令的地址就会被找到并修改do_page_fault()函数的返回地址,以达到跳转修复的功能。其实查找过程是根据出问题的地址addr,查找_extable段(exception table)是否有对应的exception table entry,如果有就代表可以被修复。由于32位处理器和64位处理器实现方式有差别,因此我们先从32位处理器异常表的实现原理说起。


_extable段的首尾地址分别是 __start___ex_table和 __stop___ex_table(定义在include/asm-generic/vmlinux.lds.h。这段内存可以看作是一个数组,数组的每个元素都是 struct exception_table_entry类型,其记录着异常发生地址及其对应的修复地址。


exception tables

__start___ex_table --> ±--------------+
| entry |
±--------------+
| entry |
±--------------+
| … |
±--------------+
| entry |
±--------------+
| entry |
__stop___ex_table --> ±--------------+
在32位处理器上,struct exception_table_entry定义如下:


struct exception_table_entry {
unsigned long insn, fixup;
};
有一点需要明确,在32位处理器上,unsigned long是4 bytes。insn和fixup分别存储异常发生地址及其对应的修复地址。根据异常地址ex_addr查找对应的修复地址(未找到返回0),其示意代码如下:


unsigned long search_fixup_addr32(unsigned long ex_addr)
{
const struct exception_table_entry *e;
for (e = __start___ex_table; e < __stop___ex_table; e++)
if (ex_addr == e->insn)
return e->fixup;
return 0;
}
在32位处理器上,创建exception table entry相对简单。针对copy{to,from}user()汇编代码中每一处用户空间地址访问的指令都会创建一个entry,并且insn存储当前指令对应的地址,fixup存储修复指令对应的地址。


当64位处理器开始发展起来,如果我们继续使用这种方式,势必需要2倍于32位处理器的内存存储exception table(因为存储一个地址需要8 bytes)。所以,kernel换用另一种方式实现。在64处理器上,struct exception_table_entry定义如下:


struct exception_table_entry {
int insn, fixup;
};
每个exception table entry占用的内存和32位处理器情况一样,因此内存占用不变。但是insn和fixup的意义发生变化。insn和fixup分别存储着异常发生地址及修复地址相对于当前结构体成员地址的偏移(有点拗口)。例如,根据异常地址ex_addr查找对应的修复地址(未找到返回0),其示意代码如下:


unsigned long search_fixup_addr64(unsigned long ex_addr)
{
const struct exception_table_entry *e;
for (e = __start___ex_table; e < __stop___ex_table; e++)
if (ex_addr == (unsigned long)&e->insn + e->insn)
return (unsigned long)&e->fixup + e->fixup;
return 0;
}
因此,我们的关注点就是如何去构建exception_table_entry。我们针对每个用户空间地址的内存访问都需要创建一个exception table entry,并插入_extable段。例如下面的汇编指令(汇编指令对应的地址是随意写的,不用纠结对错。理解原理才是王道)。


0xffff000000000000: ldr x1, [x0]
0xffff000000000004: add x1, x1, #0x10
0xffff000000000008: ldr x2, [x0, #0x10]
/* … */
0xffff000040000000: mov x0, #0xfffffffffffffff2 // -14
0xffff000040000004: ret
假设x0寄存器保存着用户空间地址,因此我们需要对0xffff000000000000地址的汇编指令创建一个exception table entry,并且我们期望当x0是非法用户空间地址时,跳转返回的修复地址是0xffff000040000000。为了计算简单,假设这是创建第一个entry, __start___ex_table值是0xffff000080000000。那么第一个exception table entry的insn和fixup成员的值分别是:0x80000000和0xbffffffc(这两个值都是负数)。因此,针对copy{to,from}user()汇编代码中每一处用户空间地址访问的指令都会创建一个entry。所以0xffff000000000008地址处的汇编指令也需要创建一个exception table entry。


所以,如果内核态访问非法用户空间地址究竟发生了什么?上面的分析流程可以总结如下:


访问非法用户空间地址:


0xffff000000000000:ldr x1,[x0]


MMU触发异常


CPU调用do_page_fault()


do_page_fault()调用search_exception_table()(regs->pc == 0xffff000000000000)


查看_extable段,寻找0xffff000000000000 并且返回修复地址0xffff000040000000


do_page_fault()修改函数返回地址(regs->pc = 0xffff000040000000)并返回


程序继续执行,处理出错情况


修改函数返回值x0 = -EFAULT (-14) 并返回(ARM64通过x0传递函数返回值)


总结
到了回顾总结的时候,copy_{to,from}_user()的思考也到此结束。我们来个总结结束此文。


无论是内核态还是用户态访问合法的用户空间地址,当虚拟地址并未建立物理地址的映射关系的时候,page fault的流程几乎一样,都会帮助我们申请物理内存并创建映射关系。所以这种情况下memcpy()和copy_{to,from}_user()是类似的。


当内核态访问非法用户空间地址的时候,根据异常地址查找修复地址。这种修复异常的方法并不是建立地址映射关系,而是修改do_page_fault()返回地址。而memcpy()无法做到这点。


在使能 CONFIG_ARM64_SW_TTBR0_PAN或者 CONFIG_ARM64_PAN(硬件支持的情况下才有效)的时候,我们只能使用copy_{to,from}_user()这种接口,直接使用memcpy()是不行的。


最后,我想说,即使在某些情况下memcpy()可以正常工作。但是,这也是不推荐的,不是良好的编程*惯。在用户空间和内核空间数据交互上,我们必须使用类似copy_{to,from}user()的接口。为什么类似呢?因为还有其他的接口用于内核空间和用户空间数据交互,只是没有copy{to,from}_user()出名。例如:{get,put}_user()。


本文转载自蜗窝科技:


http://www.wowotech.net/memory_management/454.html

相关文档

  • 三严三实专题教育材料
  • 游泳属于有氧还是无氧
  • 如何用正则表达式表示非“字符串”
  • 五年级作文650字我学会了坚持
  • RedHat Linux 9.0详尽安装图解
  • 关于师德学习的心得
  • 半路父子的演员有哪些
  • 我们应该交什么样的朋友? What Kind of Friend Should We Make
  • 日本优排行 十大最好看的av女星排行
  • 经典的农场生态基地的广告词
  • 朋友圈底部评论怎么全显示
  • 美发烫染知识
  • 番茄猪肝瘦肉汤营养丰富
  • 精华开学第一课日记3篇
  • 单位介绍信原件
  • 替代Xshell的良心国产SSH工具软件
  • 一个很简短但很炫的html5代码(canvas画布)。。。。。
  • SpringBoot一个类中注入多个JdbcTemplate提示@Primary
  • 生命,因挫折而美丽作文
  • 【C#知识点详解】String与StringBuilder的区别
  • 白云山旅游作文
  • 生产环境不停机启动 docker remote rpc
  • 去火清热解毒排毒吃什么粥好_去火清热排毒怎么做
  • 关于小学生消防安全手抄报内容
  • 相遇是缘,相识是份
  • 高中必修1物理期中试题答案
  • ReentrantLock Condition的使用和实现原理(不留死角!!!)
  • 2016年高考理综化学答题策略
  • 关于移民的英语作文生活的英文范文
  • 如何自定义CAD编辑器中的窗口?
  • 猜你喜欢

  • 部编版五年级复习测试综合复习上学期小学语文期中真题模拟试卷卷(④)课后练习
  • 销售人员销售激励管理方案 (2)
  • 结婚纪念日祝福语怎么说
  • 高中思想政治课时政信息运用的有效策略
  • 秋天梦寐在牧羊女的眼里
  • 【工作总结】20XX年科技局科技项目工作总结
  • WeUI 入门教程
  • 【范文】高二数学下册《分层抽样》知识点复*
  • 解析《鲁豫有约》的成功
  • 家庭营养食谱
  • 简单玫瑰花的折法图解
  • 基于小波变换的用于医学图像的半脆弱水印算法
  • 【人教版】2020年春六年级下册数学:导学案设计-第4单元-第11课时 图形的放大与缩小(例4)
  • 秦岭黄柏塬地区大熊猫保护现状浅析
  • XX简单个人租房合同模板_1
  • 【教育资料】六年级下语文单元测试-第二单元_人教版(无答案 )学习专用
  • 精品-2019年春八年级语文下册第三单元9桃花源记教案新人教版(1)
  • 2015年度新巴尔虎右旗隆利源矿业有限责任公司销售收入与资产数据报告
  • 谁知道中国历史朝代纪年表
  • 【免费下载】第10章 资本成本练*
  • 第二学期中学体育学科工作计划|第二学期中学体育学科工作计划
  • 仙人掌种子多久发芽 从播种到发芽要几天时间
  • 西安坤泰园餐饮管理有限公司企业信息报告-天眼查
  • 部编版五年级语文下册11军神教案
  • 【精选范文】《水浒传》的读书心得
  • 幼儿教师资格考试精选*题4及答案-优质
  • 江苏环通建设工程有限公司吉林分公司企业信用报告-天眼查
  • 【品质管理资料】活动隔墙工程检验批质量验收记录表精品版
  • 冰上“女王”强势回归
  • 企业人力资源管理师基础知识练*题(二级)
  • for循环实现简单图形打印
  • 中考优秀作文范文:秋天的色彩
  • 2019-2020年八年级期末考试生物试题及答案
  • 试论即兴在民族民间舞编创中的作用
  • 执业药师资格考试资料
  • 葛根粉怎么吃最好
  • 2014新人教版六年级上册数学期中试卷
  • 2020年小学读书活动总结模板
  • 中国金属天花行业市场前景分析预测年度报告(目录)
  • 2019年学校“纪念中国人民解放军建军xx周年”主题活动总结
  • 佛山市顺德区淘金时代广场物业管理有限公司企业信用报告-天眼查
  • 人力资源部人才测评面试指南
  • 电脑版