当前位置: 首页 > article >正文

Linux ARM64 将内核虚拟地址转化为物理地址

文章目录

  • 前言
  • 一、通用方案
    • 1.1 kern_addr_valid
    • 1.2 __pa
  • 二、ARM64架构
    • 2.1 AT S1E1R
    • 2.2 is_kernel_addr_vaild
    • 2.3 va2pa_helper
  • 三、demo演示
  • 参考资料

前言

本文介绍一种通用的将内核虚拟地址转化为物理地址的方案以及一种适用于ARM64 将内核虚拟地址转化为物理地址的方案,两种方案都可以。

一、通用方案

1.1 kern_addr_valid

int kern_addr_valid(unsigned long addr)
{
	pgd_t *pgdp;
	pud_t *pudp, pud;
	pmd_t *pmdp, pmd;
	pte_t *ptep, pte;

	if ((((long)addr) >> VA_BITS) != -1UL)
		return 0;

	pgdp = pgd_offset_k(addr);
	if (pgd_none(READ_ONCE(*pgdp)))
		return 0;

	pudp = pud_offset(pgdp, addr);
	pud = READ_ONCE(*pudp);
	if (pud_none(pud))
		return 0;

	if (pud_sect(pud))
		return pfn_valid(pud_pfn(pud));

	pmdp = pmd_offset(pudp, addr);
	pmd = READ_ONCE(*pmdp);
	if (pmd_none(pmd))
		return 0;

	if (pmd_sect(pmd))
		return pfn_valid(pmd_pfn(pmd));

	ptep = pte_offset_kernel(pmdp, addr);
	pte = READ_ONCE(*ptep);
	if (pte_none(pte))
		return 0;

	return pfn_valid(pte_pfn(pte));
}

kern_addr_valid用于检测内核虚拟地址是否有效,适用于x86架构和arm64架构。

1.2 __pa

__pa用于将内核虚拟地址转化为物理地址,适用于x86架构和arm64架构。

对于arm64架构:

// v5.4/source/arch/arm64/include/asm/memory.h

#define __pa(x)			__virt_to_phys((unsigned long)(x))
// v5.4/source/arch/arm64/include/asm/memory.h

#define __virt_to_phys(x)	__virt_to_phys_nodebug(x)
// v5.4/source/arch/arm64/include/asm/memory.h

#define __virt_to_phys_nodebug(x) ({					\
	phys_addr_t __x = (phys_addr_t)(__tag_reset(x));		\
	__is_lm_address(__x) ? __lm_to_phys(__x) : __kimg_to_phys(__x);	\
})

二、ARM64架构

2.1 AT S1E1R

ARM64架构将内核虚拟地址转化为物理地址主要是采用ARM64架构的 at s1e1r 指令。
在这里插入图片描述
在这里插入图片描述

at s1e1r

AT S1E1R 是 ARM64 架构中的一条系统指令,用于执行 Stage 1 地址翻译,并模拟从 EL1(Exception Level 1,通常是操作系统内核)读取给定虚拟地址的权限。

执行 Stage 1 地址翻译,并根据翻译结果更新 PAR_EL1(Physical Address Register)寄存器。

指令类型:
64 位系统指令。

操作数:
一个 64 位虚拟地址。

结果存储:
翻译结果(物理地址和状态信息)存储到 PAR_EL1 寄存器中。
PAR_EL1 寄存器:
在这里插入图片描述
bit0:Fault 标志位(F bit)。如果为 1,表示翻译失败;如果为 0,表示翻译成功。

2.2 is_kernel_addr_vaild

static bool is_kernel_addr_vaild(unsigned long addr)
{
	unsigned long flags;
	u64 par;

	local_irq_save(flags);
	asm volatile("at s1e1r, %0" :: "r" (addr));
	isb();
	par = read_sysreg(par_el1);
	local_irq_restore(flags);

	/*
	 * If we now have a valid translation, treat the translation fault as
	 * spurious.
	 */
	if (par & SYS_PAR_EL1_F)
		return false;

    return true;
}

该函数用于检查给定的内核虚拟地址是否有效。它通过 ARM64 的 AT(Address Translate)指令和 PAR_EL1(Physical Address Register)寄存器来完成地址翻译,并根据翻译结果判断地址的有效性。

代码说明:
(1)保存当前中断状态并禁用中断,确保接下来的操作不会被中断打断。
(2)使用 AT 指令将虚拟地址 addr 翻译为物理地址,并将结果存储到 PAR_EL1 寄存器中。
s1e1r:表示在 EL1 阶段使用 Stage 1 翻译表进行读取操作。
%0:表示输入操作数 addr(虚拟地址)。
(3)插入指令屏障
插入一条指令屏障(Instruction Synchronization Barrier),确保 AT 指令的结果在读取 PAR_EL1 之前完成。
(4)读取 PAR_EL1 寄存器的值
读取 PAR_EL1 寄存器的值,该寄存器保存了 AT 指令的结果。
(5) 恢复中断状态
(6) 检查翻译结果
SYS_PAR_EL1_F:表示 PAR_EL1 寄存器中的 Fault 标志位。如果该位为 1,表示地址翻译失败(如页表项无效或权限不足)。

