基于内存保护键值的细粒度访存监控*
2024-01-24王睿伯吴振伟张文喆邬会军张于舒晴
王睿伯,吴振伟,张文喆,邬会军,张于舒晴,卢 凯
(国防科技大学计算机学院,湖南 长沙 410073)
1 引言
访存监控,即获取程序访问内存的行为,在系统软件和体系结构研究中有着非常广泛的应用。当前,实现访存监控的主流技术手段包括代码插桩(Instrumentation)和页保护(Page Protection)2大类。
代码插桩是一种通用的程序分析手段。代码插桩在保证目标程序原始执行逻辑完整性的基础上,在目标程序中插入一些额外的代码片段用以收集程序的执行信息,如程序的控制流、数据流等。基于代码插桩技术实现访存监控,即通过插桩程序中的内存访问指令监控程序的内存访问行为。从插桩时机的角度看,代码插桩技术可以分为静态编译插桩和动态二进制插桩2大类。编译插桩是指在目标程序的编译过程中注入插桩代码,生成新的可执行程序[1]。动态二进制插桩则是在可执行程序运行期间动态注入插桩代码。典型的动态二进制插桩工具,如Intel的Pin[2]、QEMU(Quick EMUlator)[3]、DynamoRIO[4]等,采用即时编译 (Just-In-Time Compilation) 技术对目标二进制代码执行动态重编译,并在动态重编译过程中实现代码插桩。相比于编译插桩,动态二进制插桩通常会引入数十倍于前者的性能开销。然而,动态二进制插桩无需重新编译目标程序,可以灵活适用于无法获得目标程序源代码的情形。此外,编译插桩的监控目标通常局限于用户态程序,而动态二进制插桩普遍适用于监控用户态程序和操作系统内核。
Figure 1 Page table entry图1 页表项
页保护是内存管理单元MMU(Memory Manag- ement Unit)在硬件层面提供的一种页面访问权限检查功能[5-7]。软件可以通过配置页表项PTE(Page Table Entry)中的权限标志位实现页面粒度的访问权限控制。当用户程序的访存请求违背页表项所规定的被访问内存页的访问权限时,MMU会触发页错误。针对因访存违例而触发的页错误,Linux内核会向触发异常的进程发送段错误信号SIGSEGV(SIGnal:SEGmentation Violation)。基于页保护实现内存写访问监控的基本原理是通过写保护目标内存区域,即将其访问权限配置为只读,使被监控程序的写请求触发访问权限违例,进而在自定义的SIGSEGV信号处理函数中获取到被监控程序正在试图写入的内存地址。在记录了被访问到的内存页地址后,SIGSEGV信号处理函数可通过mprotect系统调用解除对该内存页的写保护,使得信号处理函数返回后被中断的写访问可以恢复执行。类似地,可以通过将目标内存区域的访问权限设置为不可访问,使得被监控程序对该区域的读写请求均触发访问权限违例异常,从而实现内存读写访问监控。基于页保护实现访存监控的性能开销,主要来自于因修改PTE的页访问权限标志位而引入的特权级切换和页表缓存TLB(Translation Lookaside Buffer)刷新。
总体而言,编译插桩引入的运行时开销最低,但应用场景相对受限。动态二进制插桩应用范围广,但性能开销显著。页保护相比于动态二进制插桩,运行时开销大幅降低,但在编程灵活性和监控粒度方面仍有局限性[8]。本文的目标是:突破页保护在编程灵活性方面的局限性,构建一种融合页保护和编译插桩优势的高效细粒度访存监控机制。
2 背景
2.1 基于页保护的访存监控
如图1所示,分页式内存管理模式下,页表描述虚拟页到物理页的映射关系,并且存放页的保护位,即访问权限。每个进程拥有独立的页表。进程的页表由操作系统创建和维护,供MMU硬件查询实现虚实地址转换和内存访问权限检查。如果进程违背页表中所设置的权限访问内存(如写入只读内存区域),MMU将触发页错误中断。出现页错误后,操作系统的页错误处理例程通过读取硬件寄存器(如x86处理器的CR2寄存器),获得引起页错误的内存地址,并向被中断的进程发送SIGSEGV信号。进程收到SIGSEGV信号的缺省行为是终止执行并返回错误代码。
基于页保护的访存监控机制,通过操作系统提供的内存保护系统调用(mprotect)接口,预先将目标虚拟内存区域的访问权限设置为不可访问或只读,使得被监控程序对目标虚拟内存区域的读写访问或写访问无法通过MMU的访存权限检查而被硬件中断。与此同时,访存监控机制通过向操作系统注册SIGSEGV信号处理函数对因访存违例而产生的SIGSEGV信号进行进一步处理。如图 2所示,SIGSEGV信号处理函数首先从中断栈中读取引发页错误的内存地址,即被监控程序正在访问的地址。此外,SIGSEGV信号处理函数可以进一步通过保存在中断栈中的错误代码确定访存类型是读访问还是写访问。在记录被监控进程的访存行为之后,SIGSEGV信号处理函数再次发起mprotect系统调用,解除被监控程序对相应虚拟内存页的访问限制,使被中断的访存操作在SIGSEGV信号处理函数返回后可以恢复正常执行。
Figure 2 Workflow of page protection-based memory access monitoring mechanism图2 基于页保护的访存监控机制工作流程
Figure 3 Architecture of MPK图3 MPK架构
2.2 MPK硬件扩展
基于PTE的页保护机制中,用户态程序需要陷入到内核态修改页访问权限,上下文切换开销较大。此外,操作系统在修改PTE后需要执行开销显著的TLB flush操作以确保TLB的数据一致性。
MPK(Memory Protection Keys)提供了一种相对于操作页表项更加轻量化且更加灵活的页保护机制。MPK硬件扩展在每个处理器核心中增加1个每核私有的(core-private)寄存器PKRU (Protection Key rights Register for User pages)来描述页访问权限,支持软件层在用户态实现线程局部的页访问权限控制。如图 3所示,MPK使用传统PTE中的4个空闲位域存储pkey(protection key)值,提供0~15共计16个pkey。拥有相同pkey的页构成一个分组,共享PKRU中pkey所对应的内存页访问权限描述。具体而言,PKRU中为每个pkey维护WD(Write Disable)和AD(Access Disable)2个权限控制位。软件可以将某pkey在PKRU中的WD或AD位置为1,使其对该pkey所指向页分组的写访问或所有访问触发异常。由于PKRU为用户态寄存器,用户程序读/写PKRU寄存器只需要执行非特权态的RDPKRU/WRPKRU指令,而无需发起系统调用。
在使用MPK的情形下,MMU在执行权限检查时,会同时检查PTE中描述的访问权限和PTE中的pkey在PKRU中对应的访问权限,最终被允许的访问权限为二者的交集。由于PKRU为处理器核心独有,MPK支持线程局部的访问权限控制。如图 4所示,针对同一内存页,Corea和Coreb可以同时拥有相互独立的访问权限。而PTE为所有线程所共享,修改PTE中的页保护位将影响所有线程的访问权限。
MPK硬件扩展的设计初衷是增强程序的访存安全性。基于MPK所提供的轻量化内存访问控制机制,已有大量研究工作围绕敏感数据隔离保护[10]、unikernel内部内存隔离[11]、线程级访存约束[12]等应用场景开展研究。本文将MPK提供的轻量化页保护特性用于建立低开销的访存监控机制。
Figure 4 Memory access permissions control in MPK[9]图4 MPK中的访问权限检查[9]
3 MemTracker设计
3.1 细粒度的轻量化页保护
在基于页保护的访存监控中,被监控程序对同一内存页的多次访问中,只有第1次访问会触发页错误中断。在SIGSEGV信号处理函数通过mprotect系统调用解除内存页访问限制后,对该页面的后续访问将不会继续触发页错误中断,从而无法被页保护机制监控到。为了实现能够监控到每条访存指令的细粒度访存监控,需要在对被保护内存页的第1次访问结束后且对该内存页的后续访问未开始执行前的某个时机再次中断被监控程序,并重新施加页保护。对此,本文利用处理器的单步(Single-Stepping)调试模式实现了基于硬件页保护的细粒度访存监控。单步调试模式下,处理器每执行一条指令,便会触发一次调试异常。操作系统的调试异常处理例程,会向触发调试异常的程序发送SIGTRAP信号。在x86架构下,可以通过配置EFLAGS寄存器的TF(Trap Flag)标志位,控制处理器进入或退出单步执行模式。
在程序触发调试异常陷入到内核态时,被中断的程序执行上下文被保存在内核栈。内核在调用程序预先注册的SIGTRAP信号处理函数时,会将被中断程序的执行上下文拷贝到信号栈(Signal Stack)。由此,程序自定义的信号处理函数可以通过信号栈访问到程序被中断时的上下文信息。信号处理函数结束后,将执行sigreturn函数。sigreturn函数通过信号栈中保存的上下文信息,恢复被中断程序的执行现场。
如图5所示,本文通过向操作系统注册自定义的SIGSEGV和SIGTRAP信号处理函数,并通过设置页保护和处理器调试模式,使访存指令在执行前和执行后分别触发页错误和调试异常,从而实现可以拦截到每一条访存指令的细粒度访存监控。具体而言,本文在SIGSEGV信号处理函数中,将信号栈中保存的TF标志位置为1。这样一来,当SIGSEGV信号处理函数执行结束后,sigreturn函数使用信号栈中保存的上下文信息恢复被中断程序的执行现场时会将处理器的TF标志位置为1,从而使处理器进入到单步执行模式。被中断的访存指令在单步执行模式下执行结束后,处理器将会触发调试异常。接下来,操作系统的调试异常处理例程将向被监控程序发送SIGTRAP信号。被监控程序接收到SIGTRAP后,操作系统内核会进一步调用此前注册的SIGTRAP信号处理函数。至此,通过对SIGSEGV和SIGTRAP信号的融合处理,本文实现了分别在一条访存指令执行前和执行后这2个时间点中断被监控程序的执行。在SIGTRAP信号处理函数中,本文对单步模式下被访问到的虚拟内存页重新施加页保护,使得后续对该虚拟内存页的访问可以被持续监控,从而突破页保护机制只支持页粒度访存监控的局限性。
Figure 5 Fine-grained page protection图5 细粒度页保护
在借助调试异常实现细粒度访存监控之外,本文进一步利用MPK的轻量化特性,实现运行时开销更低的内存访问权限控制。具体而言,本文在程序初始阶段,向操作系统内核申请一个内存保护键值pkey,并通过pkey_mprotect系统调用将pkey标记到拟监控内存区域的页表项。拥有相同pkey的虚拟内存页构成了一个内存页分组,以下简称pkey分组。在完成pkey与虚拟页之间的绑定后,本文通过执行WRPKRU指令修改PKRU寄存器中pkey对应的访问权限描述位,限制被监控程序对pkey分组的访问,即施加页保护。WRPKRU是非特权态指令,执行开销非常低。相较于通过mprotect系统调用修改页表项,以配置PKRU寄存器的方式修改虚拟内存页访问权限,可以避免引入用户态和内核态之间的上下文切换开销。此外,每个处理器核心拥有独立的PKRU寄存器,对PKRU寄存器内容的修改不需要在处理器核心之间同步。而修改页表项则不得不引入开销显著的TLB flush操作,以实现处理器核之间的同步。综上,本文借助MPK机制,实现了一种相较于传统的mprotect更加轻量化的页保护机制。
在针对pkey分组施加页保护之后,被监控程序对pkey分组的访问将使其触发页错误而被中断。针对此情形,操作系统内核同样会向被监控程序发送SIGSEGV信号,并将被访问到的pkey分组对应的pkey值保存于信号栈。本文在SIGSEGV信号处理函数中,从信号栈中获取到被监控程序正试图访问的pkey分组对应的pkey,并通过修改中断现场保存的PKRU寄存器中pkey对应的访问权限控制位,解除对pkey分组的访问限制,使被监控程序可以恢复执行对pkey分组的访问。类似地,本文通过在SIGSEGV信号处理函数中设置中断现场保存的TF标志位,使被监控程序在完成对pkey分组的访问权限之后触发调试异常,从而实现在访存指令执行结束后再次拦截被监控程序。SIGTRAP信号处理函数通过配置中断现场保存的PKRU寄存器中pkey对应的访问权限控制位,重新对pkey分组施加访问限制,使被监控程序对pkey分组的后续访问可以被持续监控到。
至此,本文基于对pkey分组访问权限违例异常和处理器调试异常的融合处理,突破了MPK仅支持pkey分组粒度访问权限控制的监控粒度局限性,实现了细粒度的访存监控。此外,上述轻量化细粒度访存监控机制仅需要占用1个pkey,没有触及MPK仅提供有限数量pkey的硬件资源局限性。
3.2 混合式访存监控
基于页保护实现访存监控,是以使被监控程序触发访问权限违例异常为基础;而编译器插桩则是在程序编译期间面向访存指令插入访存监控逻辑。与基于页保护监控访存行为相比,编译器插桩访存指令引入的运行时开销更低且可扩展性更好,具有更好的性能表现。但是,在监控能力方面,编译器插桩具有一定的局限性。基于编译器插桩实现访存监控,需要重新编译被监控程序的源码,这使得编译器插桩无法被用于非开源程序和第三方库等。本文提出的轻量化细粒度访存监控,可有效弥补编译器插桩在监控能力方面的局限性。通过融合基于MPK的轻量化细粒度页保护和编译插桩,本文提出了一种混合式访存监控机制,实现了对访存监控性能和访存监控能力的兼顾。
混合式访存监控机制的目标场景是目标程序主体源码可重编译而目标程序所依赖的部分动态链接库不可重编译的情形,这种场景也是真实应用中普遍存在的情形。混合式访存监控的核心思想是利用MPK的轻量化特性,实现对虚拟内存访问权限的低开销频繁切换。具体而言,与细粒度页保护机制类似,混合式访存监控在程序初始化阶段申请pkey并使用该pkey标记拟监控的虚拟内存区域构建pkey分组,并通过配置pkey对应的PKRU寄存器中的访问权限控制位,实现对pkey分组的访问约束。对于目标程序中支持源码重编译的部分,混合式访存监控采用编译器插桩访存指令的方式注入访存监控逻辑,并在访存指令前后分别插入2条WRPKRU指令配置PKRU寄存器中pkey对应的访问权限控制位,以解除和恢复对pkey分组的访问约束,从而使被插桩到的访存指令在程序运行阶段不会触发访问权限违例异常,即实现页保护免疫。由于WRPKRU操作非常轻量化(4.3节的实验结果表明,WRPKRU操作引入的额外开销不超过4%),上述针对单条访存指令的细粒度页保护免疫过程得以有效兼顾编译器插桩的性能优势。同时,对于被监控程序中不支持重编译的访存指令,其对pkey分组的访问会因触发访问权限违例异常而被监控到。
至此,本文通过基于MPK的细粒度页保护免疫,实现了兼顾编译器插桩性能优势和页保护功能优势的混合式访存监控机制。
4 实验与结果分析
4.1 实验环境
本文在一个Linux服务器(内核版本5.4.0)上开展了实验测评。该服务器有2个20核Intel®Xeon®Gold 6230 2.10 GHz处理器。DRAM大小是64 GB (每个socket 32 GB)。本文使用的测试程序由clang-6.0.1编译,编译插桩工具基于LLVM 6.0.1实现。所有实验结果都是10次运行结果的平均值。
4.2 实验设置
本文在内存对象缓存系统Memcached[13]中,分别实现了以下几种访存监控机制:(1)本文提出的细粒度页保护机制PP(Page Protection);(2)本文提出的基于MPK处理器扩展的轻量化细粒度页保护机制(MPK);(3)本文提出的轻量化细粒度页保护与编译器插桩相融合的混合式访存监控机制(MPK-CI);(4)仅插入WRPKRU操作而不执行访存监控的对比组(WRPKRU)。其中,WRPKRU对比组不具备访存监控功能,主要用于验证WRPKRU操作的轻量化特性。
本文使用Memtier[14]测试程序生成输入数据,对上述集成了访存监控功能的Memcached变体实现进行压力测试。测试过程中,Mmcached运行4个服务器工作线程,Memtier运行16个工作线程,每个工作线程模拟50个客户端,每个客户端发起10 000个针对Memcached服务端的读写请求操作(共计800万次请求)。其中,Memtier触发的读写请求操作中,Set操作和Get操作的比例为1∶10,数据对象的大小为32 B。实验过程中,使用绑核工具numactl分别将Memcached进程和Memtier进程绑定在CPU 0和CPU 1上。
4.3 实验结果分析
本文记录了在不同Memcached变体实现作为服务端的情形下,Memtier完成800万次读写请求操作所需的时间,并以缺省Memcached实现(不监控访存行为)作为服务端的运行时间为基准,对实验结果进行归一化处理。
本文通过写保护Memcached内置slab分配器所分配的内存页,监控Memcached对动态内存对象的访存行为。图 6展示了本文对比测评的访存监控机制相对于缺省Memcached实现的相对时间开销。其中,PP引入了相对缺省Memcached实现5.07倍的时间开销。得益于用户空间内存访问权限控制的轻量化特性,轻量化细粒度页保护的相对时间开销下降至3.43,相比于PP的减少了约32%。MPK-CI分组展示了使用编译器插桩监控Memcached程序源码树下items.c源文件中的访存操作而基于轻量化细粒度页保护机制监控Memcached程序中其他访存行为的实验结果。 MPK-CI的相对时间开销约为2.1。WRPKRU分组只在缺省Memcached实现的所有访存操作前后插入WRPKRU操作而不执行访存监控,该分组的实验结果表明,在Memcached中频繁执行WRPKRU引入的额外开销低于4%。
5 结束语
本文基于Intel MPK硬件扩展提出了一种轻量化且细粒度的访存监控机制,相比于传统方法,降低了超过30%的性能开销。与此同时,本文工作突破了传统页保护机制仅支持页粒度访存监控的局限性,基于硬件页保护机制实现了字节粒度的访存监控。此外,本文提出的轻量化细粒度页保护访存监控机制能够与编译插桩方法高效融合,有效弥补了编译器插桩无法覆盖程序中不支持重编译部分的局限性。