3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

BitVisorAdvent Calendar 2016

Day 20

BitVisorからゲストのメモリ領域を参照する

Last updated at Posted at 2016-12-19

BitVisorからゲストのデータを読みたいとか,あるいはBitVisorから何かデータをゲストに送りたいとか,その他諸々の理由でBitVisorからゲストのメモリ領域を参照したいことがあると思います.
今回いくつかの方法についてまとめたいと思います.

なお,以下の文章は簡略化や実験環境等の理由から基本的にIntel 64かつゲストOSとしてGNU/Linuxを仮定しています.ご了承ください.

目次

VMCALLのレジスタ渡しで頑張る

文章冒頭でメモリ領域を参照すると言っておきながらいきなりそれに反する内容ですが,少量のデータのやりとりが目的であればVMCALL(AMDならVMMCALL)によるレジスタ渡しで十分かもしれません.実際dbgshはそういう風に実装されていた気がします.

VMCALLで呼ばれるBitVisorの関数では以下のようにcurrent->vmctrl.read_general_reg()で呼び出し元のレジスタの値が取得できます.また戻り値はraxレジスタに設定します.

#include "current.h"
#include "initfunc.h"
#include "printf.h"
#include "vmmcall.h"

static void
foo(void)
{
    ulong rbx, rcx, rdx;

    current->vmctl.read_general_reg(GENERAL_REG_RBX, &rbx);
    current->vmctl.read_general_reg(GENERAL_REG_RCX, &rcx);
    current->vmctl.read_general_reg(GENERAL_REG_RDX, &rdx);

    printf("rbx = %ld\n", rbx);
    printf("rcx = %ld\n", rcx);
    printf("rdx = %ld\n", rdx);

    current->vmctl.write_general_reg(GENERAL_REG_RAX, (ulong)10);
}

static void
vmmcall_foo_init(void)
{
    vmmcall_register("foo", foo);
}

INITFUNC("vmmcal0", vmmcall_foo_init);

呼び出し側はtools/common/call_vmm.hを利用すれば以下のように簡単にレジスタに値を設定してVMCALLできます.

#include <stdio.h>
#include <stdlib.h>
#include "../common/call_vmm.h"

int
main (int argc, char **argv)
{
    call_vmm_function_t f;
    call_vmm_arg_t a;
    call_vmm_ret_t r;

    CALL_VMM_GET_FUNCTION ("foo", &f);
    if (!call_vmm_function_callable (&f)) {
        fprintf (stderr, "vmmcall \"foo\" failed\n");
        exit (1);
    }

    a.rbx = (long)1;
    a.rcx = (long)2;
    a.rdx = (long)3;
    call_vmm_call_function (&f, &a, &r);
    printf("ret: %ld\n", r.rax);
    return 0;
}

VMCALLについては以下に記事があります

ゲストの物理アドレスを伝える (カーネル編)

ゲストのメモリ領域をBitVisorから参照する方法としては,ゲストの物理アドレスをBitVisorに伝え,それをBitVisorが参照する方法があります.ちなみに,ゲストの物理アドレスとホストの物理アドレスの対応は基本的にpass throughです(BitVisorですしね).ただしゲストがBitVisorが使用している領域にアクセスすると困るので,そこだけROMに偽装しているようです(core/gmm_pass.c:gmm_pass_gp2hp()).

この方法を使用する場合,まずVMCALLの引数として,メモリの物理アドレス及びその長さを渡します.Linuxのカーネル内ではkmalloc()で連続した物理メモリ領域が取得できます.さらに,virt_to_phys()関数を使うことで物理アドレスが取得できます.このとき対象のメモリ領域がスワップアウトされてしまうと困ったことになりますが,kmalloc()で取得したメモリ領域はスワップされないことが保証されているため,安心して使えます.

作成するカーネルモジュールの例(かなり処理を簡単化しています.):

#include <linux/module.h>
#include <linux/io.h>
#include <linux/slab.h>

static int __init
foo_init (void)
{
    ulong phys;
    ulong len = 1024;
    char *buf;
    u32 callnum;

    asm volatile ("vmcall"
                  : "=a" (callnum)
                  : "a" (0), "b" ("foo"));

    if (callnum == 0) {
        printk ("[foo] vmcall foo failed.\n");
        return -EINVAL;
    }

    buf = (char *)kmalloc (len, GFP_KERNEL);

    if (!buf) {
        printk ("foo: kmalloc failed.\n");
        return -ENOMEM;
    }
    buf[0] = 'a'; buf[1] = 'b'; buf[2] = 'c'; buf[3] = '\0';
    phys = virt_to_phys (buf);

    printk("[foo] buf: %s\n",buf);
    asm volatile ("vmcall"
                  :
                  : "a" (callnum), "b" (phys)
                  , "c" (len));

    printk("[foo] buf: %s\n",buf);
    return 0;
}