2.3 va2pa_helper

typedef uintptr_t vaddr_t;
typedef uint64_t paddr_t;

#define BIT32(nr)		((uint64_t)1 << (nr))  // 生成 32 位掩码
#define BIT64(nr)		((uint64_t)1 << (nr))  // 生成 64 位掩码

#define PAR_F			    BIT32(0) // PAR 寄存器的 Fault 标志位,表示 PAR 寄存器中的 Fault 标志位(第 0 位)。如果该位为 1,表示地址翻译失败。

#define PAR_PA_SHIFT		UL(12) // 物理地址在 PAR 寄存器中的偏移量
#define PAR_PA_MASK		    (BIT64(36) - 1) // 物理地址的掩码(36 位),用于从 PAR 寄存器中提取物理地址。

#define DEFINE_REG_READ_FUNC_(reg, type, asmreg)    \
static inline type read_##reg(void)            \
{                            \
    type val;                    \
                            \
    asm volatile("mrs %0, " #asmreg : "=r" (val));    \
    return val;                    \
}

#define DEFINE_U64_REG_READ_FUNC(reg) \
        DEFINE_REG_READ_FUNC_(reg, uint64_t, reg)

DEFINE_U64_REG_READ_FUNC(par_el1)

// 使用 AT S1E1R 指令将虚拟地址 va 翻译为物理地址,并将结果存储到 PAR_EL1 寄存器中。
static inline void write_at_s1e1r(uint64_t va)
{
	asm volatile ("at	S1E1R, %0" : : "r" (va));
}

//通过 ARM64 的 AT(Address Translate)指令和 PAR_EL1(Physical Address Register)寄存器来完成地址翻译。
bool va2pa_helper(void *va, paddr_t *pa)
{
	paddr_t par = 0;
	paddr_t par_pa_mask = 0;
	bool ret = false;

	write_at_s1e1r((vaddr_t)va);  // 使用 AT 指令翻译虚拟地址
	isb();  // 插入指令屏障,确保 AT 指令完成
	par = read_par_el1();  // 读取 PAR_EL1 寄存器的值
	par_pa_mask = PAR_PA_MASK;

	if (par & PAR_F)  // 检查 Fault 标志位
		goto out;

	// 提取物理地址并合并低 12 位
	*pa = (par & (par_pa_mask << PAR_PA_SHIFT)) |
		((vaddr_t)va & (BIT64(PAR_PA_SHIFT) - 1));

	ret = true;

out:
	return ret;
}

函数逻辑:
(1)使用 AT 指令翻译虚拟地址:
调用 write_at_s1e1r,将虚拟地址 va 翻译为物理地址,并将结果存储到 PAR_EL1 寄存器中。
(2)插入指令屏障:
使用 isb() 确保 AT 指令的结果在读取 PAR_EL1 之前完成。
(3)读取 PAR_EL1 寄存器的值:
调用 read_par_el1,获取地址翻译的结果。
(4)检查 Fault 标志位:
如果 PAR_F 标志位为 1,表示地址翻译失败,直接返回 false。
(5)提取物理地址:
从 PAR_EL1 中提取物理地址的高 36 位(par & (par_pa_mask << PAR_PA_SHIFT))。
将虚拟地址的低 12 位(页内偏移)合并到物理地址中。
(6)返回结果:
如果地址翻译成功,返回 true,并将物理地址存储到 *pa。
如果地址翻译失败,返回 false。

三、demo演示

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/mm_types.h>
#include <linux/mm.h>
#include <linux/gfp.h>

#include <asm/esr.h>
#include <asm/sysreg.h>
#include <asm/system_misc.h>

typedef uintptr_t vaddr_t;
typedef uint64_t paddr_t;

#define BIT32(nr)		((uint64_t)1 << (nr))  // 生成 32 位掩码
#define BIT64(nr)		((uint64_t)1 << (nr))  // 生成 64 位掩码

#define PAR_F			    BIT32(0) // PAR 寄存器的 Fault 标志位,表示 PAR 寄存器中的 Fault 标志位(第 0 位)。如果该位为 1,表示地址翻译失败。

#define PAR_PA_SHIFT		UL(12) // 物理地址在 PAR 寄存器中的偏移量
#define PAR_PA_MASK		    (BIT64(36) - 1) // 物理地址的掩码(36 位),用于从 PAR 寄存器中提取物理地址。

#define DEFINE_REG_READ_FUNC_(reg, type, asmreg)    \
static inline type read_##reg(void)            \
{                            \
    type val;                    \
                            \
    asm volatile("mrs %0, " #asmreg : "=r" (val));    \
    return val;                    \
}

#define DEFINE_U64_REG_READ_FUNC(reg) \
        DEFINE_REG_READ_FUNC_(reg, uint64_t, reg)

DEFINE_U64_REG_READ_FUNC(par_el1)

// 使用 AT S1E1R 指令将虚拟地址 va 翻译为物理地址,并将结果存储到 PAR_EL1 寄存器中。
static inline void write_at_s1e1r(uint64_t va)
{
	asm volatile ("at	S1E1R, %0" : : "r" (va));
}

//通过 ARM64 的 AT(Address Translate)指令和 PAR_EL1(Physical Address Register)寄存器来完成地址翻译。
bool va2pa_helper(void *va, paddr_t *pa)
{
	paddr_t par = 0;
	paddr_t par_pa_mask = 0;
	bool ret = false;

	write_at_s1e1r((vaddr_t)va);  // 使用 AT 指令翻译虚拟地址
	isb();  // 插入指令屏障,确保 AT 指令完成
	par = read_par_el1();  // 读取 PAR_EL1 寄存器的值
	par_pa_mask = PAR_PA_MASK;

	if (par & PAR_F)  // 检查 Fault 标志位
		goto out;

	// 提取物理地址并合并低 12 位
	*pa = (par & (par_pa_mask << PAR_PA_SHIFT)) |
		((vaddr_t)va & (BIT64(PAR_PA_SHIFT) - 1));

	ret = true;

out:
	return ret;
}

static bool is_kernel_addr_vaild(unsigned long addr)
{
	unsigned long flags;
	u64 par;

	local_irq_save(flags);
	asm volatile("at s1e1r, %0" :: "r" (addr));
	isb();
	par = read_sysreg(par_el1);
	local_irq_restore(flags);

	/*
	 * If we now have a valid translation, treat the translation fault as
	 * spurious.
	 */
	if (par & SYS_PAR_EL1_F)
		return false;

    return true;
}

//内核模块初始化函数
static int __init lkm_init(void)
{
    unsigned long virt_address, phys_address, phys_address2;

    //调用伙伴系统接口分配 2^2 = 4 个连续的物理页,返回其内核虚拟起始地址
    virt_address = __get_free_pages(GFP_KERNEL, 2);
    phys_address = __pa(virt_address);

    if(is_kernel_addr_vaild(virt_address)){
        printk("kernel address 0x%lx is valid\n", virt_address);
    }else{
        printk("kernel address 0x%lx is not invalid\n", virt_address);
        goto out;
    }

    printk("virtual addr = 0x%lx, phys address = 0x%lx\n", virt_address, phys_address);

    if(va2pa_helper((void *)virt_address, (paddr_t *)&phys_address2)){
        printk("virtual address 0x%lx -> physical address 0x%lx\n", virt_address, phys_address2);
    }else {
        printk("Failed to translate virtual address to physical address\n");
    } 

out:
    free_pages(virt_address, 2);
    return 0;
}

//内核模块退出函数
static void __exit lkm_exit(void)
{
	printk("Goodbye\n");
}


module_init(lkm_init);
module_exit(lkm_exit);

MODULE_LICENSE("GPL");
# insmod va2pa.ko
# dmesg -c
[7816428.962021] kernel address 0xffffff9fe7bd8000 is valid
[7816428.962024] virtual addr = 0xffffff9fe7bd8000, phys address = 0x2067bd8000
[7816428.962025] virtual address 0xffffff9fe7bd8000 -> physical address 0x2067bd8000

可以看到使用 __pa 和 使用 AT 指令将虚拟地址 转化为物理地址结果相同。

参考资料

https://www.cnblogs.com/smilingsusu/p/14600585.html


http://www.kler.cn/a/541737.html

相关文章:

  • STM32F103C8----外部中断探秘:解锁嵌入式实时响应的关键
  • JavaEE-前端与后台的搭建
  • 如何将网站提交百度收录完整SEO教程
  • 【AI学习】关于 DeepSeek-R1的几个流程图
  • 【大数据安全分析】大数据安全分析技术框架与关键技术
  • 操作教程丨使用1Panel开源面板快速部署DeepSeek-R1
  • PAT乙级真题 — 1078 字符串压缩与解压(java)
  • 力扣-栈与队列-347 前k个高频元素
  • Web3 的虚实融合之路:从虚拟交互到元宇宙构建
  • DeepSeek 助力 Vue 开发:打造丝滑的进度条
  • 【天梯赛】L1-104 九宫格(C++)
  • 10苍穹外卖之Task、WebSocket(音频是前端实现)
  • 【练习】图论
  • 实验8 配置标准访问控制列表IPv4 ACL
  • 易语言文件分析工具
  • 检测网络安全漏洞 工具 网络安全 漏洞扫描 实验
  • 预训练语言模型:从BERT到GPT,NLP的新纪元
  • flask如何进行测试
  • Anaconda Navigator 与 Conda:GUI 和 CLI 的对比与使用
  • 【Python】Anaconda安装
  • 《DeepSeek+Langchain落地实操:RAG知识增强检索和智能体实战开发》
  • redis之事件
  • Qlabel 每五个一换行 并、号分割
  • 超详细的数据结构3(初阶C语言版)栈和队列。
  • ping的网络协议,ping用到的网络协议!
  • Oracle ORA-00054