欢迎来到淘文阁 - 分享文档赚钱的网站! | 帮助中心 好文档才是您的得力助手!
淘文阁 - 分享文档赚钱的网站
全部分类
  • 研究报告>
  • 管理文献>
  • 标准材料>
  • 技术资料>
  • 教育专区>
  • 应用文书>
  • 生活休闲>
  • 考试试题>
  • pptx模板>
  • 工商注册>
  • 期刊短文>
  • 图片设计>
  • ImageVerifierCode 换一换

    深入理解Linux内存映射机制精品资料.doc

    • 资源ID:96698313       资源大小:309KB        全文页数:55页
    • 资源格式: DOC        下载积分:12金币
    快捷下载 游客一键下载
    会员登录下载
    微信登录下载
    三方登录下载: 微信开放平台登录   QQ登录  
    二维码
    微信扫一扫登录
    下载资源需要12金币
    邮箱/手机:
    温馨提示:
    快捷下载时,用户名和密码都是您填写的邮箱或者手机号,方便查询和重复下载(系统自动生成)。
    如填写123,账号就是123,密码也是123。
    支付方式: 支付宝    微信支付   
    验证码:   换一换

     
    账号:
    密码:
    验证码:   换一换
      忘记密码?
        
    友情提示
    2、PDF文件下载后,可能会被浏览器默认打开,此种情况可以点击浏览器菜单,保存网页到桌面,就可以正常下载了。
    3、本站不支持迅雷下载,请使用电脑自带的IE浏览器,或者360浏览器、谷歌浏览器下载即可。
    4、本站资源下载后的文档和图纸-无水印,预览文档经过压缩,下载后原文更清晰。
    5、试题试卷类文档,如果标题没有明确说明有答案则都视为没有答案,请知晓。

    深入理解Linux内存映射机制精品资料.doc

    深入理解Linux内存映射机制创建时间:2008-08-31文章属性:原创文章提交:wzt (wzt_at_xsec.org)深入理解Linux内存映射机制Author: wztEMail: wztxsec.orgSite: http:/www.xsec.org & Date: 2008-6-13一. 绪 论二. X86的硬件寻址方法三. 内核对页表的设置四. 实例分析映射机制一. 绪 论我们经常在程序的反汇编代码中看到一些类似0x32118965这样的地址,操作系统中称为线性地址,或虚拟地址。虚拟地址有什么用?虚拟地址又是如何转换为物理内存地址的呢?本章将对此作一个简要阐述。1.1  Linux内存寻址概述现代意义上的操作系统都处于32位保护模式下。每个进程一般都能寻址4G的物理空间。但是我们的物理内存一般都是几百M,进程怎么能获得4G的物理空间呢?这就是使用了虚拟地址的好处,通常我们使用一种叫做虚拟内存的技术来实现,因为可以使用硬盘中的一部分来当作内存使用。例外一点现在操作系统都划分为系统空间和用户空间,使用虚拟地址可以很好的保护内核空间被用户空间破坏。对于虚拟地址如何转为物理地址,这个转换过程有操作系统和CPU共同完成. 操作系统为CPU设置好页表。CPU通过MMU单元进行地址转换。1.2  浏览内核代码的工具现在的内核都很大, 因此我们需要某种工具来阅读庞大的源代码体系,现在的内核开发工具都选用vim+ctag+cscope浏览内核代码,网上已有现成的makefile文件用来生成ctags/cscope/etags。一、用法:    找一个空目录,把附件Makefile拷贝进去。然后在该目录中选择性地运行如下make命令:$ make将处理/usr/src/linux下的源文件,在当前目录生成ctags, cscope   注:SRCDIR用来指定内核源代码目录,如果没有指定,则缺省为/usr/src/linux/1) 只创建ctags$ make SRCDIR=/usr/src/linux-2.6.12/ tags2) 只创建cscope$ make SRCDIR=/usr/src/linux-2.6.12/ cscope3) 创建ctags和cscope$ make SRCDIR=/usr/src/linux-2.6.12/4) 只创建etags$ make SRCDIR=/usr/src/linux-2.6.12/ TAGS二、处理时包括的内核源文件:1) 不包括drivers,sound目录2) 不包括无关的体系结构目录3) fs目录只包括顶层目录和ext2,proc目录三、最简单的ctags命令1) 进入进入vim后,用:tag func_name跳到函数func_name2) 看函数(identifier)想进入光标所在的函数,用CTRL + 3) 回退回退用    CTRL + T1.3  内核版本的选取本次论文分析, 我选取的是linux-2.6.10版本的内核。最新的内核代码为2.6.25。但是现在主流的服务器都使用的是RedHat AS4的机器,它使用2.6.9的内核。我选取2.6.10是因为它很接近2.6.9,现在红帽企业Linux 4以Linux2.6.9内核为基础,是最稳定、最强大的商业产品。在2004年期间,Fedora等开源项目为Linux 2.6内核技术的更加成熟提供了一个环境,这使得红帽企业 Linux v.4内核可以提供比以前版本更多更好的功能和算法,具体包括:? 通用的逻辑CPU调度程序:处理多内核和超线程CPU。? 基于对象的逆向映射虚拟内存:提高了内存受限系统的性能。? 读复制更新:针对操作系统数据结构的SMP算法优化。? 多I/O调度程序:可根据应用环境进行选择。? 增强的SMP和NUMA支持:提高了大型服务器的性能和可扩展性。? 网络中断缓和(NAPI):提高了大流量网络的性能。Linux 2.6 内核使用了许多技术来改进对大量内存的使用,使得 Linux 比以往任何时候都更适用于企业。包括反向映射(reverse mapping)、使用更大的内存页、页表条目存储在高端内存中,以及更稳定的管理器。因此,我选取linux-2.6.10内核版本作为分析对象。二. X86的硬件寻址方法请参考Intel x86手册_三. 内核对页表的设置CPU做出映射的前提是操作系统要为其准备好内核页表,而对于页表的设置,内核在系统启动的初期和系统初始化完成后都分别进行了设置。3.1 与内存映射相关的几个宏这几个宏把无符号整数转换成对应的类型    #define _pte(x)              (pte_t) (x) )    #define _pmd(x)             (pmd_t) (x) )    #define _pgd(x)              (pgd_t) (x) )    #define _pgprot(x)            (pgprot_t) (x) )    根据x把它转换成对应的无符号整数    #define pte_val(x)              (x).pte_low)    #define pmd_val(x)             (x).pmd)    #define pgd_val(x)             (x).pgd)    #define pgprot_val(x)           (x).pgprot)    把内核空间的线性地址转换为物理地址    #define _pa(x)                (unsigned long)(x)-PAGE_OFFSET)    把物理地址转化为线性地址    #define _va(x)             (void *)(unsigned long)(x)+PAGE_OFFSET)      x是页表项值, 通过pte_pfn得到其对应的物理页框号, 最后通过pfn_to_page得到对应的物理页描述符    #define pte_page(x)             pfn_to_page(pte_pfn(x)    如果对应的表项值为0, 返回1    #define pte_none(x)             (!(x).pte_low)    x是页表项值, 右移12位后得到其对应的物理页框号    #define pte_pfn(x)         (unsigned long)(x).pte_low >> PAGE_SHIFT)根据页框号和页表项的属性值合并成一个页表项值    #define pfn_pte(pfn, prot)     _pte(pfn) << PAGE_SHIFT) | pgprot_val(prot)    根据页框号和页表项的属性值合并成一个中间表项值    #define pfn_pmd(pfn, prot)   _pmd(pfn) << PAGE_SHIFT) | pgprot_val(prot)    向一个表项中写入指定的值    #define set_pte(pteptr, pteval)         (*(pteptr) = pteval)    #define set_pte_atomic(pteptr, pteval)   set_pte(pteptr,pteval)    #define set_pmd(pmdptr, pmdval)      (*(pmdptr) = pmdval)    #define set_pgd(pgdptr, pgdval)       (*(pgdptr) = pgdval)    根据线性地址得到高10位值, 也就是在目录表中的索引    #define pgd_index(address) (address)>>PGDIR_SHIFT) & (PTRS_PER_PGD-1)    根据页描述符和属性得到一个页表项值    #define mk_pte(page, pgprot)    pfn_pte(page_to_pfn(page), (pgprot)3.2内核页表的初始化内核在进入保护模式前, 还没有启用分页功能, 在这之前内核要先建立一个临时内核页表,因为在进入保护模式后, 内核继续初始化直到建立完整的内存映射机制之前, 仍然需要用到页表来映射相应的内存地址。 临时页表的初始化是在arch/i386/kernel/head.S中进行的:swapper_pg_dir是临时页全局目录表, 它是在内核编译过程中静态初始化的.pg0是第一个页表开始的地方, 它也是内核编译过程中静态初始化的.内核通过以下代码建立临时页表:ENTRY(startup_32)    /* 得到开始目录项的索引,从这可以看出内核是在swapper_pg_dir的768个表项开始进行建立的, 其对应的线性地址就是0xc0000000以上的地址, 也就是内核在初始化它自己的页表 */        page_pde_offset = (_PAGE_OFFSET >> 20);/* pg0地址在内核编译的时候, 已经是加上0xc0000000了, 减去0xc00000000得到对应的物理地址 */        movl $(pg0 - _PAGE_OFFSET), %edi/* 将目录表的地址传给edx, 表明内核也要从0x00000000开始建立页表, 这样可以保证从以物理地址取指令到以线性地址在系统空间取指令的平稳过渡, 下面会详细解释 */              movl $(swapper_pg_dir - _PAGE_OFFSET), %edx           movl $0x007, %eax                   leal 0x007(%edi),%ecx              Movl %ecx,(%edx)        movl %ecx,page_pde_offset(%edx)        addl $4,%edx        movl $1024, %ecx11:        stosl  addl $0x1000,%eax        loop 11b        /* 内核到底要建立多少页表, 也就是要映射多少内存空间, 取决于这个判断条件。在内核初始化程中内核只要保证能映射到包括内核的代码段,数据段, 初始页表和用于存放动态数据结构的128k大小的空间就行 */        leal (INIT_MAP_BEYOND_END+0x007)(%edi),%ebp        cmpl %ebp,%eax        jb 10b        movl %edi,(init_pg_tables_end - _PAGE_OFFSET)在上述代码中, 内核为什么要把用户空间和内核空间的前几个目录项映射到相同的页表中去呢,虽然在head.S中内核已经进入保护模式,但是内核现在是处于保护模式的段式寻址方式下,因为内核还没有启用分页映射机制,现在都是以物理地址来取指令, 如果代码中遇到了符号地址,只能减去0xc0000000才行, 当开启了映射机制后就不用了现在cpu中的取指令指针eip仍指向低区,如果只建立内核空间中的映射, 那么当内核开启映射机制后, 低区中的地址就没办法寻址了,应为没有对应的页表, 除非遇到某个符号地址作为绝对转移或调用子程序为止。因此要尽快开启CPU的页式映射机制.movl $swapper_pg_dir-_PAGE_OFFSET,%eax        movl %eax,%cr3          /* cr3控制寄存器保存的是目录表地址 */        movl %cr0,%eax          /* 向cr0的最高位置1来开启映射机制 */        orl $0x80000000,%eax        movl %eax,%cr0        ljmp $_BOOT_CS,$1f     /* Clear prefetch and normalize %eip */1:        lss stack_start,%esp通过ljmp $_BOOT_CS,$1f这条指令使CPU进入了系统空间继续执行 因为_BOOT_CS是个符号地址,地址在0xc0000000以上。在head.S完成了内核临时页表的建立后,它继续进行初始化,包括初始化INIT_TASK,也就是系统开启后的第一个进程;建立完整的中断处理程序,然后重新加载GDT描述符,最后跳转到init/main.c中的start_kernel函数继续初始化.3.3内核页表的完整建立内核在start_kernel()中继续做第二阶段的初始化,因为在这个阶段中, 内核已经处于保护模式下,前面只是简单的设置了内核页表, 内核必须首先要建立一个完整的页表才能继续运行,因为内存寻址是内核继续运行的前提。pagetable_init()的代码在mm/init.c中:start_kernel()>setup_arch()>paging_init()>pagetable_init()为了简单起见, 我忽略了对PAE选项的支持。static void _init pagetable_init (void)               pgd_t *pgd_base = swapper_pg_dir;                kernel_physical_mapping_init(pgd_base);        在这个函数中pgd_base变量指向了swapper_pg_dir, 这正是内核目录表的开始地址,pagetable_init()函数在通过kernel_physical_mapping_init()函数完成内核页表的完整建立。kernel_physical_mapping_init函数同样在mm/init.c中, 我略去了与PAE模式相关的代码:static void _init kernel_physical_mapping_init(pgd_t *pgd_base)        unsigned long pfn;        pgd_t *pgd;        pmd_t *pmd;        pte_t *pte;        int pgd_idx, pmd_idx, pte_ofs;        pgd_idx = pgd_index(PAGE_OFFSET);        pgd = pgd_base + pgd_idx;        pfn = 0;        for (; pgd_idx < PTRS_PER_PGD; pgd+, pgd_idx+)                 pmd = one_md_table_init(pgd);                if (pfn >= max_low_pfn)                        continue;                for (pmd_idx = 0; pmd_idx < PTRS_PER_PMD && pfn < max_low_pfn; pmd+, pmd_idx+)                 unsigned int address = pfn * PAGE_SIZE + PAGE_OFFSET;                                pte = one_page_table_init(pmd);                for (pte_ofs = 0; pte_ofs < PTRS_PER_PTE && pfn < max_low_pfn; pte+, pfn+, pte_ofs+)                       if (is_kernel_text(address)                           set_pte(pte, pfn_pte(pfn, PAGE_KERNEL_EXEC);                      else                           set_pte(pte, pfn_pte(pfn, PAGE_KERNEL);                        通过作者的注释, 可以了解到这个函数的作用是把整个物理内存地址都映射到从内核空间的开始地址,即从0xc0000000的整个内核空间中,直到物理内存映射完毕为止。这个函数比较长, 而且用到很多关于内存管理方面的宏定义,理解了这个函数, 就能大概理解内核是如何建立页表的,将这个抽象的模型完全的理解。 下面将详细分析这个函数:函数开始定义了4个变量pgd_t *pgd, pmd_t *pmd, pte_t *pte, pfn;pgd指向一个目录项开始的地址,pmd指向一个中间目录开始的地址,pte指向一个页表开始的地址pfn是页框号被初始为0. pgd_idx根据pgd_index宏计算结果为768,也是内核要从目录表中第768个表项开始进行设置。 从768到1024这个256个表项被linux内核设置成内核目录项,低768个目录项被用户空间使用. pgd = pgd_base + pgd_idx; pgd便指向了第768个表项。然后函数开始一个循环即开始填充从768到1024这256个目录项的内容。one_md_table_init()函数根据pgd找到指向的pmd表。它同样在mm/init.c中定义:static pmd_t * _init one_md_table_init(pgd_t *pgd)        pmd_t *pmd_table;#ifdef CONFIG_X86_PAE        pmd_table = (pmd_t *) alloc_bootmem_low_pages(PAGE_SIZE);        set_pgd(pgd, _pgd(_pa(pmd_table) | _PAGE_PRESENT);        if (pmd_table != pmd_offset(pgd, 0)                BUG();#else        pmd_table = pmd_offset(pgd, 0);#endif        return pmd_table;可以看出, 如果内核不启用PAE选项, 函数将通过 pmd_offset返回pgd的地址。因为linux的二级映射模型,本来就是忽略pmd中间目录表的。接着又个判断语句:>> if (pfn >= max_low_pfn)>>        continue;这个很关键, max_low_pfn代表着整个物理内存一共有多少页框。 当pfn大于max_low_pfn的时候,表明内核已经把整个物理内存都映射到了系统空间中, 所以剩下有没被填充的表项就直接忽略了。因为内核已经可以映射整个物理空间了, 没必要继续填充剩下的表项。紧接着的第2个for循环,在linux的3级映射模型中,是要设置pmd表的, 但在2级映射中忽略, 只循环一次,直接进行页表pte的设置。>> address = pfn * PAGE_SIZE + PAGE_OFFSET;address是个线性地址, 根据上面的语句可以看出address是从0xc000000开始的,也就是从内核空间开始,后面在设置页表项属性的时候会用到它.>> pte = one_page_table_init(pmd);根据pmd分配一个页表, 代码同样在mm/init.c中:static pte_t * _init one_page_table_init(pmd_t *pmd)        if (pmd_none(*pmd)         pte_t *page_table = (pte_t *) alloc_bootmem_low_pages(PAGE_SIZE);        set_pmd(pmd, _pmd(_pa(page_table) | _PAGE_TABLE);        if (page_table != pte_offset_kernel(pmd, 0)               BUG();        return page_table;                return pte_offset_kernel(pmd, 0);pmd_none宏判断pmd表是否为空, 如果为空则要利用alloc_bootmem_low_pages分配一个4k大小的物理页面。 然后通过set_pmd(pmd, _pmd(_pa(page_table) | _PAGE_TABLE);来设置pmd表项。page_table显然属于线性地址,先通过_pa宏转化为物理地址,在与上_PAGE_TABLE宏,此时它们还是无符号整数,在通过_pmd把无符号整数转化为pmd类型,经过这些转换, 就得到了一个具有属性的表项, 然后通过set_pmd宏设置pmd表项.接着又是一个循环,设置1024个页表项。is_kernel_text函数根据前面提到的address来判断address线性地址是否属于内核代码段,它同样在mm/init.c中定义:static inline int is_kernel_text(unsigned long addr)        if (addr >= (unsigned long)_stext && addr <= (unsigned long)_init_end)                return 1;        return 0;_stext, _init_end是个内核符号, 在内核链接的时候生成的, 分别表示内核代码段的开始和终止地址.如果address属于内核代码段, 那么在设置页表项的时候就要加个PAGE_KERNEL_EXEC属性,如果不是,则加个PAGE_KERNEL属性.#define _PAGE_KERNEL_EXEC         (_PAGE_PRESENT | _PAGE_RW | _PAGE_DIRTY | _PAGE_ACCESSED)       #define _PAGE_KERNEL         (_PAGE_PRESENT | _PAGE_RW | _PAGE_DIRTY | _PAGE_ACCESSED | _PAGE_NX)       最后通过set_pte(pte, pfn_pte(pfn, PAGE_KERNEL);来设置页表项, 先通过pfn_pte宏根据页框号和页表项的属性值合并成一个页表项值,然户在用set_pte宏把页表项值写到页表项里。当pagetable_init()函数返回后,内核已经设置好了内核页表,紧着调用load_cr3(swapper_pg_dir);#define load_cr3(pgdir)         asm volatile("movl %0,%cr3": :"r" (_pa(pgdir)将控制swapper_pg_dir送入控制寄存器cr3. 每当重新设置cr3时, CPU就会将页面映射目录所在的页面装入CPU内部高速缓存中的TLB部分. 现在内存中(实际上是高速缓存中)的映射目录变了,就要再让CPU装入一次。由于页面映射机制本来就是开启着的, 所以从这条指令以后就扩大了系统空间中有映射区域的大小, 使整个映射覆盖到整个物理内存(高端内存)除外. 实际上此时swapper_pg_dir中已经改变的目录项很可能还在高速缓存中, 所以还要通过_flush_tlb_all()将高速缓存中的内容冲刷到内存中,这样才能保证内存中映射目录内容的一致性。3.4 对如何构建页表的总结通过上述对pagetable_init()的剖析, 我们可以清晰的看到, 构建内核页表, 无非就是向相应的表项写入下一级地址和属性。 在内核空间保留着一部分内存专门用来存放内核页表.当cpu要进行寻址的时候,无论在内核空间,还是在用户空间, 都会通过这个页表来进行映射。对于这个函数, 内核把整个物理内存空间都映射完了, 当用户空间的进程要使用物理内存时, 岂不是不能做相应的映射了? 其实不会的, 内核只是做了映射, 映射不代表使用, 这样做是内核为了方便管理内存而已。四. 实例分析映射机制4.1示例代码    通过前面的理论分析,我们通过编写一个简单的程序, 来分析内核是如何把线性地址映射到物理地址的。rootlocalhost temp# cat test.c#include <stdio.h>void test(void)        printf("hello, world.n");int main(void)        test();这段代码很简单, 我们故意要main调用test函数, 就是想看下test函数的虚拟地址是如何映射成物理地址的。4.2  段式映射分析我们先编译, 在反汇编下test文件rootlocalhost temp# gcc -o test test.crootlocalhost temp# objdump -d test08048368 <test>:8048368:       55                      push   %ebp8048369:       89 e5                    mov    %esp,%ebp804836b:       83 ec 08                  sub    $0x8,%esp804836e:       83 ec 0c                  sub    $0xc,%esp8048371:       68 84 84 04 08             push   $0x80484848048376:       e8 35 ff ff ff               call   80482b0 <printfplt>804837b:       83 c4 10                  add    $0x10,%esp804837e:       c9                       leave804837f:       c3                       ret08048380 <main>:8048380:       55                      push   %ebp8048381:       89 e5                   mov    %esp,%ebp8048383:       83 ec 08                 sub    $0x8,%esp8048386:       83 e4 f0                 and    $0xfffffff0,%esp8048389:       b8 00 00 00 00            mov    $0x0,%eax804838e:       83 c0 0f                  add    $0xf,%eax8048391:       83 c0 0f                  add    $0xf,%eax8048394:       c1 e8 04                  shr    $0x4,%eax8048397:       c1 e0 04                  shl    $0x4,%eax804839a:       29 c4                    sub    %eax,%esp804839c:       e8 c7 ff ff ff               call   8048368 <test>80483a1:       c9                       leave80483a2:       c3                       ret80483a3:       90                       nop从上述结果可以看到, ld给test()函数分配的地址为0x08048368.在elf格式的可执行文件代码中,ld的实际位置总是从0x8000000开始安排程序的代码段, 对每个程序都是这样。至于程序在执行时在物理内存中的实际位置就要由内核在为其建立内存映射时临时做出安排, 具体地址则取决于当时所分配到的物理内存页面。假设该程序已经运行, 整个映射机制都已经建立好, 并且CPU正在执行main()中的call 8048368这条指令, 要转移到虚拟地址0x08048368去运行. 下面将详细介绍这个虚拟地址转换为物理地址的映射过程.首先是段式映射阶段。由于0x08048368是一个程序的入口,更重要的是在执行的过程中是由CPU中的指令计数器EIP所指向的, 所以在代码段中。 因此, i386CPU使用代码段寄存器CS的当前值作为段式映射的选择子, 也就是用它作为在段描述表的下标.那么CS的值是多少呢?用GDB调试下test:(gdb) info regeax            0x10     16ecx            0x1      1edx            0x9d915c 10326364ebx            0x9d6ff4 10317812esp            0xbfedb480       0xbfedb480ebp            0xbfedb488       0xbfedb488esi            0xbfedb534       -1074940620edi            0xbfedb4c0       -1074940736eip            0x804836e        0x804836eeflags          0x282    642cs             0x73     115ss             0x7b     123ds             0x7b     123es             0x7b     123fs             0x0      0gs             0x33     51可以看到CS的值为0x73, 我们把它分解成二进制:0000 0000 0111 0011最低2位为3, 说明RPL的值为3, 应为我们这个程序本省就是在用户空间,RPL的值自然为3.第3位为0表示这个下标在GDT中。高13位为14, 所以段描述符在GDT表的第14个表项中, 我们可以到内核代码中去验证下:在i386/asm/segment.h中:#define GDT_ENTRY_DEFAULT_USER_CS       14#define _USER_CS (GDT_ENTRY_DEFAULT_USER_CS * 8 + 3)可以看到段描述符的确就是GDT表的第14个表项中。我们去GDT表看看具体的表项值是什么, GDT的内容在arch/i386/kernel/head.S中定义:ENTRY(cpu_gdt_table)        .quad 0x0000000000000000        /* NULL descriptor */        .quad 0x0000000000000000        /* 0x0b reserved */        .quad 0x0000000000000000        /* 0x13 reserved */        .quad 0x0000000000000000        /* 0x1b reserved */        .quad 0x0000000000000000        /* 0x20 unused */        .quad 0x0000000000000000        /* 0x28 unused */        .quad 0x0000000000000000        /* 0x33 TLS entry 1 */        .quad 0x0000000000000000        /* 0x3b TLS entry 2 */        .quad 0x0000000000000000        /* 0x43 TLS entry 3 */        .quad 0x0000000000000000        /* 0x4b reserved */        .quad 0x0000000000000000        /* 0x53 reserved */        .quad 0x0000000000000000        /* 0x5b reserved */        .quad 0x00cf9a000000ffff       /* 0x60 kernel 4GB code at 0x00000000 */        .quad 0x00cf92000000ffff       /* 0x68 kern

    注意事项

    本文(深入理解Linux内存映射机制精品资料.doc)为本站会员(封****n)主动上传,淘文阁 - 分享文档赚钱的网站仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。 若此文所含内容侵犯了您的版权或隐私,请立即通知淘文阁 - 分享文档赚钱的网站(点击联系客服),我们立即给予删除!

    温馨提示:如果因为网速或其他原因下载失败请重新下载,重复下载不扣分。




    关于淘文阁 - 版权申诉 - 用户使用规则 - 积分规则 - 联系我们

    本站为文档C TO C交易模式,本站只提供存储空间、用户上传的文档直接被用户下载,本站只是中间服务平台,本站所有文档下载所得的收益归上传人(含作者)所有。本站仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。若文档所含内容侵犯了您的版权或隐私,请立即通知淘文阁网,我们立即给予删除!客服QQ:136780468 微信:18945177775 电话:18904686070

    工信部备案号:黑ICP备15003705号 © 2020-2023 www.taowenge.com 淘文阁 

    收起
    展开