[zz] 从VMM中识别GUEST OS中的用户进程

从 VMM 中识别 GUEST OS 中的用户进程

康华 :主要从事 Linux 操作系统内核、虚拟机、 Linux 技术标准、计算机安全、软件测试等领域的研究与开发工作,曾就职 MII-HP 软件实验室 、瞬联软件公司 /MOTOROLA 、 LENOVO 研究院 。其所合写的 Linux 专栏见http://www.csdn.net/subject/linux/ 。   如果需要可以联系通过 kanghua151@msn.com ( MSN )联系他 . 

 

摘要 : 本文给出了一种从 VMM(virtual machine monitor) 中根据截获的硬件访问信息和 GUEST OS 的进程管理信息,在系统运行时自动识别 GUEST OS 中运行进程的方法——该方法不需要 GUEST OS 做任何修改或者安装任何软件。其意义在于将VMM 的监控粒度从系统级 , 提高到进程级别。我们可以很方便的在此基础上实现很多有趣的功能 ( 见下文 ) 。

 

1. 背景介绍

       我的动机来自于一个简单的问题 " 如何从 VMM 中获知 GUEST OS 的负载 ?" , 最直接的办法是在 GUEST OS 安装一个精灵进程 , 不断收集负载信息 , 然后通过共享内存告诉 VMM, 但是如果 GUEST OS 禁止安装任何软件 , 那该怎么办呢 ? 我当时想借助最原始的方法:计算 idle 进程在单位时间内的运行时间 , 从而获得 GUEST OS 的运行负载。所以我需要从VMM 中识别 GUEST OS 的 idle 进程。 沿着该思路 , 我想也许由 VMM 识别 GUEST OS 的进程会对一些管理场景有所帮助, 比如: 1 从 VMM 中监控 GUEST OS 的进程运行状况和资源利用情况; 2 从 VMM 中杀死 GUEST OS 中的运行进程 , 这也许被用于死锁解锁等目的; 3 加固 GUEST OS —— VMM 可限特定进程资源访问权限和范围等 ( 比如只让某个进程访问物理特定内存 ) ; 4 从 VMM 中为 GUEST OS 的进程动态打补丁( patch ) , 在不重启进程或系统的情况下修改 bug 。

 

   

注 : xen 等 VMM 会利用 GUEST OS 执行 idle 进程时调用的 hlt 指令 (hlt 指令可以配置成陷入条件 ) 时间来估计 GUEST OS 的负载 , 当然这么做的前提是 GUEST OS 中 idle 指令会循环调用 hlt 指令。

 

2. 工作原理

    VMM 会利用 GUEST OS 在做进程切换时 ( 需要访问特权积存器 CR3, 以载入新进程的页表基地址 , 在 VT 环境下会造成一个陷入—— vmexit) 的陷入时机和所带硬件信息 , 建立并维护一个 GUEST OS 进程踪迹记录 (GPTR) 的多维向量 , 其中包含 1 GUEST OS 的进程页目录地址 (GPPDA), 其值从 CR3 中获取; 2 GUEST OS 进程的名字或 ID, 其值从 GUEST OS 的进程描述符中获得 , 或者人为指定一个唯一值 ( 如果无法从描述符获得的情况下 ) 。

       有了 GPTR 向量我们就可以在运行时识别 GUEST OS 的运行进程了 , 具体做法是——用被 VMM 捕获的 GUEST OS 当前进程的 GPPDA 做键值 , 在 GPTR 的 GPPDA 记录向量里进行匹配 , 以识别 GUEST OS 中哪个进程在运行。

      注: 寻找GUEST OS 进程描述符号的方法需要根据操作系统而定, 并无固定做法. 比如对于Linux 系统当前进程描述符号的索引和内核堆栈连续存放于2 页或1 页( 根据内核堆栈的编译选项) 内, 所以我们可通过分析内核堆栈指针(RSP 也将在访问CR3 陷入时被VMM 获得) 而定位进程描述符位置, 从而解析出其中的进程ID 等信息( 见下图); 对于windows 系统, 定位当前进程描述符号更为方便。因为当前进程描述符会被放在一个位置固定( 对每种处理器而言)的PRCB(processor control block) 中.

 