static void __exit
foo_exit(void)
{
    printk("[foo] exit\n");
}

module_init (foo_init);
module_exit (foo_exit);

VMCALLで呼び出される関数は,以下のようにmapmem_gphys()関数でゲストの物理アドレスをBitVisorから参照することができます.

static void
foo(void)
{
    ulong physaddr, bufsize;
    u8 *buf;

    current->vmctl.read_general_reg(GENERAL_REG_RBX, &physaddr);
    current->vmctl.read_general_reg(GENERAL_REG_RCX, &bufsize);

    buf = mapmem_gphys (physaddr, bufsize, MAPMEM_WRITE);
    if(buf == NULL){
        printf("[vmmcall foo] mapmem_gphys error: %ld, %ld\n", physaddr, bufsize);
        current->vmctl.write_general_reg(GENERAL_REG_RAX, (ulong)1);
        return;
    }

    printf("[vmmcall foo] buf = %s\n", buf);
    buf[0] = 'f'; buf[1] = 'o'; buf[2] = 'o'; buf[3] = '\0';

    unmapmem(buf, bufsize);
    current->vmctl.write_general_reg(GENERAL_REG_RAX, (ulong)0);
}

static void
vmmcall_foo_init(void)
{
    vmmcall_register("foo", foo);
}

INITFUNC("vmmcal0", vmmcall_foo_init);

実行例:

$ cd <モジュールのディレクトリ>
$ make -C /lib/modules/`uname -r`/build SUBDIRS=`pwd` modules
$ sudo insmod foo.ko
$ dmesg | tail -n2
[  953.088991] [foo] buf: abc
[  953.093051] [foo] buf: foo

このようにカーネル内のメモリ領域をBitVisorから参照する方法は,BitVisorのcore/vmmcall_log.cおよびtools/log/が参考になります.

ゲストの物理アドレスを伝える (ユーザランド編)

ユーザランドで物理アドレスを伝える方法は,基本的にはカーネルでやることと同じですが,いくつか問題があります.

  1. virt_to_phys()が使えない
  2. malloc()で確保した領域が物理的に連続とは限らない
  3. 確保したメモリ領域がスワップアウトされる可能性がある

1.については,/proc/self/pagemapを参照することで解決できます.

2.については,これはカーネル内でvmalloc()を利用したときも同様の問題がありますが,対策としては,ページ単位ごとにVMCALLを呼び出すとか,あるいは物理ページのリストをVMCALLで渡し,BitVisor側でページごとに処理するなどの方法があるかと思います.

3.については,Linuxであればmlock(2)を使用できます. 適当にやるならVMCALL直前に対象メモリ領域にアクセスしておけばなんとかなるかもしれません (ずっとその領域をBitVisor側が保持して参照する感じだと問題が起きやすいですかね).

以下,簡単な例を示します.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <inttypes.h>
#include <sys/mman.h>

// http://stackoverflow.com/a/28987409
uintptr_t vtop(uintptr_t vaddr) {
    FILE *pagemap;
    intptr_t paddr = 0;
    int offset = (vaddr / sysconf(_SC_PAGESIZE)) * sizeof(uint64_t);
    uint64_t e;

    // https://www.kernel.org/doc/Documentation/vm/pagemap.txt
    if ((pagemap = fopen("/proc/self/pagemap", "r"))) {
        if (lseek(fileno(pagemap), offset, SEEK_SET) == offset) {
            if (fread(&e, sizeof(uint64_t), 1, pagemap)) {
                if (e & (1ULL << 63)) { // page present ?
                    paddr = e & ((1ULL << 54) - 1); // pfn mask
                    paddr = paddr * sysconf(_SC_PAGESIZE);
                    // add offset within page
                    paddr = paddr | (vaddr & (sysconf(_SC_PAGESIZE) - 1));
                }
            }
        }
        fclose(pagemap);
    }
    return paddr;
}

