Linux Arm64修改页表项属性

本文演示了在Linux ARM64系统中通过修改页表属性来hook系统调用。详细介绍了获取pte的相关宏,如pgd_offset、pud_offset等;修改pte属性的函数,如set/clear_pte_bit等;还阐述了set_pte_at、__flush_tlb_kernel_pgtable函数的实现及作用,最后给出了arm64平台下系统调用hook的demo。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

在这篇文章中演示了通过 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.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)))

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 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))))

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))))

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))))

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))

二、修改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 */

(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 */

(3)

typedef u64 pteval_t;

typedef struct { pteval_t pgprot; } pgprot_t;

#define pgprot_val(x)	((x).pgprot)
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;
}

两个内联函数,用于清除和设置页表项(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;
}

用于修改页表项的权限,将其设置为只读状态,以实现对页面的写保护。通过调用 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;
}

用于修改页表项的权限,将其设置为可写状态,以实现对页面的写入操作。通过调用 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;
}

用于修改页表项的属性,将其标记为干净并设置为只读状态。通过调用 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))

宏 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;
}

用于修改页表项的属性,将其标记为脏并根据需要取消只读状态。通过调用 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();
	}
}

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);
}

内联函数 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();
}

内联函数 __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 <linux/init.h>
#include <linux/mm.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kallsyms.h> 
#include <linux/syscalls.h>
#include <asm/unistd.h>
#include <asm/ptrace.h>
#include <asm/pgtable.h>
#include <asm/tlbflush.h>

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");

参考资料

Linux 5.4.18

