ICode9

精准搜索请尝试: 精确搜索
首页 > 系统相关> 文章详细

arm64_linux head.S的执行流程- 8.stext之__create_page_tables

2020-12-12 15:57:46  阅读:219  来源: 互联网

标签:__ tables head pgd 地址 页表 table dir pg


1.前言

本文基于高通8996平台,kernel版本为3.18.31。
本文主要介绍head.S的__create_page_tables执行流程

2. 页表基础知识

PGD(Page Global Directory)对应Level 0 translation table
PUD (Page Upper Directory) 对应Level 1 translation table
PMD (Page Middle Directory) 对应Level 2 translation table
PTE (Page Table Entry) 对应Level 3 translation table。

  • 4k,48bit虚拟地址的划分
    在这里插入图片描述

48bit的地址被分成9 + 9 + 9 + 9 + 12 = 48
PGD(Level 0)、PUD(Level 1)、PMD(Level 2)、PTE(Level 3)的translation table中的entry都是512项,每个entry是8byte,所以这些translation table都是4KB,刚好是一页。
TTBR0存储User Space所在的页表,TTBR1存储Kernel Space的页表。

注:PGD index 、PUD index、 PMD index、PTE index ,实际存储的都是对应各级page table entry的offset,各级page table的地址存放在上级page table 的相应页表项entry 中

  • 4k,39bit虚拟地址的划分
    在这里插入图片描述

  • MSM8996平台
    在MSM8996 Linux3.18中采用3级页表,4K的page size,39位虚拟地址

#.config
CONFIG_PGTABLE_LEVELS=3
arch/arm64/include/asm/page.h

/*
 * The idmap and swapper page tables need some space reserved in the kernel
 * image. Both require pgd, pud (4 levels only) and pmd tables to (section)
 * map the kernel. With the 64K page configuration, swapper and idmap need to
 * map to pte level. The swapper also maps the FDT (see __create_page_tables
 * for more information).
 */
#ifdef CONFIG_ARM64_64K_PAGES
#define SWAPPER_PGTABLE_LEVELS  (CONFIG_PGTABLE_LEVELS)
#else
#define SWAPPER_PGTABLE_LEVELS  (CONFIG_PGTABLE_LEVELS - 1)
#endif

在本例中,采用的是三级页表,即PGD PUD PTE,不包括PMD,其中:
虚拟地址中PGD,PUD,PTE分别表示PGD页表索引,PUD页表索引,2M BLOCK块内偏移;
PGD页表项,PUD页表项分别指向PUD页表地址,2M BLOCK块地址,因此PTE不需要专门的页表
所以由上面的宏定义可以知道SWAPPER_PGTABLE_LEVELS为2

2. 重要的宏说明

.macro pgtbl, ttb0, ttb1, virt_to_phys

  • 含义
    将idmap_pg_dir(存放用户空间页表地址)转换为物理地址存放到ttb0;
    将swapper_pg_dir(存放内核空间页表地址)转换为物理地址保存在ttb1
  • 参数
    virt_to_phys:物理地址与虚拟地址的偏移
    idmap_pg_dir :用于存放用户空间的页表,在进程切换的时候,其地址空间的切换实际就是修改TTBR0的值;
    swapper_pg_dir :用于存放kernel space页表,所有的内核线程都是共享一个空间。
.macro	pgtbl, ttb0, ttb1, virt_to_phys
ldr	\ttb1, =swapper_pg_dir
ldr	\ttb0, =idmap_pg_dir
add	\ttb1, \ttb1, \virt_to_phys//将swapper_pg_dir转换为物理地址并保存到ttb1
add	\ttb0, \ttb0, \virt_to_phys//将idmap_pg_dir转换为物理地址并保存到ttb0
.endm

idmap_pg_dir 和swapper_pg_dir分别定义如下:

#arch/arm64/kernel/vmlinux.lds.S

SECTIONS
{
    .....
        BSS_SECTION(0, 0, 0)
        
        . = ALIGN(PAGE_SIZE);
        idmap_pg_dir = .;
        . += IDMAP_DIR_SIZE;
        swapper_pg_dir = .;
        . += SWAPPER_DIR_SIZE;
  ......
} 

idmap_pg_dir 和 swapper_pg_dir定义在vmlinux.ld.S,位于bss段之后,idmap_pg_dir页表和swapper_pg_dir页表相邻。

