x86 Linux虚拟地址转物理地址

在x86架构的Linux系统中,虚拟地址到物理地址的转换依赖于分页机制。以下是详细的转换步骤和注意事项:

转换原理

  1. 分页结构

    • x86使用多级页表(通常为四级:PGD、PUD、PMD、PTE)将虚拟地址映射到物理地址。
    • 虚拟地址被划分为多个索引字段,用于遍历各级页表,最终得到物理页框号(PFN)和页内偏移。
  2. 地址划分(以64位系统为例):

    • 虚拟地址划分为:PGD索引、PUD索引、PMD索引、PTE索引、页内偏移。
    • 每级页表项指向下一级表的基地址,最后一级页表项提供PFN。

用户态转换方法

通过读取/proc/[pid]/pagemap文件获取物理地址:

  1. 计算条目位置

    • 虚拟地址的页帧号为virt_addr / PAGE_SIZE
    • pagemap中,每个条目占8字节,偏移量为(virt_addr / PAGE_SIZE) * 8
  2. 读取并解析条目

    • 检查第63位(页面是否存在于物理内存)。
    • 提取0-54位作为PFN,计算物理地址:物理地址 = PFN * PAGE_SIZE + 页内偏移

示例代码

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
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdint.h>

#define PAGE_SIZE 4096
#define PFN_MASK ((((uint64_t)1) << 55) - 1)

int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s virtual_address\n", argv[0]);
return 1;
}

uintptr_t virt_addr = strtoul(argv[1], NULL, 0);
off_t offset = (virt_addr / PAGE_SIZE) * sizeof(uint64_t);

int fd = open("/proc/self/pagemap", O_RDONLY);
if (fd == -1) {
perror("open");
return 1;
}

if (lseek(fd, offset, SEEK_SET) == -1) {
perror("lseek");
close(fd);
return 1;
}

uint64_t entry;
if (read(fd, &entry, sizeof(entry)) != sizeof(entry)) {
perror("read");
close(fd);
return 1;
}

close(fd);

if ((entry & (1ULL << 63)) == 0) {
printf("Page not present in physical memory.\n");
return 1;
}

uint64_t pfn = entry & PFN_MASK;
uintptr_t phys_addr = (pfn * PAGE_SIZE) + (virt_addr % PAGE_SIZE);

printf("Virtual: 0x%lx → Physical: 0x%lx\n", virt_addr, phys_addr);
return 0;
}

内核态转换方法

在内核模块中直接遍历页表:

  1. 获取页表项
    • 使用pgd_offset()p4d_offset()pud_offset()pmd_offset()pte_offset_map()逐级查找。
    • 从PTE中提取PFN。

示例代码片段

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
#include <linux/mm.h>
#include <linux/module.h>

void get_phys_addr(unsigned long virt_addr) {
pgd_t *pgd;
p4d_t *p4d;
pud_t *pud;
pmd_t *pmd;
pte_t *pte;
struct mm_struct *mm = current->mm;

pgd = pgd_offset(mm, virt_addr);
if (pgd_none(*pgd) || pgd_bad(*pgd)) return;

p4d = p4d_offset(pgd, virt_addr);
if (p4d_none(*p4d) || p4d_bad(*p4d)) return;

pud = pud_offset(p4d, virt_addr);
if (pud_none(*pud) || pud_bad(*pud)) return;

pmd = pmd_offset(pud, virt_addr);
if (pmd_none(*pmd) || pmd_bad(*pmd)) return;

pte = pte_offset_map(pmd, virt_addr);
if (!pte_present(*pte)) return;

unsigned long pfn = pte_pfn(*pte);
unsigned long phys_addr = (pfn << PAGE_SHIFT) | (virt_addr & ~PAGE_MASK);

printk("Physical address: 0x%lx\n", phys_addr);
pte_unmap(pte);
}

注意事项

  1. 权限要求

    • 用户态程序需以root权限运行,且内核需启用CONFIG_PROC_PAGE_MONITOR
  2. 大页(Huge Pages)

    • 大页会减少页表层级,需检查PMD或PUD层级的大页标志(pmd_huge()pud_huge())。
  3. 页面状态

    • 页面可能被换出(不在物理内存),需检查存在位。
  4. 地址空间

    • 用户态方法仅适用于用户空间地址,内核地址需在内核态处理。

总结

  • 用户态:通过/proc/self/pagemap解析PFN,结合偏移计算物理地址。
  • 内核态:遍历页表结构,逐级获取PFN。
  • 工具辅助:使用gdbQEMU调试或内核工具(如crash)直接查看映射。