<think>好的,用户之前询问了关于Linux内核中`struct iommu_ops`的初始化过程,现在他问的是如何修改页表的只读属性为可读可写。我需要先理解用户的需求。可能他是在进行内核开发或者系统级编程,需要修改页表权限,比如为了调试、内存管理或者某些特定的硬件操作。 首先,我需要确定用户的操作环境。他提到的是Linux内核5.4.233版本,所以应该基于这个版本来回答问题。修改页表属性通常涉及到底层的内存管理,这需要了解虚拟内存管理、页表结构以及相关的内核函数。 接下来,用户可能需要了解两种修改方式:用户态和内核态。用户态的话,可能需要用到mprotect函数,而内核态则需要直接操作页表。不过用户的问题更可能是在内核层面,因为页表操作通常属于内核权限范围。 需要考虑到不同的架构,比如x86和ARM,它们的页表结构不同。x86使用多级页表,而ARM可能有不同的实现。用户可能需要通用的方法,或者特定架构的指导。但问题中没有指定架构,所以需要给出通用的步骤,同时指出架构差异。 还要注意内核版本的影响,5.4.233的内核中相关函数是否有变化。例如,set_pte_at和pte_mkwrite这些函数是否存在,或者是否有新的API替代。需要确保提供的信息在该版本中有效。 用户可能遇到的问题包括权限检查、刷新TLB、并发修改导致的竞争条件等。需要提醒注意事,比如确保在修改页表时持有正确的锁,处理TLB失效,以及可能的性能影响。 另外,用户可能对为什么需要修改页表权限感兴趣,比如是否为了写时复制(Copy-on-Write),或者内存调试,或者硬件访问。但用户没有说明具体场景,所以需要保持回答的通用性。 需要分步骤说明,用户可能希望清晰的步骤指导,包括代码示例和必要的解释。同时,要强调操作的风险性,因为错误修改页表可能导致系统崩溃或安全漏洞。 可能还需要提到相关的内核配置选,比如是否启用了CONFIG_DEBUG_PAGEALLOC等,这些可能影响页表修改的行为。 最后,总结关键点,确保用户理解操作的重要性和潜在风险,并提供调试方法,比如使用dump_page()来检查页表的内容。</think>在Linux内核中修改页表属性(从只读变为可读可写)需要深入理解内存管理机制,以下是基于5.4.233内核的详细实现指南: --- ### **一、核心概念** 1. **页表层级**: - x86_64架构:PGD → P4D → PUD → PMD → PTE - ARMv8架构:PGD → PUD → PMD → PTE 2. **权限位标志**: ```c // arch/x86/include/asm/pgtable_types.h #define _PAGE_RW BIT(1) // 可写位 #define _PAGE_PRESENT BIT(0) // 存在位 ``` --- ### **二、内核态修改流程** #### **1. 获取页表** ```c static pte_t *get_pte(struct mm_struct *mm, unsigned long addr) { pgd_t *pgd = pgd_offset(mm, addr); // 获取PGD条目 p4d_t *p4d = p4d_offset(pgd, addr); // x86_64特有层级 pud_t *pud = pud_offset(p4d, addr); pmd_t *pmd = pmd_offset(pud, addr); return pte_offset_map(pmd, addr); // 返回PTE指针 } ``` #### **2. 修改权限代码** ```c void set_page_rw(unsigned long addr) { pte_t *pte = get_pte(current->mm, addr); pte_t entry; entry = pte_mkwrite(*pte); // 设置可写位 entry = pte_mkdirty(entry); // 设置脏位 entry = pte_modify(entry, PAGE_ATTRS); // 保持其他属性 set_pte_at(current->mm, addr, pte, entry); flush_tlb_page(addr); // 刷新TLB } ``` #### **3. ARM架构差异处理** ```c // arch/arm64/include/asm/pgtable-prot.h #define PTE_RDONLY (_AT(pteval_t, 0) << 6) // 只读标志位 entry = clear_pte_bit(entry, PTE_RDONLY); // ARM架构特有操作 ``` --- ### **三、用户态修改方法** ```c #include <sys/mman.h> void user_set_rw(void *addr, size_t len) { mprotect(addr, len, PROT_READ | PROT_WRITE); // 系统调用实现 } ``` - **内核实现路径**:`mm/mprotect.c` → `do_mprotect_pkey()` --- ### **四、关键内核函数分析** | 函数 | 作用 | 源码位置 | |------|------|----------| | `pte_mkwrite()` | 设置可写位 | `include/asm-generic/pgtable.h` | | `set_pte_at()` | 原子写PTE | `arch/x86/mm/pgtable.c` | | `flush_tlb_page()` | TLB刷新 | `arch/x86/mm/tlb.c` | --- ### **五、实现注意事** 1. **内存屏障**: ```c smp_wmb(); // 确保页表修改对其他CPU可见 ``` 2. **并发控制**: ```c spin_lock(&mm->page_table_lock); // 获取页表锁 ``` 3. **大页处理**: - 需要检测PMD级别的`_PAGE_PSE`标志位 - 使用`split_huge_page()`分解大页 --- ### **六、调试技巧** 1. **查看页表内容**: ```bash cat /proc/$PID/pagemap | grep -i [目标地址] ``` 2. **内核调试打印**: ```c printk("PTE content: %llx\n", pte_val(*pte)); ``` 3. **QEMU实验环境**: ```bash qemu-system-x86_64 -kernel vmlinuz -append "nokaslr" -s -S ``` --- ### **七、安全风险** 1. **特权提升**:恶意修改内核页表可能导致提权漏洞 2. **内存污染**:错误修改可能破坏内存隔离性 3. **稳定性影响**:错误设置可能触发内核Oops --- ### **八、性能影响评估** | 操作 | 时钟周期消耗 | 说明 | |------|--------------|------| | PTE修改 | 50-100 cycles | 主要消耗在锁操作 | | TLB刷新 | 200-500 cycles | 随TLB大小增加 | | 跨核同步 | 1000+ cycles | 需处理核间中断 | --- 通过这种底层页表操作,可实现以下高级功能: - 实现**写时复制**(Copy-on-Write) - 构建**内存调试工具** - 支持**动态二进制插桩** - 实现**自定义内存保护机制** 建议在开发过程中使用`CONFIG_DEBUG_PAGEALLOC`内核配置选,该功能可以检测非法页表修改
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值