int main(){
    char *buf;
    unsigned callnum;
    uintptr_t phys;
    unsigned len = sysconf(_SC_PAGESIZE);

    buf = (char *)malloc(len);
    if(buf == NULL){
        printf("malloc failed\n");
        return 1;
    }
    mlock(buf, len);
    phys = vtop((uintptr_t)buf);

    buf[0] = 'a'; buf[1] = 'b'; buf[2] = 'c'; buf[3] = '\0';
    printf("buf: %s\n",buf);

    asm volatile ("vmcall"
                  : "=a" (callnum)
                  : "a" (0), "b" ("foo"));

    if (callnum == 0) {
        printf ("vmcall foo failed.\n");
        return 1;
    }
    asm volatile ("vmcall"
                  :
                  : "a" (callnum), "b" (phys)
                  , "c" (len));

    printf("buf: %s\n",buf);

    munlock(buf, len);
    free(buf);

    return 0;
}

実行例

$ sudo ./call_foo
buf: abc
buf: foo

なお,Linux4.0以降,/proc/self/pagemapのアクセスにはCAP_SYS_ADMINケーパビリティが必要になったようで,要するに実行には実質的に管理者権限が必要です.

ゲストの仮想ドレスを伝える

さて,今までゲスト側で物理アドレスを取得し,それをBitVisorに伝えることでメモリ参照をおこないましたが,実をいうとBitVisorにはゲストの仮想アドレスを自分で解決してメモリの読み書きをする機能があります.

具体的な関数は,core/cpu_mmu.c内の,{read,write}_linearaddr_{b,w,l,q}です.

例:

#include "current.h"
#include "initfunc.h"
#include "printf.h"
#include "vmmcall.h"
#include "mm.h"
#include "cpu_mmu.h"

static void
foo(void)
{
    ulong addr, len;
    int i;
    char c;

    current->vmctl.read_general_reg(GENERAL_REG_RBX, &addr);
    current->vmctl.read_general_reg(GENERAL_REG_RCX, &len);

    for(i = 0; i < len; i++){
        read_linearaddr_b (addr + i, &c);
        printf("[vmmcall foo] %c\n", c);
    }
    write_linearaddr_b (addr + 0, 'f');
    write_linearaddr_b (addr + 1, 'o');
    write_linearaddr_b (addr + 2, 'o');
    write_linearaddr_b (addr + 3, '\0');

    current->vmctl.write_general_reg(GENERAL_REG_RAX, (ulong)0);
}

static void
vmmcall_foo_init(void)
{
    vmmcall_register("foo", foo);
}

INITFUNC("vmmcal0", vmmcall_foo_init);
#include <stdio.h>
#include <stdlib.h>
#include "../common/call_vmm.h"

int main (int argc, char **argv)
{
    call_vmm_function_t f;
    call_vmm_arg_t a;
    call_vmm_ret_t r;

    char buf[] = "abc";
    printf("buf: %s\n",buf);

    CALL_VMM_GET_FUNCTION ("foo", &f);
    if (!call_vmm_function_callable (&f)) {
        fprintf (stderr, "vmmcall \"foo\" failed\n");
        exit (1);
    }
    a.rbx = (unsigned long)buf;
    a.rcx = 3;
    call_vmm_call_function (&f, &a, &r);
    printf("buf: %s\n",buf);
    return 0;
}

(本当はbufに関してmlockしないとスワップアウトされる可能性がありますが,手抜きしてます)

$ gcc -o foo foo.c ../common/call_vmm.c
$ ./foo
buf: abc
buf: foo

read_linearaddr_b関数は,ゲストのCR3レジスタを参照してそこからPTEを読み出し,物理アドレスを求め,それをmapmem()してデータを読み出しています.この関数を利用することでゲストの仮想アドレスを使ってデータの読み書きができますが,原理上1バイト(あるいは2/4/8バイト)のアクセスの度にPTEを解決することになるので,大量のデータのやりとりする際は遅くなるかもしれません.

read_linearaddr_bをちょっと改良すれば効率良く連続した領域をアクセスするようにすることは可能だと思います.どうしてそうした関数がないのかというと,結局のところゲストのページ不在のときどうするかという問題があるので,シンプルな1バイトごとの読み出しのみ実装しているのかなと思いました.

(2016-12-23 追記:)
BitVisorのメーリングリストにコピー高速化のパッチが投げられていたのを発見しました.

[BitVisor-devel:30] BitVisor-copy patch : http://www.bitvisor.org/archives/bitvisor-devel/2012-February/000029.html

このパッチはゲストの仮想アドレスとコピー長を受け取り,ページ単位ごとにアドレス解決してコピーをしているようです.結局ゲストのページ不在対応をどうするかという問題のためマージされていないようですが,高速にゲストからコピーしたい場合にこのパッチは参考になるかもしれません.

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?