Linux用户空间/内核空间获取用户空间地址的页表
文章目录
- 前言
- 一、用户空间获取用户空间地址的页表
- 二、内核空间获取用户空间地址的页表
- 三、完整代码
前言
$ cat /etc/os-release
PRETTY_NAME="Ubuntu 24.04.2 LTS"$ uname -r
6.14.0-27-generic
一、用户空间获取用户空间地址的页表
/proc/[pid]/pagemap 文件,它是 Linux 内核暴露出来的一个 虚拟地址到物理页帧号 (PFN) 的映射接口。用来查询某个进程虚拟地址对应的物理页帧号(PFN)以及其他状态信息。
具体请参考:Linux 获取用户空间虚拟地址的物理地址— pagemap文件
代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdint.h>
#include <sys/mman.h>#define PAGE_SHIFT 12
#define PAGE_SIZE (1UL << PAGE_SHIFT)
#define PFN_MASK ((1UL << 55) - 1)int main() {int fd;uint64_t va, pa, page, pfn;unsigned long virt_addr;uint64_t file_offset;// 分配一个页面char *buffer = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);if (buffer == MAP_FAILED) {perror("mmap failed");return 1;}// 写入数据确保页面已分配// 关键操作:触发缺页中断,确保该虚拟页已被分配实际的物理页帧。// 如果不访问内存,Linux 可能不会真正分配物理页(延迟分配),此时 pagemap 中的“present”位可能为 0。buffer[0] = 'A';virt_addr = (unsigned long)buffer;printf("虚拟地址: 0x%lx\n", virt_addr);// 打开 pagemap 文件fd = open("/proc/self/pagemap", O_RDONLY);if (fd < 0) {perror("open pagemap failed");munmap(buffer, PAGE_SIZE);return 1;}// 计算在 pagemap 文件中的偏移量// 每个虚拟页在 pagemap 文件中对应一个 8 字节(64位)条目。// 一个虚拟页对应一个 entry,所以除以 PAGE_SIZE,再乘 8 字节。// 计算该虚拟地址所在页在 pagemap 文件中的偏移量。file_offset = (virt_addr / PAGE_SIZE) * sizeof(uint64_t);//定位到对应条目。if (lseek(fd, file_offset, SEEK_SET) == (off_t)-1) {perror("lseek failed");close(fd);munmap(buffer, PAGE_SIZE);return 1;}// 读取页表项:读取 8 字节数据。if (read(fd, &page, sizeof(uint64_t)) != sizeof(uint64_t)) {perror("read pagemap failed");close(fd);munmap(buffer, PAGE_SIZE);return 1;}close(fd);// 检查页面是否在内存中// bit 63: 是否 presentif ((page & (1UL << 63)) == 0) {printf("页面不在内存中\n");munmap(buffer, PAGE_SIZE);return 1;}// 获取物理帧号 (PFN)// bits 0–54: PFNpfn = page & PFN_MASK;//PFN 左移 PAGE_SHIFT 得到物理页基址//加上虚拟地址页内偏移pa = (pfn << PAGE_SHIFT) | (virt_addr & (PAGE_SIZE - 1));printf("物理地址: 0x%lx\n", pa);printf("PFN: 0x%lx\n", pfn);munmap(buffer, PAGE_SIZE);return 0;
}
这里要用 root 或拥有 CAP_SYS_ADMIN 的进程才能读取 PFN:
$ gcc user_virt_to_phy.c
$ sudo ./a.out
虚拟地址: 0x7d8fa47b3000
物理地址: 0x186f28000
PFN: 0x186f28
二、内核空间获取用户空间地址的页表
内核态获取用户空间地址的页表,获取进程的mm,然后依次遍历页表即可:
task -> mm -> pgd -> p4d -> pud -> pmd -> pte -> pfn -> phys_addr
如下图所示:
代码如下:
// 通过遍历页表获取物理地址
static int virt_to_phys(struct task_struct *task, unsigned long virt_addr,unsigned long *phys_addr, unsigned long *pfn)
{struct mm_struct *mm;pgd_t *pgd;p4d_t *p4d;pud_t *pud;pmd_t *pmd;pte_t *pte;struct page *page;*phys_addr = 0;// 获取进程的 mm_structmm = get_task_mm(task);if (!mm) {printk(KERN_ERR "无法获取进程的内存结构\n");return -EINVAL;}down_read(&mm->mmap_lock);// 遍历页表pgd = pgd_offset(mm, virt_addr);if (pgd_none(*pgd) || pgd_bad(*pgd)) {up_read(&mm->mmap_lock);goto out;}p4d = p4d_offset(pgd, virt_addr);if (p4d_none(*p4d) || p4d_bad(*p4d)) {goto out;}pud = pud_offset(p4d, virt_addr);if (pud_none(*pud) || pud_bad(*pud)) {goto out;}pmd = pmd_offset(pud, virt_addr);if (pmd_none(*pmd) || pmd_bad(*pmd)) {goto out;}pte = pte_offset_map(pmd, virt_addr);if (!pte || pte_none(*pte)) {if (pte) pte_unmap(pte);goto out;}// 检查页面是否在内存中if (!pte_present(*pte)) {pte_unmap(pte);goto out;}// 直接获取 PFN 和物理地址*pfn = pte_pfn(*pte);*phys_addr = (*pfn << PAGE_SHIFT) | (virt_addr & ~PAGE_MASK);pte_unmap(pte);out:up_read(&mm->mmap_lock);mmput(mm);return 0;
}
三、完整代码
接下来写一个代码来验证上述通过两种方式获取的用户空间地址的页表结果一致。
内核模块代码:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/sched.h>
#include <linux/mm.h>
#include <linux/slab.h>
#include <linux/pid.h>
#include <linux/ioctl.h>#include <linux/version.h>
#include <linux/kallsyms.h>#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 7, 0)
#include <linux/kprobes.h>static unsigned long (*kallsyms_lookup_name_sym)(const char *name);static int _kallsyms_lookup_kprobe(struct kprobe *p, struct pt_regs *regs)
{return 0;
}static unsigned long get_kallsyms_func(void)
{struct kprobe probe;int ret;unsigned long addr;memset(&probe, 0, sizeof(probe));probe.pre_handler = _kallsyms_lookup_kprobe;probe.symbol_name = "kallsyms_lookup_name";ret = register_kprobe(&probe);if (ret)return 0;addr = (unsigned long)probe.addr;unregister_kprobe(&probe);return addr;
}static unsigned long generic_kallsyms_lookup_name(const char *name)
{/* singleton */if (!kallsyms_lookup_name_sym) {kallsyms_lookup_name_sym = (void *)get_kallsyms_func();if(!kallsyms_lookup_name_sym)return 0;}return kallsyms_lookup_name_sym(name);
}
#else
unsigned long generic_kallsyms_lookup_name(const char *name)
{return kallsyms_lookup_name(name);
}
#endif#define DEVICE_NAME "virt_to_phys"
#define VIRT_TO_PHYS_IOCTL _IOWR('p', 0x01, struct virt_phys_request)struct virt_phys_request {pid_t pid;unsigned long virt_addr;unsigned long phys_addr;unsigned long pfn;int result;
};static int major_number;
static struct class *virt_phys_class = NULL;
static struct device *virt_phys_device = NULL;pte_t *(*my___pte_offset_map)(pmd_t *pmd, unsigned long addr, pmd_t *pmdvalp);static inline pte_t *my__pte_offset_map(pmd_t *pmd, unsigned long addr,pmd_t *pmdvalp)
{pte_t *pte;__cond_lock(RCU, pte = my___pte_offset_map(pmd, addr, pmdvalp));return pte;
}static inline pte_t *my_pte_offset_map(pmd_t *pmd, unsigned long addr)
{return my__pte_offset_map(pmd, addr, NULL);
}// 通过遍历页表获取物理地址
static int virt_to_phys(struct task_struct *task, unsigned long virt_addr,unsigned long *phys_addr, unsigned long *pfn)
{struct mm_struct *mm;pgd_t *pgd;p4d_t *p4d;pud_t *pud;pmd_t *pmd;pte_t *pte;struct page *page;*phys_addr = 0;// 获取进程的 mm_structmm = get_task_mm(task);if (!mm) {printk(KERN_ERR "无法获取进程的内存结构\n");return -EINVAL;}down_read(&mm->mmap_lock);// 遍历页表pgd = pgd_offset(mm, virt_addr);if (pgd_none(*pgd) || pgd_bad(*pgd)) {up_read(&mm->mmap_lock);goto out;}p4d = p4d_offset(pgd, virt_addr);if (p4d_none(*p4d) || p4d_bad(*p4d)) {goto out;}pud = pud_offset(p4d, virt_addr);if (pud_none(*pud) || pud_bad(*pud)) {goto out;}pmd = pmd_offset(pud, virt_addr);if (pmd_none(*pmd) || pmd_bad(*pmd)) {goto out;}pte = my_pte_offset_map(pmd, virt_addr);if (!pte || pte_none(*pte)) {if (pte) pte_unmap(pte);goto out;}// 检查页面是否在内存中if (!pte_present(*pte)) {pte_unmap(pte);goto out;}// 直接获取 PFN 和物理地址*pfn = pte_pfn(*pte);*phys_addr = (*pfn << PAGE_SHIFT) | (virt_addr & ~PAGE_MASK);// 也可以通过 page 结构获取(备用方法)page = pte_page(*pte);if (page) {unsigned long phys_from_page = page_to_phys(page) | (virt_addr & ~PAGE_MASK);unsigned long pfn_from_page = page_to_pfn(page);printk(KERN_INFO "通过 PTE 获取 - PFN: 0x%lx, 物理地址: 0x%lx\n", *pfn, *phys_addr);printk(KERN_INFO "通过 PAGE 获取 - PFN: 0x%lx, 物理地址: 0x%lx\n", pfn_from_page, phys_from_page);// 验证两种方法结果是否一致if (*pfn != pfn_from_page) {printk(KERN_WARNING "PFN 获取方式不一致: PTE=0x%lx, PAGE=0x%lx\n", *pfn, pfn_from_page);}}pte_unmap(pte);out:up_read(&mm->mmap_lock);mmput(mm);return 0;
}static long virt_phys_ioctl(struct file *file, unsigned int cmd, unsigned long arg) {struct virt_phys_request req;struct task_struct *task;int ret = 0;if (copy_from_user(&req, (void __user *)arg, sizeof(req))) {return -EFAULT;}printk(KERN_INFO "virt_to_phys: 收到请求 PID=%d, VA=0x%lx\n", req.pid, req.virt_addr);// 根据 PID 查找任务rcu_read_lock();task = pid_task(find_vpid(req.pid), PIDTYPE_PID);if (!task) {rcu_read_unlock();printk(KERN_ERR "找不到 PID=%d 的进程\n", req.pid);return -ESRCH;}get_task_struct(task);rcu_read_unlock();// 获取物理地址req.result = virt_to_phys(task, req.virt_addr, &req.phys_addr, &req.pfn);put_task_struct(task);if (req.result == 0) {printk(KERN_INFO "virt_to_phys: 转换成功 PA=0x%lx, PFN = 0x%lx\n", req.phys_addr, req.pfn);} else {printk(KERN_ERR "virt_to_phys: 转换失败 err=%d\n", req.result);}if (copy_to_user((void __user *)arg, &req, sizeof(req))) {return -EFAULT;}return ret;
}static int virt_phys_open(struct inode *inode, struct file *file) {return 0;
}static int virt_phys_release(struct inode *inode, struct file *file) {return 0;
}static struct file_operations fops = {.owner = THIS_MODULE,.open = virt_phys_open,.release = virt_phys_release,.unlocked_ioctl = virt_phys_ioctl,
};static int __init virt_phys_init(void) {my___pte_offset_map = (void *)generic_kallsyms_lookup_name("___pte_offset_map");// 分配主设备号major_number = register_chrdev(0, DEVICE_NAME, &fops);if (major_number < 0) {printk(KERN_ALERT "注册字符设备失败\n");return major_number;}// 创建设备类virt_phys_class = class_create(DEVICE_NAME);if (IS_ERR(virt_phys_class)) {unregister_chrdev(major_number, DEVICE_NAME);printk(KERN_ALERT "创建设备类失败\n");return PTR_ERR(virt_phys_class);}// 创建设备节点virt_phys_device = device_create(virt_phys_class, NULL, MKDEV(major_number, 0), NULL, DEVICE_NAME);if (IS_ERR(virt_phys_device)) {class_destroy(virt_phys_class);unregister_chrdev(major_number, DEVICE_NAME);printk(KERN_ALERT "创建设备节点失败\n");return PTR_ERR(virt_phys_device);}printk("my___pte_offset_map = 0x%lx\n", (unsigned long)my___pte_offset_map);printk(KERN_INFO "virt_to_phys 模块加载成功,主设备号: %d\n", major_number);return 0;
}static void __exit virt_phys_exit(void) {device_destroy(virt_phys_class, MKDEV(major_number, 0));class_destroy(virt_phys_class);unregister_chrdev(major_number, DEVICE_NAME);printk(KERN_INFO "virt_to_phys 模块卸载\n");
}module_init(virt_phys_init);
module_exit(virt_phys_exit);MODULE_LICENSE("GPL");
Makefile:
obj-m += user_virt_to_phy.oall:make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modulesclean:make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
$ make
$ sudo insmod user_virt_to_phy.ko
用户空间代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdint.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <string.h>#define PAGE_SHIFT 12
#define PAGE_SIZE (1UL << PAGE_SHIFT)
#define PFN_MASK ((1UL << 55) - 1)// 定义 ioctl 命令
#define VIRT_TO_PHYS_IOCTL _IOWR('p', 0x01, struct virt_phys_request)struct virt_phys_request {pid_t pid;unsigned long virt_addr;unsigned long phys_addr;unsigned long pfn;int result;
};// 通过 /proc/self/pagemap 获取物理地址
unsigned long get_phys_addr_user(unsigned long virt_addr) {int fd;uint64_t page, pfn;unsigned long file_offset;fd = open("/proc/self/pagemap", O_RDONLY);if (fd < 0) {perror("open pagemap failed");return 0;}file_offset = (virt_addr / PAGE_SIZE) * sizeof(uint64_t);if (lseek(fd, file_offset, SEEK_SET) == (off_t)-1) {perror("lseek failed");close(fd);return 0;}if (read(fd, &page, sizeof(uint64_t)) != sizeof(uint64_t)) {perror("read pagemap failed");close(fd);return 0;}close(fd);if ((page & (1UL << 63)) == 0) {printf("页面不在内存中\n");return 0;}pfn = page & PFN_MASK;return (pfn << PAGE_SHIFT) | (virt_addr & (PAGE_SIZE - 1));
}int main() {int fd;struct virt_phys_request req;unsigned long user_phys_addr, kernel_phys_addr, kernel_pfn;// 分配内存并写入测试数据char *buffer = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);if (buffer == MAP_FAILED) {perror("mmap failed");return 1;}// 写入测试数据strcpy(buffer, "Hello from user space!");printf("Buffer content: %s\n", buffer);unsigned long virt_addr = (unsigned long)buffer;printf("虚拟地址: 0x%lx\n", virt_addr);// 用户空间获取物理地址user_phys_addr = get_phys_addr_user(virt_addr);if (user_phys_addr == 0) {printf("用户空间获取物理地址失败\n");munmap(buffer, PAGE_SIZE);return 1;}printf("用户空间获取的物理地址: 0x%lx, PFN: 0x%lx\n", user_phys_addr, user_phys_addr >> PAGE_SHIFT);// 打开字符设备fd = open("/dev/virt_to_phys", O_RDWR);if (fd < 0) {perror("打开设备失败");munmap(buffer, PAGE_SIZE);return 1;}// 准备请求req.pid = getpid();req.virt_addr = virt_addr;req.phys_addr = 0;req.pfn = 0;req.result = -1;printf("向内核发送请求: PID=%d, VA=0x%lx\n", req.pid, req.virt_addr);// 发送 ioctl 请求if (ioctl(fd, VIRT_TO_PHYS_IOCTL, &req) < 0) {perror("ioctl 失败");close(fd);munmap(buffer, PAGE_SIZE);return 1;}kernel_phys_addr = req.phys_addr;kernel_pfn = req.pfn;printf("内核返回的物理地址: 0x%lx, PFN: 0x%lx\n", kernel_phys_addr, kernel_pfn);printf("内核返回结果: %d\n", req.result);// 验证结果if (req.result == 0) {if (user_phys_addr == kernel_phys_addr) {printf("✓ 验证成功:用户空间和内核获取的物理地址一致!\n");printf("✓ 内核空间: 0x%lx (PFN: 0x%lx)\n", kernel_phys_addr, kernel_pfn);} else {printf("✗ 验证失败:地址不一致!\n");printf(" 用户空间: 0x%lx (PFN: 0x%lx)\n", user_phys_addr, user_phys_addr >> PAGE_SHIFT);printf(" 内核空间: 0x%lx (PFN: 0x%lx)\n", kernel_phys_addr, kernel_pfn);}}// 阻塞等待,保持页面在内存中printf("进程进入阻塞状态,按回车键退出...\n");getchar();close(fd);munmap(buffer, PAGE_SIZE);return 0;
}
$ gcc user_virt_to_phy1.c
$ sudo ./a.out
Buffer content: Hello from user space!
虚拟地址: 0x753603629000
用户空间获取的物理地址: 0x20672d000, PFN: 0x20672d
向内核发送请求: PID=7044, VA=0x753603629000
内核返回的物理地址: 0x20672d000, PFN: 0x20672d
内核返回结果: 0
✓ 验证成功:用户空间和内核获取的物理地址一致!
✓ 内核空间: 0x20672d000 (PFN: 0x20672d)
进程进入阻塞状态,按回车键退出...
可以看到用户空间和内核获取的物理地址一致。
查看内核打印的结果:
$ sudo dmesg -c
[ 5116.621968] virt_to_phys: 收到请求 PID=7044, VA=0x753603629000
[ 5116.621973] 通过 PTE 获取 - PFN: 0x20672d, 物理地址: 0x20672d000
[ 5116.621974] 通过 PAGE 获取 - PFN: 0x20672d, 物理地址: 0x20672d000
[ 5116.621975] virt_to_phys: 转换成功 PA=0x20672d000, PFN = 0x20672d