定位 Linux 的进程描述符:

movl $0xffffe000,%ecx /* or 0xfffffe000 for 8KB  kernel stacks */    

andl %esp,%ecx

movl (%ecx),p              /* p pointe to current process descriptor */

 

 

 

 

3. 实现概要

我们以Linux 做GUEST OS 为例讲述实现概要。

1.        VMM 在GUEST OS 做进程切换时捕获到 vmexit 。

2.        VMM 从CR3 中获得待运行进程的GPPDA 和栈指针RSP (指向内核堆栈,因为进程切换发生与内核态)。

3.        VMM 通过RSP 找到当前进程的描述符。

4.        VMM 解析当前进程描述符,进程ID (GPRID) 。

5.        VMM 将上次获得的GPPDA 和本次获得的GPRID 作为键值对形式,存储到GPTR 向量中。注意上轮获得的GPPDA 对于上轮来说就是待运行进程,对于本次来说则是当前进程(见下图)。

6.        VMM 执行正常流程。

 

进程切换示意图:

 

 

4. 原形设计与实现

 

为了验证上述方法是否可行,我以 KVM 为 VMM 实现了一个原形加以验证(之所以选择 KVM 是因为 KVM 易于调试,结构清晰;当然你也可以使用 XEN 等 VMM 作平台),该原形中用户可以指定被跟踪的 VM ( virtual machine, 即GUEST OS ) , 并且在运行期获取该 VM 的运行进程信息。当然 GUEST OS 不需要有任何改动,修改的仅仅是 KVM 。原型代码请见< http://sourceforge.net/project/showfiles.php?group_id=200727 >。

   

4.1 kvm 修改部分

主要修改是增加了用户操作接口,以及一个进程跟踪模块(目前该模块仅仅面向 Linux 做 GUEST OS) .

 

1 . 设置了一个 Hook 函数( in handler_cr )—— ToTraceCr3 (vm process id) 来截获并解析 CR3 寄存器,并添加一个注册函数( in vmx.c) 以将 gptrace.ko 中的回调函数挂到 hook 上。

注:当 kvm 处理 GUEST OS 因为 EXIT_REASON_CR_ACCESS 原因而陷入时,回调用 "handler cr" 函数。

typedef int (Cr3TraceFunc)(struct kvm *kvm ,int reg);

Register_cr3_trace(Cr3TraceFunc *func)

{

  Hook_ToTraceCr3 = func;

}

EXPORT_SYMBOL_GPL(Register_cr3_trace);

 

2 . 在 kvm 结构中增加了 vm_id 域 ( 即承载 GUEST OS 运行的 Qemu 进程的 pid) ,目的是为了能识别VM( virtual machine ,即 GUEST OS )。

3 . 在 kvm 结构中增加了 trace_enable 标识 (vm trace enable) ,目的是为了打开或关闭VM跟踪。

4 . 在 kvm 结构中增加一个 opaque pointer 域,目的是为了存储 gptrv 结构 ( 见下文 ) 。

5 . 增加一些 ioctl 处理项

KVM_ENABLE_VM_TRACE (ioctl for /dev/kvm)

KVM_GET_VM_GPTRV(ioctl for /dev/kvm)

KVM_SET_VMID(for vcpu fd)

 

4.2 主要数据结构

 

最重要的数据结构是 "struct gptrv" ,每个 VM 都会维护一个该结构数据,用来存储进程跟踪记录 "guest process trace record vector"

 

struct gptrv                             //GUEST process trace record vector

   Struct gptritem{

   unsigned long gpptaddr;               //GUEST process page table directory address

   unsigned long gpdaddr;        //GUEST process descriptor address

   int gpid;                            //GUEST process id

   char gpname[30];                    //GUEST process's name

   __64 begin_time;              // first record time

   __64 last_time;               // last record time

}gptr[MAX_TRACE_NUMBER];           //how many process that can be record

int last;                          //last time running process

int curr;                           //current running process

}

 

另外一个需要解释的地方是 PID_OFFSE/COMM_OFFSET 这些宏 , 它们表示的是 pid/comm 域在进程描述符表中的偏移.我们解析 GUEST OS 的进程 id 和名称需要找到并读取这些值。不过要注意由于不同版本的 Linux 内核进程描述符表结构有变化 , 其中 pid/comm 的偏移不尽相同。 ( 今后我会寻求一个能自动探测并获取 pid/comm 等域的方法 )

 

