Instrew: leveraging LLVM for high performance dynamic binary instrumentation [SIGPLAN/SIGOPS 2020]
动态二进制检测框架是一种流行的工具,可以通过附加的分析、调试或分析功能来增强程序,或者在不需要重新编译或访问源代码的情况下添加优化或翻译。他们分析二进制代码,将其转换为典型的低级中间表示,添加所需的工具或转换,然后在运行时按需生成新代码。因此,大多数工具都以较低质量的代码为代价,专注于快速的代码重写过程,从而导致工具化代码的显著放缓。此外,大多数工具都在应用程序的地址空间中运行,这使得它们的开发很麻烦。
我们提出了一种新的动态二进制检测框架,Instrew,它通过(a)利用LLVM编译器基础设施进行高质量的代码优化和生成,以及(b)实现目标代码和工具之间的进程隔离来缩小这些差距。我们的框架没有使用我们自己的不可移植和低级的中间表示,而是直接将原始机器码提升到LLVM-IR中,在LLVM-IR中可以执行仪器和行为更改,并从中产生高质量的代码。SPEC CPU2017基准测试的结果表明,重写开销仅为使用最先进的工具链Valgrind的开销的1/5。
一句话:Instrew,将机器码提升到LLVM-IR,高效生成高质量代码
导论
在运行时动态重写二进制文件的工具经常用于程序分析、调试和可移植性。在重写过程中,这些工具还可以添加额外的工具代码来执行它们的分析、代码专门化甚至代码翻译。与在编译时添加这种转换相比,优点在于插装可以在运行中进行,而无需重新编译程序或其库。因此,存在几种动态二进制仪表(DBI)工具[11,12,25],最著名的是Valgrind[26]和Pin[24]。
这些框架中的许多都关注于插装和代码生成过程中的低开销。因此,它们只执行轻量级优化,代价是新生成的代码的性能降低。此外,插装通常发生在基本块或超级块级别,从而进一步限制了可能的优化范围。因此,插装程序的运行速度比未插装代码慢几倍是很正常的。
通过结合高质量的优化器和机器码生成器,可以避免缺乏优化。事实上,DBILL[25]表明,使用LLVM[23]编译器基础架构进行代码生成可以获得更高的性能。然而,由于机器码首先被提升到TCG (QEMU的IR[10]),它面临着几个限制:(1)只有常见的整数指令可以映射到LLVM-IR,导致其他指令的开销很高,包括浮点和SIMD指令;(2)代码以基本块粒度进行检测,限制了可能的优化范围;(3)模拟原始行为的代码和管理模拟的代码混合在一起,只能使用元数据注释来区分,而元数据注释会导致性能问题[18],并增加工具编写者的复杂性。
现有二进制检测框架的另一个限制是,检测工具需要使用与执行的二进制文件相同的地址空间。虽然这简化了对要检测的代码的访问,因为它驻留在相同的地址空间中,但工具开发人员必须小心不要破坏程序状态,这通常限制了对外部库的使用。据我们所知,唯一的例外是DynInst[11],其中使用DynInst API的工具在单独的进程中运行,并使用ptrace调试接口修改仪表化的进程。
在本文中,我们描述了一个新颖而全面的框架,通过利用LLVM的动态二进制重写的优化和转换功能来克服这些缺点。特别是,我们提供了一个库,可以将x86-64机器码直接提升到LLVM-IR,涵盖大多数x86-64指令(包括SSE/SSE2,但不包括很少使用的x87 FPU/MMX和AVX),并能够提升整个函数以实现高效的代码生成。我们的重写框架基于这个库,并实现了一个用于重写的客户机-服务器模型,其中服务器执行插装和代码生成,而客户机只是调度和执行生成的代码。利用LLVM-IR为高质量的代码插插和优化打开了大门,在极端情况下,还可以通过更改所使用的后端将二进制代码转换为其他平台。SPEC CPU2017基准测试的结果表明,我们的平均重写开销仅为72%,即仅为Valgrind平均开销367%的1/5左右。
Contributions
背景
动态二进制检测技术在调试和性能分析方面有着广泛的应用。例如,内存检查器,如Valgrind Memcheck[30]或Dr. memory[13],可以通过边界检查增强已经编译的代码,从而在运行时检测无效的内存访问。另一个用例是将跟踪功能添加到程序中,从而能够提取有关程序元素的信息,例如函数调用或内存分配。然后,这些信息可以用于性能分析,例如,用于检测程序的性能关键部分或用于分析缓存使用情况[33]。
虽然这些功能也可以在编译时添加,这将允许更多的优化和更高的检测程序性能,但这将要求使用这些检测重新编译所有源代码,包括所有库。这不仅需要大量的时间,而且有时是不可能的,例如,如果一个库的源代码不可用。
动态二进制检测系统的一般方法如下(参见图1):在开始时,主机将要检测的二进制文件(目标或客户)加载到内存中,初始化其状态,并将客户指令指针设置为客户二进制文件的入口地址,然后启动主机代码的主执行循环。主机从当前指令指针开始,解码客户代码(guest code),并将其提升为某种中间表示形式,在这种中间表示形式中,程序代码可以用所请求的工具进行修改和增强。这将产生一个修改后的客户程序(仍然在IR中表示),它从中生成新的机器代码,然后将其存储在单独的位置。然后执行重写的代码,并在结束时将来宾指令指针设置为下一条指令的地址,主执行循环在此继续翻译。
为了减少冗余解码和检测,重写者通常部署代码缓存。如果执行循环遇到一个已经翻译过且仍在缓存中的地址,则重用先前存储在缓存中的代码。作为进一步的优化,可以将翻译后的代码修补为直接分支到下面的翻译代码块,而无需经过主机的主循环。这种优化被称为链接。但是,对于链接来说,目标地址必须是已知的,因此这种优化不能应用于间接分支和函数返回。
方案
Lifting x86-64 to LLVM-IR
动态二进制重写和插装通常依赖于将机器码提升到中间表示(IR)。这增加了灵活性并简化了代码转换的实现。这方面的两个突出例子是VEX, Valgrind框架的IR[26]和TCG, QEMU使用的IR[10,28]。但这些IR通常是不完整的,需要依赖外部辅助函数处理IR中未涉及的复杂指令,这不利于代码优化。为了满足目标,需要一个更高层次的、与体系结构无关的IR,即LLVM-IR,具有高质量的代码优化器和机器码生成器。
Rellume Design
Lifter以半惰性方式工作:我们首先为每个指令独立生成相应的LLVM-IR代码,然后通过LLVM优化通道跨指令或基本块边界执行优化。由于这可能导致大量未使用或琐碎的指令,因此仅根据需要生成LLVM-IR代码的某些部分。这提高了整体性能,减少了分析时间,并消除了对死代码的处理。
LLVM-IR Functions
在LLVM-IR中,所有代码都必须封装在函数中,函数可以接受参数并有返回值。函数只有一个入口点,但除此之外,它还可以包含在多个基本块级别上定义的任意(且良好的)控制流。
创建的函数接受一个指向CPU状态的指针作为单个参数,并且不返回任何值。CPU状态结构包含描述驱动主机主循环所需的CPU状态所需的所有必要信息。这包括所有通用寄存器,状态标志和SSE矢量寄存器,以及指令指针RIP和寄存器fs/gs的段寄存器基偏移量。
Basic Blocks
基本块是一个指令序列,它不修改控制流,以另一个基本块的分支或返回指令结束。在解码期间检测x86-64机器码中的基本块。在同一阶段,我们还确保每个指令部分恰好属于一个基本块。这减少了代码大小,避免了重复的提升和优化工作,提高了整体性能。
Registers
对以下x86-64寄存器进行建模:16×64位通用寄存器、64位指令指针、16×128-bit SSE矢量寄存器和标志寄存器。我们目前不支持x87 FPU、MMX和AVX寄存器,但这可以在以后添加。
Status Flags
标志寄存器必须单独处理,因为各个标志位是相互独立的。因此,标志寄存器为七个存储的标志(符号、零、进位、溢出、奇偶校验、调整和方向)中的每一个都有一个单独的1位面。
Memory Access
x86-64中的内存操作数相当复杂,最多包含四个可选组件:(a)基寄存器,也可以是指令指针;(b)缩放索引寄存器,即寄存器乘以1、2、4或8;©一个恒定的32位偏移量;(d)段寄存器作为附加偏移量。此外,在64位模式下,该体系结构允许将地址大小覆盖到32位,为单个指令指定。
Instruction Coverage & Limitations
Rellume涵盖了基本x86-64架构的很大一部分,包括SSE和SSE2,它们也通常用于x86-64上的浮点计算。但是,还不支持很少使用的旧式x87 FPU和MMX指令集。此外,有些指令不能独立于目标体系结构而被提升,例如,sycall、cpuid或rdtsc。对于这些指令,没有提供默认实现。相反,使用lifter的工具使用配置API指定自定义实现。
Contrasts with Other Approaches
本文的提升方法在几个方面不同于其他直接将x86-64机器码转换为LLVM-IR的提升器。许多提升器,例如McSema[31]和RetDec,并不在SSA寄存器中保留寄存器值,而是在每次访问时将它们加载/存储到CPU结构中。转换到更可优化的SSA寄存器是使用现有的LLVM优化通道完成的。与我们的方法相反,这些系统无法传播类型信息,特别是对于较小的整数寄存器和向量类型。为不同类型存储多个方面的概念只能通过直接使用SSA寄存器来有效地表示。此外,从加载/存储到SSA寄存器的转换需要额外的优化时间,我们的lifter避免了这一点。
Rewriting Framework
为了使用提升库Rellume,将其集成到名为Instrew的新框架中,该框架也是免费提供的开源框架(https://github.com/aengelke/instrew, accessed 2020-02-17)。
Client/Server Architecture
与大多数其他二进制检测系统相比[24-26],我们将插装器分成两个进程:一个轻量级客户端进程,它管理程序状态并执行检测代码; 另一个服务器进程,它负责根据客户端请求实际解除、检测和编译新代码。它们通过IPC通信机制连接,如图3所示。
Instrew Server 重写服务器是被动的,只对客户端请求起作用;除了少量配置选项外,它是无状态的。每当客户端请求给定地址的新代码时,服务器通过相同的连接查询相关的指令字节。然后,它解码这些指令并将它们传递给我们的升降机库Rellume,它为解码的指令返回一个LLVM函数。在提升过程中,可以在指令和基本块边界添加代码,在生成LLVM-IR之后,可以在更高的语义级别上转换程序。
Instrew Client 客户端负责执行实际的(重写的)程序代码并管理代码缓存,它将指令地址映射到已经重写的代码片段以供将来使用。在程序启动时,将二进制文件映射到内存中,根据System-V ABI[21]初始化CPU状态和堆栈,并将指令指针设置为ELF标头中指定的入口地址。
Communication 两个进程之间的通信目前是作为通过UNIX管道发送的自定义二进制协议实现的。
Translation Granularity
该框架选择了函数粒度,因为它有三个主要好处:(1)可以利用LLVM的整个函数和循环转换的优化可能性,因为复杂的控制流也被提升到LLVM-IR,从而导致更好的寄存器分配和更优化的代码; (2)简化了客户端,因为不需要区块链来获得良好的性能;在解码时已经处理了所有应用链的可能性,并在单个函数块中生成; (3)它减少了客户端和服务器之间所需的交换机数量。
Optimizations for LLVM-based Dynamic Rewriting
为了进一步提升系统性能,增加3种优化措施
HHVM Calling Convention 对于x86-64目标,LLVM支持hhvmcc调用约定,该约定最初是为HipHop Virtual Machine[29]设计的,HipHop Virtual Machine是一种用于PHP和Hack的JIT编译器[27]。与其他调用约定相比,在通用寄存器中可以传递15个参数并返回14个值。此外,16个通用寄存器中只有2个是调用保存寄存器,即r12和rsp。这种调用约定对于二进制重写的情况也非常有益:我们可以在上下文切换期间(例如,查找/转换返回值或间接跳转目标)在主机寄存器中保留12个通用寄存器的值。这大大减少了代码块开头和结尾的内存访问量。此外,我们传递一个指向CPU结构的指针作为参数,并获得下一条指令的地址作为附加返回值。注意,我们有意留下一个未使用的寄存器,以减少主机端的寄存器压力,避免翻译缓存查找时溢出寄存器。完整的寄存器映射可以在表2中找到。
Use of native fs/gs registers x86-64体系结构有两个特殊的段寄存器,即fs/gs,其基址可以用作内存操作数的额外偏移量[22]。这些段寄存器的基址通常只能从内核空间配置。在Linux上,这是使用arch_prctl系统调用完成的。虽然可以在重写的代码中使用额外的计算来添加基址,但是也可以在x86-64目标上发出使用本机段寄存器的代码。我们将任何相应的arch_prctl系统调用转发给主机,然后使用与体系结构相关的LLVM地址空间256和257进行基于gs和fs的内存访问。根据定义,这些地址空间对应于各自的段寄存器[5]。
Optimization of flag usage on calls/returns x86-64上的许多算术指令覆盖rflags寄存器中的状态标志。由于很少需要这些标志,因此避免显式计算标志值与性能有关。虽然在LLVM函数中,死代码消除传递将在可能的情况下删除此类计算,但这在代码块的边界上是不可能的。由于使用了函数粒度,除了间接跳转之外,在所有函数调用和返回之前都会出现这种情况。
System Call Handling 为了正确处理可能与检测系统发生冲突的系统调用(例如,fork或arch_prctl),所有系统调用都被重定向到客户端的一个单独的函数。在大多数情况下,系统调用被转发到主机操作系统,无需修改。
实验
我们没有与Pin[24]进行比较,因为Pin具有不同的作用域,因为它只允许插入函数调用,但不允许对代码进行进一步的转换或修改,就像我们在系统中所做的那样。此外,为了评估我们的x86-64到LLVM提升器的质量,我们使用HQEMU [19]运行基准测试,HQEMU是一个使用LLVM进行代码生成和优化的QEMU变体。我们使用HQEMU在相同的体系结构上模拟x86-64用户级二进制文件,并将其配置为使用LLVM-only模式。注意,我们必须修补HQEMU以避免与LLVM的选项名称冲突。
Rewriting Overhead
图4显示了基准测试结果。我们可以看到,andrew的平均开销为72%,明显低于Valgrind的367%。特别是在浮点基准测试中,可以看到更好地处理浮点操作和SIMD指令。Valgrind在这个子集上的平均开销为490%,而Instrew的开销仅为43%。
结果进一步表明,我们将x86-64机器码提升到LLVM-IR的方法通常比仅LLVM模式下的HQEMU的开销要低得多(开销为142%)。同样,在浮点基准测试中,性能差异也得到了强调。
Dynamic Binary Translation
比较了Instrew与QEMU[10]的性能,QEMU是在用户空间仿真模式下运行的最先进的仿真工具,以及为AArch64本地编译的程序的性能。我们也会在比较中包括HQEMU[19],但是在运行任何程序时都会遇到崩溃,可能是由于内存损坏。
图6显示了我们的实验结果。在AArch64上用QEMU模拟的x86-64代码比在AArch64上编译和优化的代码平均慢5倍。与QEMU相比,使用Instrew的速度降低了3倍,平均速度提高了40%。
总结
References
[10] Fabrice Bellard. 2005. QEMU, a fast and portable dynamic translator. In USENIX Annual Technical Conference, FREENIX Track, Vol. 41. 46.
[11] Andrew R. Bernat and Barton P. Miller. 2011. Anywhere, Any-time Binary Instrumentation. In SIGPLAN-SIGSOFT Workshop on Program Analysis for Software Tools (PASTE). 9–16.
[12] Derek Bruening, Timothy Garnett, and Saman Amarasinghe. 2003. An infrastructure for adaptive dynamic optimization. In International Symposium on Code Generation and Optimization (CGO). 265–275.
[13] Derek Bruening and Qin Zhao. 2011. Practical memory checking with Dr. Memory. In Proceedings of the IEEE/ACM International Symposium on Code Generation and Optimization (CGO’11). IEEE Computer Society, 213–223.
[18] Hal Finkel. 2016. Intrinsics, Metadata, and Attributes: The story continues. In 2016 LLVM Developers’ Meeting
[19] Ding Yong Hong, Chun Chen Hsu, Pen Chung Yew, Jan Jan Wu, Wei Chung Hsu, Pangfeng Liu, Chien Min Wang, and Yeh Ching Chung. 2012. HQEMU: A multi-threaded and retargetable dynamic binary translator on multicores. In International Symposium on Code Generation and Optimization (CGO). 104–113.
[22] Intel Corporation. 2019. Intel 64 and IA-32 Architectures Software Developer’s Manual. https://intel.com/sdm, accessed 2020-02-17.
[23] Chris Lattner and Vikram Adve. 2004. LLVM: A compilation framework for lifelong program analysis & transformation. In International Symposium on Code Generation and Optimization (CGO).
[24] Chi-Keung Luk, Robert Cohn, Robert Muth, Harish Patil, Artur Klauser, Geoff Lowney, Steven Wallace, Vijay Janapa Reddi, and Kim Hazelwood. 2005. Pin: building customized program analysis tools with dynamic instrumentation. In SIGPLAN Conference on Programming language design and implementation (PLDI), Vol. 40. 190–200.
[25] Yi-Hong Lyu, Ding-Yong Hong, Tai-Yi Wu, Jan-Jan Wu, Wei-Chung Hsu, Pangfeng Liu, and Pen-Chung Yew. 2014. DBILL: an efficient and retargetable dynamic binary instrumentation framework using LLVM backend. In International Conference on Virtual Execution Environments (VEE). 141–152.
[26] Nicholas Nethercote and Julian Seward. 2007. Valgrind: A Framework for Heavyweight Dynamic Binary Instrumentation. In ACM SIGPLAN notices, Vol. 42. 89–100.
[27] Guilherme Ottoni. 2018. HHVM JIT: A Profile-guided, Region-based Compiler for PHP and Hack. In Proceedings of the 39th ACM SIGPLAN Conference on Programming Language Design and Implementation (PLDI 2018). 151–165. https://doi.org/10.1145/3192366.3192374
[28] QEMU Developers. 2019. Documentation/TCG. https://wiki.qemu.org/Documentation/TCG, accessed 2020-02-17
[29] Philip Reames. 2015. LLVM Phabricator: Calling convention for HHVM (D12681). https://reviews.llvm.org/D12681, accessed 2020-02-17.
[30] Julian Seward and Nicholas Nethercote. 2005. Using Valgrind to detect undefined value errors with bit-precision. In USENIX Annual Technical Conference, General Track. 17–30.
[31] Trail of Bits, Inc. [n. d.]. McSema. https://www.trailofbits.com/research-and-development/mcsema/, accessed 2020-02-17
[33] Josef Weidendorfer, Markus Kowarschik, and Carsten Trinitis. 2004. A tool suite for simulation based analysis of memory access behavior. In International Conference on Computational Science. Springer, 440–447.
Insights
(1) Instrew可以用来获得二进制仿真信息