Virtual memory to Physical memory
2018-03-02

Virtual memory to Physical memory

We all know that processes running in Linux acts only in virtual address space. So whenever a process wants to access a data (okay datum) it requests CPU for a virtual address. The CPU intern converts it into physical address and fetches the data. It will be nice to have a program that converts virtual address to physical address, won’t it?

Linux from 2.5.26 provides a proc interface, pagemap that contains information what we want. Each process has its pagemap at /proc/p_id/pagemap. According to the Documentation it is a binary file contains a sequence of 64-bit words. Each word contains information regarding one virtual page for full virtual address space. Among them bits 0-54 (55-bits) represents the address of the physical frame number (PFN). I think that’s all we need. Adding the offset of a variable from virtual page address to the PFN will give us the physical memory address.

WARNING: Don’t try to read the pagemap file directly. cat /proc/self/pagemap or vim /proc/p_id/pagemap is not going to return anytime soon.

We’ll write a small C program and the let’s try to get physical address of a variable used in that C program. As the PFN data will be present only if the data is not moved to swap, lets use mlock() to lock the memory in physical memory.

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
#include <stdio.h>
#include <sys/mman.h> /* for mlock() */
#include <stdlib.h> /* for malloc() */
#include <string.h> /* for memset() */

/* for getpid() */
#include <sys/types.h>
#include <unistd.h>

#define MEM_LENGTH 1024

int main()
{
/* Allocate 1024 bytes in heap */
char *ptr = NULL;
ptr = malloc(MEM_LENGTH);
if (!ptr) {
perror("malloc fails. ");
return -1;
}

/* obtain physical memory */
memset(ptr, 1, MEM_LENGTH);

/* lock the allocated memory in RAM */
mlock(ptr, MEM_LENGTH);

/* print the pid and vaddr. Thus we can work on him */
printf("my pid: %d\n\n", getpid());
printf("virtual address to work: 0x%lx\n", (unsigned long)ptr);

/* make the program to wait for user input */
scanf("%c", &ptr[16]);

return 0;
}


Run the specimen.c program, get its p_id and start the dissection.

1
2
3
4
5
$ gcc specimen.c -o specimen
$ ./specimen
my pid: 11953

virtual address to work: 0x55cd75821260


In a 64-bit machine, virtual address-space is from 0x00 and to 2^64 - 1. First we have to calculate the page offset for the given virtual address [find on which virtual memory page, the address resides]. And multiply that with 8 as each virtual page table has 8-byte information word in the pagemap file.

1
2
3
#define PAGEMAP_LENGTH 8
page_size = getpagesize();
offset = (vaddr page_size) * PAGEMAP_LENGTH;


Open the pagemap file and seek to that offset location.

1
2
pagemap = fopen(filename, "rb");
fseek(pagemap, (unsigned long)offset, SEEK_SET)


Now cursor is on the first byte of 64-bit word containing the information we need. According to the Documentation bits 0-54 represents the physical page frame number (PFN). So read 7-bytes and discard most significant bit.

1
2
fread(&paddr, 1, (PAGEMAP_LENGTH-1), pagemap)
paddr = paddr & 0x7fffffffffffff;


This is the PFN. Add offset of the virtual address from its virtual page base address to the page shifted PFN to get the physical address of the memory.

1
2
3
4
offset = vaddr % page_size;
/* PAGE_SIZE = 1U << PAGE_SHIFT */
while (!((1UL << ++page_shift) & page_size));
paddr = (unsigned long)((unsigned long)paddr << page_shift) + offset;


Here is the complete program.

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
51
52
53
54
55
56
57
58
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

#define PAGE_SHIFT 12
#define PAGEMAP_LENGTH 8

int main(int argc, char **argv)
{
unsigned long vaddr, pid, paddr = 0, offset;
char *endptr;
FILE *pagemap;
char filename[1024] = {0};
int ret = -1;
int page_size, page_shift = -1;

page_size = getpagesize();
pid = strtol(argv[1], &endptr, 10);
vaddr = strtol(argv[2], &endptr, 16);
printf("getting page number of virtual address %lu of process %ld\n",vaddr, pid);

sprintf(filename, "/proc/%ld/pagemap", pid);

printf("opening pagemap %s\n", filename);
pagemap = fopen(filename, "rb");
if (!pagemap) {
perror("can't open file. ");
goto err;
}

offset = (vaddr / page_size) * PAGEMAP_LENGTH;
printf("moving to %ld\n", offset);
if (fseek(pagemap, (unsigned long)offset, SEEK_SET) != 0) {
perror("fseek failed. ");
goto err;
}

if (fread(&paddr, 1, (PAGEMAP_LENGTH-1), pagemap) < (PAGEMAP_LENGTH-1)) {
perror("fread fails. ");
goto err;
}
paddr = paddr & 0x7fffffffffffff;
printf("physical frame address is 0x%lx\n", paddr);

offset = vaddr % page_size;

/* PAGE_SIZE = 1U << PAGE_SHIFT */
while (!((1UL << ++page_shift) & page_size));

paddr = (unsigned long)((unsigned long)paddr << page_shift) + offset;
printf("physical address is 0x%lx\n", paddr);

ret = 0;
err:
fclose(pagemap);
return ret;
}


And the output

1
2
3
4
5
6
$ sudo ./a.out 11953 0x55cd75821260
getting page number of virtual address 94340928115296 of process 11953
opening pagemap /proc/11953/pagemap
moving to 184259625224
physical frame address is 0x20508
physical address is 0x20508260


References

  1. https://www.kernel.org/doc/Documentation/vm/pagemap.txt
  2. https://elixir.bootlin.com/linux/latest/source/arch/x86/include/asm/page_types.h
  3. man pages