• Linux Arm64修改页表项属性


    前言

    在这篇文章中演示了通过 update_mapping_prot 函数 来 hook 系统调用
    Linux ARM64 hook系统调用
    接下来我们直接来修改对应系统调用的页表属性来hook 系统调用。

    一、获取pte

    pte_t *ptep_from_virt(uintptr_t addr) {
    
    	pgd_t *pgdp;
    	pud_t *pudp;
    	pmd_t *pmdp;
    	pte_t *ptep;
    
        struct mm_struct *mm = kallsyms_lookup_name("init_mm");
    
    	//获取内核全局页目录
    	pgdp = pgd_offset(mm, addr);
    	if (pgd_none(READ_ONCE(*pgdp))) {
    		printk(KERN_INFO "failed pgdp");
    		return 0;
    	}
    	
    	pudp = pud_offset(pgdp, addr);
    	if (pud_none(READ_ONCE(*pudp))) {
    		printk(KERN_INFO "failed pudp");
    		return 0;
    	}
    	
    	pmdp = pmd_offset(pudp, addr);
    	if (pmd_none(READ_ONCE(*pmdp))) {
    		printk(KERN_INFO "failed pmdp");
    		return 0;
    	}
    	
    	ptep = pte_offset_kernel(pmdp, addr);
    	if (!pte_valid(READ_ONCE(*ptep))) {
    		printk(KERN_INFO "failed pte");
    		return 0;
    	}
    
        return ptep;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    1.1 pgd_offset

    pgd_offset用于在页表目录(page-table-directory)中查找条目(entry)

    /* to find an entry in a page-table-directory */
    #define pgd_index(addr)		(((addr) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))
    
    #define pgd_offset_raw(pgd, addr)	((pgd) + pgd_index(addr))
    
    #define pgd_offset(mm, addr)	(pgd_offset_raw((mm)->pgd, (addr)))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    pgd_index(addr) 宏用于计算给定地址 addr 在页表目录中的索引。它通过将地址右移 PGDIR_SHIFT 位,然后与 (PTRS_PER_PGD - 1) 进行按位与运算来计算索引值。

    pgd_offset_raw(pgd, addr) 宏用于计算给定页表目录 pgd 和地址 addr 对应的页表目录项(pgd entry)的指针。它通过将 pgd 指针与 pgd_index(addr) 计算得到的索引相加来获得目录项的地址。

    pgd_offset(mm, addr) 宏用于在给定的内存管理结构 mm 中计算地址 addr 对应的页表目录项的指针。它通过调用 pgd_offset_raw 宏,并传递 mm->pgd(页全局目录指针)和地址 addr 来获得目录项的指针。

    如果是给定内核虚拟地址 addr 在页表目录中的索引等价于:

    /*
     * a shortcut which implies the use of the kernel's pgd, instead
     * of a process's
     */
    #define pgd_offset_k(address) pgd_offset(&init_mm, (address))
    
    • 1
    • 2
    • 3
    • 4
    • 5

    1.2 pud_offset

    pud_offset用于在一级页表(first-level page table)中查找条目(entry)

    /* Find an entry in the frst-level page table. */
    #define pud_index(addr)		(((addr) >> PUD_SHIFT) & (PTRS_PER_PUD - 1))
    
    #define pud_offset_phys(dir, addr)	(pgd_page_paddr(READ_ONCE(*(dir))) + pud_index(addr) * sizeof(pud_t))
    
    #define pud_offset(dir, addr)		((pud_t *)__va(pud_offset_phys((dir), (addr))))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    pud_index(addr) 宏用于计算给定地址 addr 在一级页表中的索引。它通过将地址右移 PUD_SHIFT 位,然后与 (PTRS_PER_PUD - 1) 进行按位与运算来计算索引值。

    pud_offset_phys(dir, addr) 宏用于计算给定一级页表指针 dir 和地址 addr 对应的页上一级目录项(pud entry)的物理地址。它通过先读取 dir 指针指向的页全局目录项(pgd entry),然后获取其物理地址,再加上 pud_index(addr) 乘以 sizeof(pud_t) 的偏移量来计算目录项的物理地址。

    pud_offset(dir, addr) 宏用于在给定的一级页表指针 dir 中计算地址 addr 对应的页上一级目录项的指针。它通过调用 pud_offset_phys 宏,并传递 dir 和地址 addr 来获得目录项的物理地址,并使用 __va 宏将其转换为虚拟地址,然后将其强制转换为 pud_t 类型的指针。

    1.3 pmd_offset

    pmd_offset用于在二级页表(second-level page table)中查找条目(entry)

    /* Find an entry in the second-level page table. */
    #define pmd_index(addr)		(((addr) >> PMD_SHIFT) & (PTRS_PER_PMD - 1))
    
    #define pmd_offset_phys(dir, addr)	(pud_page_paddr(READ_ONCE(*(dir))) + pmd_index(addr) * sizeof(pmd_t))
    
    #define pmd_offset(dir, addr)		((pmd_t *)__va(pmd_offset_phys((dir), (addr))))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    pmd_index(addr) 宏用于计算给定地址 addr 在二级页表中的索引。它通过将地址右移 PMD_SHIFT 位,然后与 (PTRS_PER_PMD - 1) 进行按位与运算来计算索引值。

    pmd_offset_phys(dir, addr) 宏用于计算给定二级页表指针 dir 和地址 addr 对应的页中一级目录项(pmd entry)的物理地址。它通过先读取 dir 指针指向的页上一级目录项(pud entry),然后获取其物理地址,再加上 pmd_index(addr) 乘以 sizeof(pmd_t) 的偏移量来计算目录项的物理地址。

    pmd_offset(dir, addr) 宏用于在给定的二级页表指针 dir 中计算地址 addr 对应的页中一级目录项的指针。它通过调用 pmd_offset_phys 宏,并传递 dir 和地址 addr 来获得目录项的物理地址,并使用 __va 宏将其转换为虚拟地址,然后将其强制转换为 pmd_t 类型的指针。

    1.4 pte_offset_kernel

    pte_offset_kernel用于在三级页表(third-level page table)中查找条目(entry)

    /* Find an entry in the third-level page table. */
    #define pte_index(addr)		(((addr) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))
    
    #define pte_offset_phys(dir,addr)	(pmd_page_paddr(READ_ONCE(*(dir))) + pte_index(addr) * sizeof(pte_t))
    
    #define pte_offset_kernel(dir,addr)	((pte_t *)__va(pte_offset_phys((dir), (addr))))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    pte_index(addr) 宏用于计算给定地址 addr 在三级页表中的索引。它通过将地址右移 PAGE_SHIFT 位,然后与 (PTRS_PER_PTE - 1) 进行按位与运算来计算索引值。

    pte_offset_phys(dir, addr) 宏用于计算给定三级页表指针 dir 和地址 addr 对应的物理页表项(pte entry)的物理地址。它通过先读取 dir 指针指向的页中一级目录项(pmd entry),然后获取其物理地址,再加上 pte_index(addr) 乘以 sizeof(pte_t) 的偏移量来计算页表项的物理地址。

    pte_offset_kernel(dir, addr) 宏用于在给定的三级页表指针 dir 中计算地址 addr 对应的物理页表项的指针。它通过调用 pte_offset_phys 宏,并传递 dir 和地址 addr 来获得页表项的物理地址,并使用 __va 宏将其转换为虚拟地址,然后将其强制转换为 pte_t 类型的指针。

    有一个等价的宏:

    #define pte_offset_map(dir,addr)	pte_offset_kernel((dir), (addr))
    
    • 1

    二、修改pte属性

    2.1 set/clear_pte_bit

    (1)硬件页表项描述符

    // arch/arm64/include/asm/pgtable-hwdef.h
    
    /*
     * Level 3 descriptor (PTE).
     */
    #define PTE_VALID		(_AT(pteval_t, 1) << 0)
    #define PTE_TYPE_MASK		(_AT(pteval_t, 3) << 0)
    #define PTE_TYPE_PAGE		(_AT(pteval_t, 3) << 0)
    #define PTE_TABLE_BIT		(_AT(pteval_t, 1) << 1)
    #define PTE_USER		(_AT(pteval_t, 1) << 6)		/* AP[1] */
    #define PTE_RDONLY		(_AT(pteval_t, 1) << 7)		/* AP[2] */
    #define PTE_SHARED		(_AT(pteval_t, 3) << 8)		/* SH[1:0], inner shareable */
    #define PTE_AF			(_AT(pteval_t, 1) << 10)	/* Access Flag */
    #define PTE_NG			(_AT(pteval_t, 1) << 11)	/* nG */
    #define PTE_DBM			(_AT(pteval_t, 1) << 51)	/* Dirty Bit Management */
    #define PTE_CONT		(_AT(pteval_t, 1) << 52)	/* Contiguous range */
    #define PTE_PXN			(_AT(pteval_t, 1) << 53)	/* Privileged XN */
    #define PTE_UXN			(_AT(pteval_t, 1) << 54)	/* User XN */
    #define PTE_HYP_XN		(_AT(pteval_t, 1) << 54)	/* HYP XN */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    (2)bit55-58是硬件预留给软件使用:

    // arch/arm64/include/asm/pgtable-prot.h
    
    /*
     * Software defined PTE bits definition.
     */
    #define PTE_WRITE		(PTE_DBM)		 /* same as DBM (51) */
    #define PTE_DIRTY		(_AT(pteval_t, 1) << 55)
    #define PTE_SPECIAL		(_AT(pteval_t, 1) << 56)
    #define PTE_DEVMAP		(_AT(pteval_t, 1) << 57)
    #define PTE_PROT_NONE		(_AT(pteval_t, 1) << 58) /* only when !PTE_VALID */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    (3)

    typedef u64 pteval_t;
    
    typedef struct { pteval_t pgprot; } pgprot_t;
    
    #define pgprot_val(x)	((x).pgprot)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    static inline pte_t clear_pte_bit(pte_t pte, pgprot_t prot)
    {
    	pte_val(pte) &= ~pgprot_val(prot);
    	return pte;
    }
    
    static inline pte_t set_pte_bit(pte_t pte, pgprot_t prot)
    {
    	pte_val(pte) |= pgprot_val(prot);
    	return pte;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    两个内联函数,用于清除和设置页表项(page table entry)中的位:

    clear_pte_bit 函数用于清除页表项 pte 中通过 pgprot_t 类型的 prot 参数指定的位。它通过对 pte 的值执行按位取反操作,然后与 prot 的值执行按位与操作,最后将结果返回。

    set_pte_bit 函数用于设置页表项 pte 中通过 pgprot_t 类型的 prot 参数指定的位。它通过对 pte 的值执行按位或操作,使用 prot 的值作为掩码,将指定位设置为 1,最后将结果返回。

    2.2 pte_wrprotect

    static inline pte_t pte_wrprotect(pte_t pte)
    {
    	pte = clear_pte_bit(pte, __pgprot(PTE_WRITE));
    	pte = set_pte_bit(pte, __pgprot(PTE_RDONLY));
    	return pte;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    用于修改页表项的权限,将其设置为只读状态,以实现对页面的写保护。通过调用 clear_pte_bit 和 set_pte_bit 函数,可以方便地进行位操作,从而修改页表项的属性。

    2.3 pte_mkwrite

    static inline pte_t pte_mkwrite(pte_t pte)
    {
    	pte = set_pte_bit(pte, __pgprot(PTE_WRITE));
    	pte = clear_pte_bit(pte, __pgprot(PTE_RDONLY));
    	return pte;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    用于修改页表项的权限,将其设置为可写状态,以实现对页面的写入操作。通过调用 set_pte_bit 和 clear_pte_bit 函数,可以方便地进行位操作,从而修改页表项的属性。

    2.4 pte_mkclean

    static inline pte_t pte_mkclean(pte_t pte)
    {
    	pte = clear_pte_bit(pte, __pgprot(PTE_DIRTY));
    	pte = set_pte_bit(pte, __pgprot(PTE_RDONLY));
    
    	return pte;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    用于修改页表项的属性,将其标记为干净并设置为只读状态。通过调用 clear_pte_bit 和 set_pte_bit 函数,可以方便地进行位操作,从而修改页表项的属性。这样可以实现对页面的写保护,并将其标记为干净,以便在需要时进行页面回写操作。

    2.5 pte_mkdirty

    /*
     * The following only work if pte_present(). Undefined behaviour otherwise.
     */
    #define pte_write(pte)		(!!(pte_val(pte) & PTE_WRITE))
    
    • 1
    • 2
    • 3
    • 4

    宏 pte_write(pte),用于检查页表项 pte 是否允许写入操作。
    宏展开后的逻辑如下:
    使用 pte_val(pte) 获取页表项 pte 的值。
    执行位与操作 pte_val(pte) & PTE_WRITE,其中 PTE_WRITE 是一个位掩码,用于表示写权限的位。
    使用双重取反 !! 将结果转换为布尔值,将非零值转换为 true,将零值转换为 false。

    static inline pte_t pte_mkdirty(pte_t pte)
    {
    	pte = set_pte_bit(pte, __pgprot(PTE_DIRTY));
    
    	if (pte_write(pte))
    		pte = clear_pte_bit(pte, __pgprot(PTE_RDONLY));
    
    	return pte;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    用于修改页表项的属性,将其标记为脏并根据需要取消只读状态。通过调用 set_pte_bit 和 clear_pte_bit 函数,可以方便地进行位操作,从而修改页表项的属性。这样可以在页面发生写入操作时标记页面为脏,并根据需要设置为可写状态。

    三、set_pte_at

    static inline void set_pte(pte_t *ptep, pte_t pte)
    {
    	WRITE_ONCE(*ptep, pte);
    
    	/*
    	 * Only if the new pte is valid and kernel, otherwise TLB maintenance
    	 * or update_mmu_cache() have the necessary barriers.
    	 */
    	if (pte_valid_not_user(pte)) {
    		dsb(ishst);
    		isb();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    set_pte,用于在指定地址处设置页表项(pte),该地址由页表项指针(ptep)表示。
    函数的实现如下:
    (1)使用 WRITE_ONCE 宏将 pte 的值写入 ptep 指向的内存位置。WRITE_ONCE 宏通常用于确保写操作不被编译器进行优化或重新排序,从而确保写操作仅执行一次。

    (2)代码接着检查新的 pte 是否有效并且属于内核(而非用户级别的条目)。这通过使用 pte_valid_not_user 宏进行检查。如果条件为真,表示新的 pte 是有效的且与内核相关,则执行以下操作:
    使用 dsb(ishst) 指令确保在 dsb 指令之前的所有内存访问完成后,再启动 dsb 指令之后的任何内存访问。这提供了内存访问顺序的保证。
    使用 isb() 指令确保指令流同步,使得对页表项的任何更改立即生效。

    static inline void set_pte_at(struct mm_struct *mm, unsigned long addr,
    			      pte_t *ptep, pte_t pte)
    {
    	if (pte_present(pte) && pte_user_exec(pte) && !pte_special(pte))
    		__sync_icache_dcache(pte);
    
    	__check_racy_pte_update(mm, ptep, pte);
    
    	set_pte(ptep, pte);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    内联函数 set_pte_at,用于在指定的地址处设置页表项,用于把pte页表项写入硬件页表。

    函数的实现如下:
    (1)首先,通过一系列条件判断语句检查新的页表项 pte 的属性:
    使用 pte_present(pte) 判断页表项是否存在(present)。
    使用 pte_user_exec(pte) 判断页表项是否允许用户执行(user exec)。
    使用 pte_special(pte) 判断页表项是否是特殊类型(special)。

    如果以上条件都满足,则调用 __sync_icache_dcache 函数,执行一些与页表项相关的同步操作。
    (2)接着,调用 __check_racy_pte_update 函数,用于检查潜在的竞态条件(race condition)并处理页表项的更新。
    (3)最后,调用 set_pte 函数,将新的页表项 pte 设置到指定地址处的页表项指针 ptep 上。

    用于设置页表项,并处理一些与页表项更新相关的操作。它执行了一系列检查和同步操作,以确保页表项的正确性和一致性。

    四、__flush_tlb_kernel_pgtable

    /*
     * Used to invalidate the TLB (walk caches) corresponding to intermediate page
     * table levels (pgd/pud/pmd).
     */
    static inline void __flush_tlb_kernel_pgtable(unsigned long kaddr)
    {
    	unsigned long addr = __TLBI_VADDR(kaddr, 0);
    
    	dsb(ishst);
    	__tlbi(vaae1is, addr);
    	dsb(ish);
    	isb();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    内联函数 __flush_tlb_kernel_pgtable,用于刷新与中间页表级别(pgd/pud/pmd)对应的TLB(转换后备缓冲器)。

    函数的实现如下:
    (1)首先,根据内核地址 kaddr 使用 __TLBI_VADDR 宏计算出相应的虚拟地址 addr。

    (2)使用 dsb(ishst) 指令确保在 dsb 指令之前的所有内存访问完成后,再启动 dsb 指令之后的任何内存访问。这提供了内存访问顺序的保证。

    (3)调用 __tlbi 函数,使用 vaae1is 选项刷新与给定虚拟地址 addr 相关联的TLB项。这个操作将使TLB中的映射失效,需要重新进行地址转换。

    (4)使用 dsb(ish) 指令确保在 dsb 指令之前的所有内存访问完成后,再启动 dsb 指令之后的任何内存访问。这提供了内存访问顺序的保证。

    (5)使用 isb() 指令确保指令流同步,使得对TLB的任何更改立即生效。

    这些指令(dsb、__tlbi、isb)用于刷新内核页表的TLB项,确保新的页表映射在地址转换时得到正确处理。TLB是用于加速虚拟地址到物理地址转换的高速缓存,当页表发生变化时,需要刷新TLB以保证正确的地址映射。该函数主要用于内核页表的维护和更新,以确保内存访问的一致性和正确性。

    五、demo

    接下来根据上面的知识来实现arm64平台下系统调用的hook:

    #include 
    #include 
    #include 
    #include 
    #include  
    #include 
    #include 
    #include 
    #include 
    #include 
    
    static pte_t *ptep;
    static struct mm_struct *mm;
    
    static unsigned long *__sys_call_table;
    static unsigned long mkdir_sys_call_addr;
    
    typedef long (*syscall_fn_t)(const struct pt_regs *regs);
    
    
    #ifndef __NR_mkdirat
    #define __NR_mkdirat 34
    #endif
    
    //用于保存原始的 mkdir 系统调用
    static syscall_fn_t orig_mkdir;
    
    asmlinkage long mkdir_hook(const struct pt_regs *regs)
    {
        printk("hook mkdir sys_call\n");
    
        //return orig_mkdir(regs);
        return 0;
    }
    
    static void set_pte_write(void)
    {
    	pte_t pte;
    
    	pte = READ_ONCE(*ptep);
    	
    	//清除pte的可读属性位
    	//设置pte的可写属性位
    	pte = pte_mkwrite(pte);
    	
    	//把pte页表项写入硬件页表钟
    	set_pte_at(mm, mkdir_sys_call_addr, ptep, pte);
    
    	//页表更新 和 TLB 刷新之间保持正确的映射关系
    	//为了保持一致性,必须确保页表的更新和 TLB 的刷新是同步的
    	__flush_tlb_kernel_pgtable(mkdir_sys_call_addr);
    
    }
    
    static void set_pte_rdonly(void)
    {
    	pte_t pte;
    
    	pte = READ_ONCE(*ptep);
    	
    	//清除pte的可写属性位
    	//设置pte的可读属性位
    	pte = pte_wrprotect(pte);
    	
    	set_pte_at(mm, mkdir_sys_call_addr, ptep, pte);
    
    	__flush_tlb_kernel_pgtable(mkdir_sys_call_addr);
    
    }
    	
    //内核模块初始化函数
    static int __init lkm_init(void)
    {
    	pgd_t *pgdp;
    	pud_t *pudp;
    	pmd_t *pmdp;
    
        /* can be directly found in kernel memory */
    	mm = (struct mm_struct *)kallsyms_lookup_name("init_mm");
    	if(mm == NULL)
    	return -1;
    
        __sys_call_table = (unsigned long *)kallsyms_lookup_name("sys_call_table");
        if (!__sys_call_table)
    		return -1;
    
    	mkdir_sys_call_addr = (unsigned long)(__sys_call_table + __NR_mkdirat);
    	
    	pgdp = pgd_offset(mm, mkdir_sys_call_addr);
    	if (pgd_none(READ_ONCE(*pgdp))) {
    		printk(KERN_INFO "failed pgdp");
    		return 0;
    	}
    	
    	pudp = pud_offset(pgdp, mkdir_sys_call_addr);
    	if (pud_none(READ_ONCE(*pudp))) {
    		printk(KERN_INFO "failed pudp");
    		return 0;
    	}
    	
    	pmdp = pmd_offset(pudp, mkdir_sys_call_addr);
    	if (pmd_none(READ_ONCE(*pmdp))) {
    		printk(KERN_INFO "failed pmdp");
    		return 0;
    	}
    	
    	ptep = pte_offset_kernel(pmdp, mkdir_sys_call_addr);
    	if (!pte_valid(READ_ONCE(*ptep))) {
    		printk(KERN_INFO "failed pte");
    		return 0;
    	}
    
        //保存原始的系统调用:mkdir
    	orig_mkdir = (syscall_fn_t)__sys_call_table[__NR_mkdirat];
    
        set_pte_write();
        __sys_call_table[__NR_mkdirat] = (unsigned long)mkdir_hook;
        set_pte_rdonly();
    
        printk("lkm_init\n");
    
    	return 0;
    }
    
    //内核模块退出函数
    static void __exit lkm_exit(void)
    {
    
    	set_pte_write();
        __sys_call_table[__NR_mkdirat] = (unsigned long)orig_mkdir;
        set_pte_rdonly();
    
        printk("lkm_exit\n");
    }
    
    module_init(lkm_init);
    module_exit(lkm_exit);
    
    MODULE_LICENSE("GPL");
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139

    参考资料

    Linux 5.4.18

  • 相关阅读:
    在Net6中使用AutoMapper
    Webpack 5 超详细解读(五)
    多篇《Nature》和《Science》关于马约拉纳费米子的研究论文近日被撤稿
    KKVIEW远程控制: 远程控制APP下载
    C语言 sizeof 函数内部进行计算
    如何加密保护配置文件中的敏感内容(Spring Cloud微服务)
    【echarts】如何将iconfont转换成echart所需的path路径 echarts折线图、柱状图如何设置自定义svg图标
    【代码规范】switch 块级的作用域问题
    智慧应管理信息化 平台建设方案
    利用Bat批处理文件将.resources转换为.resx文件
  • 原文地址:https://blog.csdn.net/weixin_45030965/article/details/132764364