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/
が参考になります.
ゲストの物理アドレスを伝える (ユーザランド編)
ユーザランドで物理アドレスを伝える方法は,基本的にはカーネルでやることと同じですが,いくつか問題があります.
-
virt_to_phys()
が使えない -
malloc()
で確保した領域が物理的に連続とは限らない - 確保したメモリ領域がスワップアウトされる可能性がある
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
このパッチはゲストの仮想アドレスとコピー長を受け取り,ページ単位ごとにアドレス解決してコピーをしているようです.結局ゲストのページ不在対応をどうするかという問題のためマージされていないようですが,高速にゲストからコピーしたい場合にこのパッチは参考になるかもしれません.