#arch/arm64/include/asm/page.h

#define SWAPPER_DIR_SIZE        (SWAPPER_PGTABLE_LEVELS * PAGE_SIZE)
#define IDMAP_DIR_SIZE          (SWAPPER_DIR_SIZE)

由于每个页表占用一个page,本例中由于是3级页表,SWAPPER_PGTABLE_LEVELS为2,因此分别为idmap_pg_dir和swapper_pg_dir准备了2个page,用于存放pgd页表项和pud页表项,由于是2M block 因此pte不需要页表(或者说没有pte?)

.macro create_table_entry, tbl, virt, shift, ptrs, tmp1, tmp2

  • 含义
    是在物理基地址为tbl的页表上为虚拟地址virt创建页表项,此页表项的内容指向下一级页表

注:以下以pgd页表项为例,具体是在物理基地址为tbl的pgd页表上,索引为(virt>>shift)&(ptrs-1)的位置创建pgd页表项,页表项内容为tbl+PAGE_SIZE

  • 参数
    tbl:页表物理基地址
    virt:要创建页表项的虚拟地址
/*
 * Macro to create a table entry to the next page.
 *
 *	tbl:	page table address
 *	virt:	virtual address
 *	shift:	#imm page table shift
 *	ptrs:	#imm pointers per table page
 *
 * Preserves:	virt
 * Corrupts:	tmp1, tmp2
 * Returns:	tbl -> next level table page address
 */
	.macro	create_table_entry, tbl, virt, shift, ptrs, tmp1, tmp2
// tmp1 = virt >> 30
	lsr	\tmp1, \virt, #\shift
 
// table index
// tmp1 = tmp1 & 0x1ff 
// 至此tmp1的值为virt的[38:30]bit 
	and	\tmp1, \tmp1, #\ptrs - 1 
 
// tmp2 = tbl + 0x1000
	add	\tmp2, \tbl, #PAGE_SIZE 
   
// address of next table and entry type
// tmp2 = tmp2 | 0x3
	orr	\tmp2, \tmp2, #PMD_TYPE_TABLE	
	
