x86 Linux虚拟地址转物理地址
在x86架构的Linux系统中,虚拟地址到物理地址的转换依赖于分页机制。以下是详细的转换步骤和注意事项:
转换原理
分页结构:
- x86使用多级页表(通常为四级:PGD、PUD、PMD、PTE)将虚拟地址映射到物理地址。
- 虚拟地址被划分为多个索引字段,用于遍历各级页表,最终得到物理页框号(PFN)和页内偏移。
地址划分(以64位系统为例):
- 虚拟地址划分为:PGD索引、PUD索引、PMD索引、PTE索引、页内偏移。
- 每级页表项指向下一级表的基地址,最后一级页表项提供PFN。
用户态转换方法
通过读取/proc/[pid]/pagemap
文件获取物理地址:
计算条目位置:
- 虚拟地址的页帧号为
virt_addr / PAGE_SIZE
。
- 在
pagemap
中,每个条目占8字节,偏移量为(virt_addr / PAGE_SIZE) * 8
。
读取并解析条目:
- 检查第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; }
|
内核态转换方法
在内核模块中直接遍历页表:
- 获取页表项:
- 使用
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); }
|
注意事项
权限要求:
- 用户态程序需以root权限运行,且内核需启用
CONFIG_PROC_PAGE_MONITOR
。
大页(Huge Pages):
- 大页会减少页表层级,需检查PMD或PUD层级的大页标志(
pmd_huge()
或pud_huge()
)。
页面状态:
地址空间:
- 用户态方法仅适用于用户空间地址,内核地址需在内核态处理。
总结
- 用户态:通过
/proc/self/pagemap
解析PFN,结合偏移计算物理地址。
- 内核态:遍历页表结构,逐级获取PFN。
- 工具辅助:使用
gdb
、QEMU
调试或内核工具(如crash
)直接查看映射。