Page 325 - 《软件学报》2021年第10期
P. 325

陈兴蜀  等:VMOffset:虚拟机自省中一种语义重构改进方法                                                3297


                        复模块判断是否已获取全部所需进程偏移量:若尚未获取完毕,则进行相关模拟操作后执行 VM-
                        ENTRY 进入 TVM 完成内核函数的正常执行流程;否则恢复对 TVM 中相关内核函数的修改,使进程/
                        线程的创建及消亡动作不再产生 VM-EXIT 陷入至 VMM,减少了对 TVM 的影响,并将所得进程偏移
                        量提供给开源或自研的 VMI 应用程序,完成 TVM 的自省过程.

                 2    VMOffset 关键技术

                 2.1   拦截内核函数调用的事件监控机制
                    VMOffset 通过监控 TVM 中进程创建及消亡的内核函数调用事件,触发 VMM 中进程偏移量的获取流程,
                 并基于此触发点得到 TVM 的状态信息,作为后续获取进程偏移量的条件与依据.Linux 用户进程的创建通过系
                 统调用 fork()、clone()和 vfork()实现,进程的消亡通过系统调用 exit()、wait()实现.
                    可通过修改 SYSENTER_CS_MSR 寄存器        [23] 或系统调用表  [24] 等方式拦截上述系统调用,以监控进程的创建
                 及消亡.但该种方式存在如下缺陷.
                    1)   系统调用是用户进程访问内核服务的一个接口,内核线程直接通过调用内核函数使用内核提供的服
                        务.因此在 TVM 系统启动阶段,产生用户进程之前,并不会发生系统调用事件,故该种方式无法监控
                        TVM 整个时期的进程创建及消亡动作;
                    2)   在虚拟化环境下,为了在 VMM 层捕获到 TVM 中的系统调用事件,通常需要设置虚拟机控制结构体的
                        相关字段    [24] ,用于在 VMM 中捕获缺页异常或一般保护异常.这样会引入大量与系统调用无关的异常
                        陷入,对 TVM 系统性能带来较大影响.
                    针对上述不足,VMOffset 通过监控特定内核函数的调用来感知 TVM 中进程创建及消亡的动作,这种方式
                 可监控 TVM 各个阶段的进程创建及消亡动作,且不会引入无关的异常陷入.系统调用 fork()、clone()和 vfork()
                 的服务例程分别为 sys_fork()、sys_clone()和 sys_vfork(),这 3 个函数最终都会调用 do_fork()函数实现具体创
                 建工作.系统调用 exit()和 wait()的服务例程分别为 sys_exit()、sys_wait(),分别调用内核函数 do_exit()及 do_
                 wait(),这两个内核函数最终都会调用函数 release_task()释放待消亡进程的进程描述符.因此,VMOffset 通过监
                 控内核函数 do_fork()及 release_task()的调用,实时感知 TVM 中进程创建及消亡事件,其实现原理如图 3 所示.




















                                              Fig.3    Principle of event monitoring
                                                    图 3   事件监控原理

                    函数之间的调用通过栈来实现,函数执行时所用栈被称为函数栈帧,EBP 寄存器作为帧指针,指向函数栈帧
                 的开始位置;ESP 寄存器作为栈指针,则指向栈顶位置.当调用函数以 call 方式调用被调用函数时,call 指令首先
                 将调用函数 call 指令的下一条指令地址压入栈中,然后将被调用函数地址保存至 EIP 寄存器.执行流程从 EIP
   320   321   322   323   324   325   326   327   328   329   330