// 将tmp2的值存入地址tbl + tmp1 * 8的内存中	
 str	\tmp2, [\tbl, \tmp1, lsl #3]
 
// next level table page
// tbl = tbl + 0x1000  
	add	\tbl, \tbl, #PAGE_SIZE		
.endm

(1)此处假设虚拟地址virt为39位,shift假设为PGDIR_SHIFT(30),ptrs为PTRS_PER_PGD(0x200),假设创建的是pgd页表及其页表项,由于是三级页表,不包含PMD
(2) tmp1存放的是virt的bit30~bit38,表示页表(pgd页表)的索引
共9个bit,对应的pgd页表为2^9=512项,每个为8个字节,所以正好一个page
(3)tmp2 组成了pgd页表的页表项
tmp2为tbl页表基地址加4k,正好是一个pud的第一个页表的基地址,pgd页表项的bit0~bit1存放下一级页表类型,因此tmp2组成一个pgd页表项,指向下一级pud页表的基地址
(4)将pgd页表项保存到pgd索引(tmp1)指向的pgd页表偏移位置

.macro create_pgd_entry, tbl, virt, tmp1, tmp2

  • 含义
    在物理基地址为tbl的pgd页表上为虚拟地址virt创建pgd页表项,此页表项的内容指向下一级页表PUD
/*
 * Macro to populate the PGD (and possibily PUD) for the corresponding
 * block entry in the next level (tbl) for the given virtual address.
 *
 * Preserves:	tbl, next, virt
 * Corrupts:	tmp1, tmp2
 */
	.macro	create_pgd_entry, tbl, virt, tmp1, tmp2
	create_table_entry \tbl, \virt, PGDIR_SHIFT, PTRS_PER_PGD, \tmp1, \tmp2
#if SWAPPER_PGTABLE_LEVELS == 3
	create_table_entry \tbl, \virt, TABLE_SHIFT, PTRS_PER_PTE, \tmp1, \tmp2
#endif
	.endm

 #define PGDIR_SHIFT ((PAGE_SHIFT - 3) * CONFIG_PGTABLE_LEVELS + 3) = \
(12 - 3) * 3 + 3 = 30 
#define PTRS_PER_PGD (1 << (VA_BITS - PGDIR_SHIFT)) = (1 << (39 - 30)) = 0x200
#define SWAPPER_PGTABLE_LEVELS    (CONFIG_PGTABLE_LEVELS - 1) = (3 - 1) = 2

.macro create_block_map, tbl, flags, phys, start, end

  • 含义
    在物理基地址为tbl的pud页表上,以phys作为起始物理地址,以(end-start)为大小创建PUD页表项,并将相应的物理地址phys+x*BLOCK_SIZE保存到tbl+(start+x)*8指向的PUD页表项,PUD页表项指向2M BLOCK块地址。

注:由于是三级页表,pud是最后一级页表,pte不需要页表,x表示第几个物理块

  • 参数
    tbl为当前级table的地址, 即PUD table的物理基地址
    phys为起始物理地址
    start为起始的虚拟地址
    end为结束的虚拟地址

注:由于采用2M的block,因此虚拟地址的分配为PGD(bit38bit30),PUD(bit29bit21),
PTE(bit20~bit0),其中PGD需要专门的table来存放下级table的地址,PUD需要专门的table来存放block address,PTE只是单纯作为offset来使用,通过PUD table定位到block address再通过PTE就可以定位到某个byte,因此PTE不需要专门的table

/*
 * Macro to populate block entries in the page table for the start..end
 * virtual range (inclusive).
 *
 * Preserves:	tbl, flags
 * Corrupts:	phys, start, end, pstate
 */
	.macro	create_block_map, tbl, flags, phys, start, end
	lsr	\phys, \phys, #BLOCK_SHIFT    //#define BLOCK_SHIFT SECTION_SHIFT
                                         //#define SECTION_SHIFT 21
                                         //phys=phys>>21
	lsr	\start, \start, #BLOCK_SHIFT //start=start>>21
	and	\start, \start, #PTRS_PER_PTE - 1 // table index
                                              //#define PTRS_PER_PTE  (1 << (PAGE_SHIFT - 3))
                                              //start=start & 0x1ff
                                              //至此start保存了其初始值的bit21~bit29
	orr	\phys, \flags, \phys, lsl #BLOCK_SHIFT	// table entry
                                              //phys=(phys<<21)|flags
	lsr	\end, \end, #BLOCK_SHIFT          //end=end>>21
	and	\end, \end, #PTRS_PER_PTE - 1	  // table end index
                                              //end=end & 0x1ff
                                              //至此end保存了其初始值的bit21~bit29
9999:	
    str	\phys, [\tbl, \start, lsl #3]	// store the entry
                                           // 将phys的值存入地址tbl + start * 8的内存中
                                           //即以start的[29:21]bit为索引的以8byte为单位的页表中
	add	\start, \start, #1			// next entry,下一个PUD页表项索引
	add	\phys, \phys, #BLOCK_SIZE		// next block,下一个PUD页表项内容
	cmp	\start, \end
	b.ls	9999b
	.endm

3. __inval_cache_range

如下引用自网上博文:
为什么要调用__inval_cache_range来invalidate idmap_pg_dir和swapper_pg_dir对应页表空间的cache呢?根据boot protocol,代码执行到此,对于cache的要求是kernel image对应的那段空间的cache line是clean到PoC的,不过idmap_pg_dir和swapper_pg_dir对应页表空间不属于kernel image的一部分,因此其对应的cacheline很可能有一些旧的,无效的数据,必须要清理掉。
顺便再提一句,将idmap和swapper页表内容设定为0是有意义的。实际上这些translation table中的大部分entry都是没有使用的,PGD和PUD都是只有一个entry是有用的,而PMD中有效的entry数目是和mapping的地址size有关。将页表内容清零也就是意味着将页表中所有的描述符设定为invalid(描述符的bit 0指示是否有效,等于0表示无效描述符)

#arch/arm64/mm/cache.S

/*
 *      __inval_cache_range(start, end)
 *      - start   - start address of region
 *      - end     - end address of region
 */
ENTRY(__inval_cache_range)
        /* FALLTHROUGH */

/*
 *      __dma_inv_range(start, end)
 *      - start   - virtual start address of region
 *      - end     - virtual end address of region
 */
ENTRY(__dma_inv_range)
        dcache_line_size x2, x3
        sub     x3, x2, #1
        tst     x1, x3                          // end cache line aligned?
        bic     x1, x1, x3
        b.eq    1f  
        dc      civac, x1                       // clean & invalidate D / U line
1:      tst     x0, x3                          // start cache line aligned?
        bic     x0, x0, x3
        b.eq    2f  
        dc      civac, x0                       // clean & invalidate D / U line
        b       3f  
2:      dc      ivac, x0                        // invalidate D / U line
3:      add     x0, x0, x2
        cmp     x0, x1
        b.lo    2b  
        dsb     sy  
        ret 
ENDPIPROC(__inval_cache_range)
ENDPROC(__dma_inv_range)

4. __create_page_tables

/*
 * Setup the initial page tables. We only setup the barest amount which is
 * required to get the kernel running. The following sections are required:
 *   - identity mapping to enable the MMU (low address, TTBR0)
 *   - first few MB of the kernel linear mapping to jump to once the MMU has
 *     been enabled, including the FDT blob (TTBR1)
 *   - pgd entry for fixed mappings (TTBR1)
 */
__create_page_tables:
	pgtbl	x25, x26, x28	// idmap_pg_dir and swapper_pg_dir addresses
                           // pgtbl是一个宏,用将idmap_pg_dir和swapper_pg_dir的物理地址分别赋给x25和x26
	mov	x27, lr        // 保存lr

	/*
	 * Invalidate the idmap and swapper page tables to avoid potential
	 * dirty cache lines being evicted.
	 */
	mov	x0, x25  // x0保存invalid cache的起始地址,即idmap_pg_dir
	add	x1, x26, #SWAPPER_DIR_SIZE // x1保存invalid cache的结束地址,即swapper_pg_dir+SWAPPER_DIR_SIZE
	bl	__inval_cache_range // 将idmap和swapper对应的cacheline设为无效

	/*
	 * Clear the idmap and swapper page tables.
	 */
	mov	x0, x25 //x25为idmap_pg_dir地址
	add	x6, x26, #SWAPPER_DIR_SIZE //x26为swapper_pg_dir地址
1:	stp	xzr, xzr, [x0], #16 //stp: 入栈指令(str 的变种指令,可以同时操作两个寄存器)入栈后x0+16
	stp	xzr, xzr, [x0], #16
	stp	xzr, xzr, [x0], #16
	stp	xzr, xzr, [x0], #16
	cmp	x0, x6
	b.lo	1b  // 循环将idmap和swapper内容清0

	ldr	x7, =MM_MMUFLAGS

	/*
	 * Create the identity mapping.
    * 创建kernel user mapping ,identity mapping实际上就是建立整个内核
    * 为地址范围KERNEL_START~KERNEL_END创建一致性mapping,即将物理地址等于虚拟地址
    * 为起始物理地址idmap_pg_dir范围KERNEL_START~KERNEL_END创建PGD页表项及PUD页表项
	 */
	mov	x0, x25			// idmap_pg_dir// x0保存idmap_pg_dir的物理地址
	ldr	x3, =KERNEL_START       //KERNEL_START是kernel text段的虚拟起始地址,
                                   // KERNEL_START = PAGE_OFFSET + TEXT_OFFSET, 
                                   //PAGE_OFFSET =  0xffffffc000000000, TEXT_OFFSET = 0x00080000
	add	x3, x3, x28	        // __pa(KERNEL_START)// x3保存kernel text段的起始地址(物理地址)
	create_pgd_entry x0, x3, x5, x6 // 创建pgd页表,x0是pgd的基地址,x3是需要创建pgd页表的内存虚拟地址,x5和x6是临时变量
                                   //x0保存idmap_pg_dir的物理地址,调用完毕x0+=0x100,指向下一个页表项指向的页表,也就是pud表基址
                                   //x3保存kernel text段的起始地址(物理地址)(此处比较特殊,按宏约定应为虚拟地址)
                                   //返回时x5保存了kernel text段的起始地址的bit30~bit38
                                   //返回时x6保存了kernel text段的起始地址的pgd索引对应的pgd页表项
                                   //至此创建PGD页表,只有一个页表项
	ldr	x6, =KERNEL_END  // #define KERNEL_END    _end
                            //_end定义在vmlinux.lds.S,顾名思义是x6保存kernel的结束地址(虚拟地址)
	mov	x5, x3				// __pa(KERNEL_START)
                                           // x5保存kernel的起始地址(物理地址)
	add	x6, x6, x28			// __pa(KERNEL_END)
                                           // x6保存kernel的结束地址(物理地址)
	create_block_map x0, x7, x3, x5, x6 // 创建pud页表,x0是pud的基地址,x7是flag,x3是需要创建pud页表的内存物理地址 
                                        //x5是起始物理地址,x6是结束物理地址(此处比较特殊,按宏约定应为虚拟地址)
                                       // 2MB block
                                       //至此为kernel start ~ kernel end创建了PUD页表

	/*
	 * Map the kernel image (starting with PHYS_OFFSET).
    * 创建kernel space mapping
    * 为起始物理地址PHYS_OFFSET即phys地址范围PAGE_OFFSET~KERNEL_IMAGE_END创建PGD页表项及PUD页表项
	 */
	mov	x0, x26				// swapper_pg_dir
                                           // swapper进程也就是idle进程的地址空间
	mov	x5, #PAGE_OFFSET               //PAGE_OFFSET等于0xffffffc000000000
	                                       //PAGE_OFFSET定义了将kernel image安放在虚拟地址空间的哪个位置上
   create_pgd_entry x0, x5, x3, x6        // 创建pgd页表,x0是pgd的基地址,x5是需要创建pgd页表的内存地址,x3和x6是临时变量
	ldr	x6, =KERNEL_END                // #define KERNEL_END    _end,_end定义在vmlinux.lds.S
                                          //顾名思义是kernel的结束地址(虚拟地址)
	mov	x3, x24			       // phys offset//x3保存kernel image起始物理地址
	create_block_map x0, x7, x3, x5, x6    // 创建pud页表,x0是pud的基地址,x7是flag,x3是需要创建pud页表的物理内存地址 
                                          //x5是起始虚拟地址,x6是结束虚拟地址
                                          // 2MB block

	/*
	 * Map the FDT blob (maximum 2MB; must be within 512MB of
	 * PHYS_OFFSET).
    *为起始物理地址FDT地址范围2M创建PUD页表项
	 */
	mov	x3, x21				// FDT phys address
                                          // x21保存的是device tree的物理地址
	and	x3, x3, #~((1 << 21) - 1)	// 2MB aligned
                                           // x3= x3 & 0xffffffffffe00000 // 2MB对齐
	mov	x6, #PAGE_OFFSET        //PAGE_OFFSET为kernel image的虚拟地址
                                   // kernel image真正内容前有TEXT_OFFSET的偏移
                                   // PAGE_OFFSET等于0xffffffc000000000
	sub	x5, x3, x24			// subtract PHYS_OFFSET
                                            // x5等于device tree相对于kernel image起始的物理内存地址的偏移
	tst	x5, #~((1 << 29) - 1)		// within 512MB?// 是否小于512MB
	csel	x21, xzr, x21, ne		// zero the FDT pointer
                                           // 如果x5大于512MB,则将x21清0
	b.ne	1f                             // 如果x5大于512MB,跳转到标号1处
	add	x5, x5, x6			// __va(FDT blob)
                                           // x5等于device tree的虚拟内存地址
	add	x6, x5, #1 << 21		// 2MB for the FDT blob
                                           //2MB,一般device tree编译生成的dtb只有几百KB
	sub	x6, x6, #1			// inclusive range
	create_block_map x0, x7, x3, x5, x6 // 创建pud页表,x0是pud的基地址,x7是flag,x3是需要创建pud页表的内存物理地址
                                          //x5是起始虚拟地址,x6是结束虚拟地址 
                                      // 2MB block
1:
	/*
	 * Since the page tables have been populated with non-cacheable
	 * accesses (MMU disabled), invalidate the idmap and swapper page
	 * tables again to remove any speculatively loaded cache lines.
	 */
	mov	x0, x25
	add	x1, x26, #SWAPPER_DIR_SIZE
	bl	__inval_cache_range // 再次将idmap和swapper对应的cacheline设为无效

	mov	lr, x27 // 恢复lr
	ret
ENDPROC(__create_page_tables)

__create_page_tables主要通过调用create_pgd_entry ,create_block_map 做了如下的工作:

  1. 将idmap_pg_dir(存放用户空间页表地址)和swapper_pg_dir(存放内核空间页表地址)对应的cache line无效;
  2. 将idmap_pg_dir(存放用户空间页表地址)和swapper_pg_dir(存放内核空间页表地址)的page table清零;
  3. 创建kernel 的一致性映射页表(物理地址与虚拟地址一致)
    在idmap_pg_dir处创建pgd table entry,存放的pud table地址为idmap_pg_dir+PAGE_SIZE;
    在idmap_pg_dir+PAGE_SIZE处创建pud table entry;
  4. 创建kernel的mapping映射页表
    在swapper_pg_dir处创建pgd table entry,存放的pud table地址为swapper_pg_dir+PAGE_SIZE;
    在swapper_pg_dir+PAGE_SIZE处创建pud table entry;
  5. 创建FDT的mapping映射页表
    在swapper_pg_dir+PAGE_SIZE处创建pud table entry,并对FDT地址和大小的合法性进行检查;
    注意在Linux4.x版本之后FDT映射页表的创建已经改为在
    setup_arch->early_fixmap_init

注:由于FDT在kernel image的512M范围以内,因此kernel image和fdt的虚拟地址的bit[38:30]相同,因此在(4)中已经创建了pud 地址为swapper_pg_dir+PAGE_SIZE的pgd table entry,在本步骤中不需要再创建pgd entry,而只是创建pud table entry即可

注:
(1)上文中kernel image表示kernel镜像,包含TEXT_OFFSET的内容,kernel表示直接从text段开始,不包含TEXT_OFFSET内容
(2)create_block_map或create_pgd_entry宏都是为虚拟地址创建页表项(kernel identity mapping除外),页表项中保存的是下级页表或块或页的物理基地址
(3)为什么还要为kernel物理地址进行一致性映射呢?
解释1:
CPU流水线取指令,在开MMU后,PC地址应该全部是虚拟地址,但是预取指令功能导致开MMU的时候既有虚拟地址,又有物理地址,如果我们只建立虚拟地址的映射,那么PC物理地址送到MMU之后就会找不到相应的页表,会出错。
解释2:
linux设置好页表之后,最终有一条指令是启用MMU的,假设该指令的PA是0x0800810c,根据我们要做的映射关系,它的VA应该是0xc000 810c,没有启用MMU之前CPU核发出的都是物理地址,从0x0800 810c地址取这条指令来执行,然而该指令执行之后,CPU核发出的地址都要被MMU拦截,CPU核就必须用虚拟地址来取指令了,因此下一条指令应该从0xc000 8110处取得,然而这时pc寄存器(也就是r15寄存器)的值并没有变,CPU核取下一条指令仍然要从0x0800 8110处取得,此时0x0800 8110已经成了非法地址了。为了解决这个问题,要求启用MMU的那条指令及其附近的指令虚拟地址跟物理地址相同,这样在启用MMU前后,附近指令的地址不会发生变化,从而实现平稳过渡。因此需要将物理地址从0x0800 0000开始的1M再映射到虚拟地址从0x0800 0000开始的1M,也就是做一个等价映射
解释3:
identity mapping实际上就是建立了整个内核(从KERNEL_START到KERNEL_END)的一致性mapping,就是将物理地址所在的虚拟地址段mapping到物理地址上去。为什么这么做呢?ARM ARM文档中有一段话:
If the PA of the software that enables or disables a particular stage of address translation differs from its VA, speculative instruction fetching can cause complications. ARM strongly recommends that the PA and VA of any software that enables or disables a stage of address translation are identical if that stage of translation controls translations that apply to the software currently being executed.
由于打开MMU操作的时候,内核代码欢快的执行,这时候有一个地址映射ON/OFF的切换过程,这种一致性映射可以保证在在打开MMU那一点附近的程序代码可以平滑切换

补充知识
1.stp: 入栈指令(str 的变种指令,可以同时操作两个寄存器),如: stp x29, x30, [sp, #0x10] ; 将 x29, x30 的值存入 sp 偏移 16 个字节的位置

标签:__,tables,head,pgd,地址,页表,table,dir,pg
来源: https://blog.csdn.net/jasonactions/article/details/111060163

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有