仮想アドレスから物理アドレスを求める

××な理由で仮想アドレスではなく実際の物理アドレスを求めたいことがあると思います.

linuxの場合,カーネルからならvirt2phys()等使えますが,ユーザランドからはそのような関数はありません. その代わり, /proc/self/pagemap を参照して解決することができます.

pagemap ファイルは各仮想ページがどの物理ページに対応しているかを保持している特殊ファイルです.一つのエントリの長さが64bitで,一つのエントリが一つの仮想ページの情報を保持しています.物理アドレスを求めたい場合はまず仮想アドレスページ番号をインデックスとしてpagemapのエントリを読み取り,そこから物理ページのアドレスを求めます.物理ページのアドレスが分かればそこにオフセットを足せば実際の物理アドレスが求まります.エントリの63bit目がページがメモリ上にあるかどうかを示すフラグになります.

DPDKにあるrte_mem_virt2phy() という関数を参考に*1以下のような関数で解決できました. http://dpdk.org/browse/dpdk/tree/lib/librte_eal/linuxapp/eal/eal_memory.c?h=v17.05-rc2#n159

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/time.h>
#include <inttypes.h>

#define PFN_MASK_SIZE 8

unsigned long
virt2phy(const void *virtaddr)
{
    unsigned long page, physaddr, virt_pfn;
    off_t offset;
    int page_size = sysconf(_SC_PAGESIZE);
    int fd, retval;

    fd = open("/proc/self/pagemap", O_RDONLY);
    if (fd < 0) {
        printf("error: open");
        return 0;
    }

    virt_pfn = (unsigned long)virtaddr / page_size;
    offset = sizeof(uint64_t) * virt_pfn;
    if (lseek(fd, offset, SEEK_SET) == (off_t) -1) {
        printf("error: lseek\n");
        close(fd);
        return 0;
    }

    retval = read(fd, &page, PFN_MASK_SIZE);
    close(fd);
    if (retval < 0) {
        printf("error: read\n");
        return 0;
    } else if (retval != PFN_MASK_SIZE) {
        printf("error: read2\n");
        return 0;
    }

    if ((page & 0x7fffffffffffffULL) == 0){
        printf("pfn == 0\n");
        printf("page = %016lX\n", page);
        return 0;
    }

    physaddr = ((page & 0x7fffffffffffffULL) * page_size)
        + ((unsigned long)virtaddr % page_size);

    return physaddr;
}

実のところ,最初は以下のstack overflowの回答をみつけ,この関数を試してみたのですが,malloc()で求めたアドレスには正しく動くものの,mmap()やローカル変数のアドレスにはなぜか動きませんでした.fread()が失敗しているようですが何が原因でしょうか..

c - How to find the physical address of a variable from user-space in Linux? - Stack Overflow

*1:というかほぼそのまま