“/proc/<pid>/pagemap”解构
从官方文件中得到如下信息:
“/proc/<pid>/pagemap”这个文件让用户空间进程发现每个虚拟页映射到哪个物理帧。每个虚拟页包含一个64位值,该64位值包含以下数据(文件fs/proc/task_mmu.c中的pagemap_read可见):
0-54位:物理页帧号(PFN)(如果存在);
0-4位:交换类型(如果交换);
5-54位:交换偏移量;
55位:pte用来标记是否是soft-dirty的;
56位:页专属映射(从4.2版本开始)
57-60位:零
61位:页是file-page或shared-anon(从3.5版本开始)
62位:页交换
63位:页是否存在
如果该页不在swap中,则PFN包含交换文件号和该页在swap中的偏移量的编码。未映射页返回一个空PFN。这允许精确地确定哪些页被映射(或在交换区中),并比较进程之间的映射页。
这个接口的高效用户将使用/proc/pid/maps来确定哪些内存区域实际上是映射的,llseek将跳过未映射的区域。
因此对该文件中所存虚拟页的64位值进行分解,并提取低55即为该虚拟页地址的物理地址值。
代码
关键信息已在代码中以注释形式展示,如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <stdint.h>
#include <string.h>
#define PAGEMAP_ENTRY 8
#define GET_BIT(X,Y) ((X & ((uint64_t)1<<Y)) >> Y)
#define GET_PFN(X) (X & 0x7FFFFFFFFFFFFF)
const int __endian_bit = 1;
#define is_bigendian() ( (*(char*)&__endian_bit) == 0 )
int i, c, pid, status;
unsigned long virt_addr;
uint64_t read_val, file_offset, page_size;
char path_buf [0x100] = {};
FILE * f;
char *end;
int read_pagemap(char * path_buf, unsigned long virt_addr);
int main(int argc, char ** argv){
if(argc != 3){
printf("Argument number is not correct! It must like:\n./VtoP PID VIRTUAL_ADDRESS\n");
return -1;
}
if(!memcmp(argv[1], "self", sizeof("self"))){ //该VtoP进程自身
sprintf(path_buf, "/proc/self/pagemap");
pid = -1;
}
else{ //指定的进程
pid = strtol(argv[1], &end, 10);
if (end == argv[1] || *end != '\0' || pid <= 0){
printf("PID must be a positive number or 'self'\n");
return -1;
}
}
virt_addr = strtoll(argv[2], NULL, 16);
if(pid != -1)
sprintf(path_buf, "/proc/%u/pagemap", pid);
page_size = getpagesize(); //获取页面大小
read_pagemap(path_buf, virt_addr); //读取页面映射内容
return 0;
}
int read_pagemap(char * path_buf, unsigned long virt_addr){
//printf("Big endian? %d\n", is_bigendian());
f = fopen(path_buf, "rb");
if(!f){
printf("Error! Cannot open %s\n", path_buf);
return -1;
}
/*
* 根据用户提供的虚拟内存地址计算该地址在文件中的偏移地址,公式为:
* 文件中偏移地址 = virt_addr偏移的字节数 * pagemap文件中条目的大小
*/
file_offset = virt_addr / page_size * PAGEMAP_ENTRY;
printf("Vaddr: 0x%lx, Page_size: %lld, Entry_size: %d\n", virt_addr, page_size, PAGEMAP_ENTRY);
printf("Reading %s at 0x%llx\n", path_buf, (unsigned long long) file_offset);
status = fseek(f, file_offset, SEEK_SET);
if(status){
perror("Failed to do fseek!");
return -1;
}
errno = 0;
read_val = 0;
unsigned char c_buf[PAGEMAP_ENTRY];
for(i = 0; i < PAGEMAP_ENTRY; i++){
c = getc(f);
if(c == EOF){
printf("\nReached end of the file\n");
return 0;
}
if(is_bigendian()) c_buf[i] = c;
else c_buf[PAGEMAP_ENTRY - i - 1] = c;
printf("[%d]0x%x ", i, c);
}
for(i = 0; i < PAGEMAP_ENTRY; i++){
read_val = (read_val << 8) + c_buf[i];
}
printf("\n");
printf("Result: 0x%llx\n", (unsigned long long) read_val);
/*
* 如果页面不存在,但在交换中,那么PFN包含交换文件编号的编码以及页面在交换中的偏移量。
* 未映射的页返回空的PFN。这允许精确地确定映射(或交换)哪些页,并比较进程之间的映射页。
*/
if(GET_BIT(read_val, 63)) {
uint64_t pfn = GET_PFN(read_val);
printf("PFN: 0x%llx (0x%llx)\n", pfn, pfn * page_size + virt_addr % page_size);
}
else printf("Page not present\n");
if(GET_BIT(read_val, 62)) printf("Page swapped\n");
fclose(f);
return 0;
}
测试
查看进程情况:
查看19238
号进程虚拟地址0x400000
所对应的物理地址(这里使用sudo提权,否则无法得到物理地址),如下:
查看不同虚拟地址的共享库调用的物理地址是否相同。利用cat /proc/<pid>/maps
来查看虚拟地址的内存映射情况,如下:
对于pid=19055
的进程,存在如下的lib函数调用,其虚拟地址如下:
对于pid=19191
的进程,存在如下的lib函数调用,其虚拟地址如下:
观察到两进程虽然都调用了同一个函数库,但是两者的虚拟地址映射的起始位置并不相同。但是由于该库是一个共享库,因此其在内存中的物理地址应该相同,使用编写的程序查看,如下:
观察到对于同一lib函数的调用,尽管虚拟地址不同,但两者的物理地址相同。
参考资料
[1]. https://stackoverflow.com/questions/6284810/proc-pid-pagemaps-and-proc-pid-maps-linux