4.3 操作和接口

1 .在VM 启动时设置VM process id 到KVM 结构中 (interface)

   利用KVM_SET_VMID ioctl, 在VM 启动时将qemu 进程的id 写入对应的kvm 结构。具体实现在kvm_qemu_create_context 中:ioctl(kvm_context->vm_fd, KVM_SET_VMID ,getpid())

2 .打开/ 关闭跟踪VM 功能(interface)

   利用 KVM_ENABLE_VM_TRACE ioctl, enable trace ==0 是关闭,1 则相反。

3 .获取VM 的运行进程信息(Interface)

利用KVM_GET_VM_GPTRV ioctl 获取给定VM 所维护的 进程跟踪记录.

4 .杀死VM 中的给定进程(interface)  -- 下篇文章中介绍.

5 .跟踪进程切换——主要功能模块,其算法描述如下:

   检查是否VM 的trace enable flag 被设置

      IF No : return;

   检查是否VM 的opaque 指针是NULL

      IF No : 创建gptrv 结构.

   检查是否CR3 值是否已经被记录在GPTR 向量中

      IF Yes :

更新last index.

          更新timestamp

          记录gpdaddr 到last 进程跟踪记录中。

          记录gpid/gpname 到last 进程跟踪记录中。

          更新last index 。

      IF No : 

          记录gpptaddr 到curr 进程跟踪记录中, 并更新其 timestamp

          记录gpdaddr 到last 进程跟踪记录中

          记录gpdaddr 到last 进程跟踪记录中

          更新 last  index 和 curr  index.

   

注:

1  gpdaddr 获得通过两步 ( 方法同 Linux 获取当前进程描述符 current 的方法 ) :a rsp&0xffff000 ( fffff000 for 4k kernel stack;ffffe000 for 8k ) ;b kvm_read_GUEST (vcpu, rsp&0xffff000,4, gpdaddr) (read the GUEST virtual address of process descriptor)

2  gpid/gpname 获取通过如下语句 : kvm_read_GUEST(vcpu,gpdaddr+PID_OFFSET/COMM_OFFSET,length,gptr->ptrt[].gpid/gpname)

 

6 加载模块

  Insmod gptrace.ko

   

4.4 限制  

1.     目前获取 pid/comm 都是通过硬编码完成 , 因此仅仅适合 FC6 作为 GUEST OS, 如果你需要运行其它 Linux 发布版或者其他内核 , 需要你修改 PID_OFFSE/COMM_OFFSET 这些宏。

2.     目前只能记录 270 个活跃进程踪迹 . 因为现在我用 ioctl 带回数据 , 而 ioctl 的最大传输限制是 4 页 ,16k. 。

3.     和 KVM 一样 , 该方法只能在支持 VT 的 CPU 上实现。

4.    目前仅仅是原型验证,因此程序中尚有很多bug 。

5. 编译、运行

5.1 编译方式

1 . 下载原代码

2 . 进入目录 kvm-24

3 . 执行编译过程 ( 同 KVM)

            ./make clean

            ./configure -prefix=/usr/local/kvm

            ./make

            ./make install

             ./modprobe kvm-intel    ( 我使用 Intel VT CPU)

            ./modprobe gptrace

4 . 启动一个 VM

   ./use/local/kvm/bin/qemu  <your vm image>

5 . 为了简单期间 , 我将用户工具实现在 <kvm>/user 目录下的 main.c 中 . 你可执行在 <kvm>/user 目录执行 make 生成kvmctl 执行文件 .

 

5.2 运行方式

1 .打开跟踪功能

./kvmctl -E vmid    (vmid is qemu process id ,you can get it form ps -aux|grep -I qemu)

2 .关闭跟踪功能

./kvmctl -D vmid

3 .显示 VM 的运行进程信息

    ./kvmctl -S vmid

4 .显示用法

    ./kvmctl -h

posted @ 2012-02-28 23:27  zaleilynn  阅读(583)  评论(0编辑  收藏  举报