@inproceedings{malecha2015automated,
  title={Automated software winnowing},
  author={Malecha, Gregory and Gehani, Ashish and Shankar, Natarajan},
  booktitle={Proceedings of the 30th Annual ACM Symposium on Applied Computing},
  pages={1504--1511},
  year={2015}
}

OCCAM:Automated Software Winnowing

开源地址

0 摘要

  • 为了解决代码膨胀问题,我们介绍一个OCCAM工具.
    • OCCAM结合了部分求值和类型理论的技术,旨在减少部署应用中的代码。
    • OCCAM可以在不对程序源代码进行注释或以其他方式修改的情况下使用。
    • 它利用配置时间信息生成一个根据将要部署的上下文进行专门定制的应用程序版本。
  • 我们介绍了我们的算法、实现和实验评估。

1 介绍

  • 模块化软件库 通过为图形,文件操作和网络提供 可复用功能,使得编写复杂应用更加容易,然而,为了实现通用性,库往往比应用程序所需要的功能更多
    • 如 libpng[10]提供了复杂的接口进行图像转换,但许多都用不到
  • 这个问题也涉及了软件栈的多个层次,由跨层依赖引入的开销
    • 如 miniblog[13]运行在PHP上,而PHP历来与libc,libc中许多函数仅被miniblog不使用的PHP代码使用. 这些函数包含在静态链接的二进制文件中,存储在磁盘中,执行期间需要内存,多余的代码甚至会被反向导向编程利用
  • 尽管大多数系统不是资源限制的,但 目前普遍的实现中,即使是 二进制文件中 不用的功能 和共享库也会影响性能,可靠性和安全.
    • 为此,我们提出了一种将代码特化到实际使用中并简化掉未使用功能的方法
  • 我们提出了 winnnowing(筛选),一种使用部分求值的静态分析和代码特化工具
    • 该过程保留了原始程序的正常语义,即原始程序在指定输入上的行为与精简程序的行为相同
    • 无效的执行,如缓冲区溢出,会以不同方式执行
  • 在基本层次上,程序的功能对应于潜在的执行的适量,如C标准库中的socket函数
int socket(int domain, int type, int protocol);
  • 该函数接收3个int值,有大量潜在行为,但大多数组合不会被特定程序使用.
    • 如Web 服务器通常打开AF_INET和AF_INET6套接字,但不需要AF_APPLETALK或AF_ATMPVC套接字。
    • Winnowing的目标是从软件堆栈中删除未使用的行为,减少需要分析的代码,同时保留需要的功能

动机

  • 筛选部署二进制文件的4个原因

  • 专用服务器:虚拟机的出现使得 高度专业化的服务器称为常态

    • 如Web服务器通常仅托管单个站点,而虚拟机中过于通用的库只会引入代码膨胀,即虚拟机之间的隔离破坏了共享代码的好处
  • 自定义库

    • 在嵌入式平台上,能够简化库,意味着开发人员可以通过删除不用的功能以使用更大更成熟的库.
    • 许多应用程序在提供 编译时配置 来移除大块代码. 然而这种方法又开发者定义而不是使用者,因此对开发人员来说增加了维护不同构建配置的负担
    • 上述方法也无法提供对应用程序获取内容的精确控制
      • 如,即使是间接地使用大型组件中的单个函数,也需要整个组件的存在
      • 此外,为管理员增加了工作量,因为他们必须及时更新系统,这会涉及与应用程序无关的补丁,因为很难确定应用程序是否使用了有缺陷的函数,尤其在使用动态语言时
  • 简化分析

    • 在安全重要的系统中,含有大型库是一种负担
    • 更小的应用程序,较少的配置选项,更少更通用功能的库,更容易进行静态分析.
    • 此外,部分估值的代码通常更简单,使得静态分析对上下文感知更有效
  • 二进制多样性

    • 专用库使得缓冲区溢出更难以利用
    • PHP等平台在可预测的地方包含相同的功能集
    • 特化每个应用的部署实例,不仅可以移除不需要的功能,还可以移动和改变遗留的功能. 这对剩余功能的攻击更加困难

贡献

  • 我们讨论了简化单个应用程序的思想和方法,我们的方法对所有同质的应用程序是相同的,即所有代码都可以编译成LLVM[7]位码格式的应用程序.

  • 我们选择LLVM框架,因为该框架包括几种流行语言的前端,包括C,C++,Java,其有着明确的中间表示,支持静态和动态编译

  • 我们的工作提供了以下内容

    • 一种用于大幅减少代码功能(包括库和应用程序)的工具. 可以用于实际的大型,工业程序中
    • 一种将外部信息纳入简化过程的模块化方法. 这可以来用执行策略,如"不应调用邮件"或"系统只能用字符串 ls 调用",类似面向切面的编程
    • 关于 简化 对 二进制大小 和 执行性能 影响的 实质分析. 结果标明,简化的二进制文件在性能上没有额外的开销,根据不同的应用程序可以显著减少二进制文件的大小
    • 一种工具 透明地增强大型软件项目的比那一过程,如Php,SQLite,以生成LLVM为嘛和本机目标文件

2 winnowing

本节介绍了我们的方法的一个实例
2.1 描述筛选一个单独的编译片段,用LLVM调用模块
2.2 描述多模块筛选,使之可在更大的库和动态加载中使用

2.1 模块内精简

  • LLVM的基本编译单元是模块,对于简单的程序,通常包含在一个单独的模块中,此时可以用上图的方法进行简化程序
    • 下面以简化C语言正则表达式库为例(仅使用C进行说明,所有的分析和转换都在LLVM位码上进行)
int regcomp ( regex_t ∗preg , const char ∗regex ,int cflags )
{ /∗ . . . regcomp code . . . ∗/ }
void main ( int argc , char∗ argv[ ] ) {
  regex_t re;
  regcomp (&re,argv[1] , REG_EXTENDED|REG_ICASE ) ;
  . . .
}
  • 算法过程如下

  • 0. 编译:将源码编译为LLVM位码. 第三部分将讨论我们的工具,他能自动化的构建LLVM位码,通过指导构建Unix系统的脚本,如GNU的自动化工具

  • 1. 部分估值:winnower的核心是部分估值器,由两个阶段组成,winnower交错并迭代执行这两个阶段

    • 1.1 优化:
      • 部分求值始于简化代码。其目标是暴露在编译时可确定的常量,消除死代码,并减少已知的控制流。将部分求值器应用于我们的代码片段将减少按位或运算,得到以下结果:
        void main ( int argc , char∗ argv[ ] ) {
          regex_t re;
          regcomp (&re,argv[1] , REG_EXTENDED|REG_ICASE ) ;
          . . .
        }
        
      • 正如 Fujita [2] 和 Smowton [12] 所指出的那样,LLVM 优化器的激进性使其成为一个合理的程序内部部分求值器。
      • 我们使用LLVM的 -O3 优化配置文件,包含了一些列简化,如 局数值编号、启发式循环展开、稀疏条件常数传播、常数折叠以及已知函数简化(简化对 libc 函数的调用,如 strlen 和 memcpy)。此优化传递还对局部函数执行一些保守的程序间优化,例如内联小函数和消除未使用的参数。
    • 1.2 特化
      • 优化阶段后,使用启发式方法 更 激进地在跨函数边界进行特化,在这个过程中寻找编译时已知参数的函数调用. 例如,regcomp的最后一个参数指定选项,如不区分大小写或是否支持扩展正则表达式. 该选项在编译时已知,因此可以进行特化,用已知的常量进行替代,删除不需要的参数
        int regcomp ( regex_t ∗preg , const char ∗regex  )
        { /∗ . . . regcomp with cflags=3 . . . ∗/ }
        
      • 再将优化运用到这个特化后的函数上,可以通过简化对cflags的分支语句来删除四代码,还有可能将常量推送到其它参数的位置上,进行更进一步地特化
      • 如果原始地使用,这两个阶段的迭代可能会增加代码的大小,但可以通过各种启发式方法来控制这一点.
        • 一种启发式方法是仅在未特化版本可以删除时特化函数,这个启发式方法完全对应减少功能的情况,是一种理想情况
        • 实际上,我们发现 需要更激进地进行特化,以揭示有益的低级特化. 我们目前的启发式方法是贪婪的,只要看到一个支持的常量就进行特化,同时忽略可变参数函数. 除了整数之外,我们的专门化过程还支持浮点数、常量字符串、任何类型的空值以及全局变量和函数的地址。
      • 递归:函数特化的主要困难是调用图中的循环,如果没有递归,我们可以限制特化的数量,因为我们指导LLVM优化器不会无限展开训话,然而,在有递归的情况下,我们必须检测特化何时会发散,考虑如下简单递归函数
        int foo(int start,int end){
          if(start>end){return start-end;}
          return foo(start+1,end);
        }
        int bar(ing x){
          return foo(1,x);
        }
        //特化后
        int foo_1(int end){
          if(1>end){return 1-end;}
          return foo_1(2,end);
        }
        
        • 此时foo_1(2,end)仍然可以特化,此时特化将无法停止,因为不知道end的值. 因此我们当前的实现选择不特化递归函数.
        • 值得注意的是,部分评估器通过实践绑定分析[1,5]解决了这个问题,同时确实存在递归函数的部分评估技术[4]
  • 2 消除:在部分评估稳定,或二进制文件变得过大而无法处理时,我们会删除不再需要的内部全局变量.

    • 在优化删除无法访问的代码时,全局变量可能变得没用.如果全局函数被特化,并且其所有被调用的位置都可以被特化的实例替代,则将删除这些全局函数.
    • 我们使用LLVM的三个优化器实现了这个阶段: globaldce, globalopt, strip-dead-prototypes
  • 3 链接:在迭代到固定点之后,我们使用 LLVM 工具将精简后的代码链接在一起构建二进制文件。这

  • 模块内的简化算法构建了输入模块的语义等效版本,同时努力消除不必要的功能。

  • 该算法可应用于任何单个编译单元,包括静态库和共享对象,因为它不通过删除导出的函数或更改其名称或类型来修改外部可见接口(在 LLVM 的抽象层面)。

  • 然而,部分评估可能会极大地改变程序的内部结构,使得在代码中未表现的监控行为(如堆栈检查)失效。

2.2 模块间精简

  • 模块内精简效果好,因为大多数应用程序可以在精简之前通过静态链接库构建为单个模块。,但大型程序必须要与共享库进行交互,因此需要模块间精简
  • 跨模块的精简的难点在于 代码的分离 和 维护兼容的二进制接口. 此外,我们希望特化是可重用的.
    • 如,我们会构建标准库的自定义版本,该版本通过仅包含这些应用程序所需的功能来支持多个应用程序。
    • 然后,我们希望能够自动重写客户端应用程序以重用相同的精简库
  • 原理上,我们将模块间精简分为三个任务
    • 2.2.1 计算模块间依赖关系,使模块间的独立精简称为可能
    • 2.2.2 对模块进行特化,
    • 2.2.3 根据生成的规范进行重写
    • 2.2.4 封存模块,隐藏其它模块未使用的符号,以便在链接时优化期间消除未使用的函数
  • 简化构成迭代 模块特化和封存 步骤,以产生完整的二进制文件
  • 处于解释目的,我们考虑特化以下简单的代码片段,其中 bar 是在另一个模块中实现的。
extern void bar(int,int);
int main(int argc, char* argv[]){
  bar(argc,5);
  bar(2,argc);
  return 0;
}
2.2.1 依赖计算
  • 核心的组合机制是计算和使用模块的(函数和全局变量)依赖关系。
    • 在但模块内 简化时,有bar的代码可以立即特化
    • 此时bar在另一个模块中定义,我们只能将foo记录未客户端模块的依赖
  • 为了对函数进行有意义的特化,我们需要知道关于参数值的信息。我们使用单例类型系统来表达这些信息 [8,16]。
    • 单例类型系统支持使用相等谓词对类型进行细化. 例如C 类型系统只能表达变量 x 是整数(int x)。使用单例类型,我们可以细化这个类型,不仅说明 x 是一个整数,而且其值为 5(int=5 x)。因此,我们简单程序的依赖关系如下:
      bar(int=?, int=5)// 使用int=?表示未知的int值
      bar(int=2, int=?)
      
  • 为了计算一个模块的依赖项,我们从模块的入口遍历调用图,并寻找外部符号引用.
    • 直接引用和调用点很简单;我们确定目标函数并记录信息
    • 间接函数调用和函数指针则更为复杂。我们依赖于 LLVM 工具计算调用图,这些工具采用标准的程序分析技术,如别名分析和控制流和数据流分析。这些分析提高了 OCCAM 解析间接函数调用的能力。
      • 当我们无法静态确定调用的目标时,我们必须记录对每个可能的目标函数的最一般化(即未特化)调用。这是因为我们可能在不进行全局代码重写情况下,更改二进制接口。例如,如果代码将 bar 的地址存储在函数指针中,我们必须保守地记录最一般的依赖关系:
    • 在某些情况下,模块内部剔除期间进行的部分评估将简化这个结构,使我们能够静态解析函数。虽然这阻止了进一步的特化,但在一般情况下是不可避免的,例如,当被特化的程序在表中查找函数指针时。
  • 要计算整个应用程序的依赖项,我们从包含程序入口点的模块开始,并遍历代码链接的库,如下图所示。为了处理循环模块依赖的一般情况,我们迭代这个过程,直到后续的依赖计算不产生额外的依赖项
    image
2.2.2 特化

image

  • 特化,使用每个客户模块的依赖性来决定要特化的函数
  • 如上图示,遍历依赖文件中的调用列表,确定是否以及如何特化每个函数。函数特化的工作方式与模块内情况相同
  • 即,复制函数体,删除常量参数,并将它们替换为函数体中。
  • 区别在,与模块内简化不同,我们无法访问函数调用点以重写它们来调用专门的函数. 因此,我们生成了一份重写规范,详细说明客户端如何修改以使用更具体地接口.
  • 以之前bar的特化举例,生成以下重写,此时只需调用一个参数
"bar"(int=?1, int=5) -> "bar_x_5"(int=?1)
"bar"(int=2, int=?2) -> "bar_2_x"(int=?2)
2.2.3 重写
  • 特化后,使用重写规范更新客户端模块. 我们遍历所有外部定义函数的调用点,并查找与调用点匹配的最精确的重写,更新调用点以使用新的函数名和参数
  • 例如之前的例子变为
int main(int argc, char* argv[]){
  bar_x_5(argc);
  bar_2_x(argc);
  return 0;
}
  • 特化步骤不会删除任何未特化的函数,因为不匹配任何规则的调用点将继续调用未专门化的函数
  • 重写本质上与模块内特化修改调用者相同,但需要向模块内添加新的函数原型,并从重写规范中重新读取.
  • 因为我们没有修改通用函数的数显,因此我们可以安全地忽略间接调用和将外部符号存储在变量中
2.2.4 封存
  • 重写完成后,模块内和跨模块的消除即可等效。在此步骤中,我们将对外部世界没有直接引用的符号变为内部,有效地覆盖了其他模块可以与我们的模块进行交互的漏洞。这允许模块内精简删除不可达代码并更激进地优化函数,因为它可以静态分析所有潜在的调用点。

  • 使用我们的接口,封装模块非常简单,如下图所示。我们只需迭代所有外部符号,并将任何未在接口中引用的符号变为内部。值得注意的是,拥有准确的接口对于此步骤的正确性至关重要,因为如果我们尝试在另一个模块中引用内部符号,链接将失败。

image

2.3 动态检查

  • 当我们能够在组件中静态确定接口的时候,简化效率最高。
  • 然而在某些情况下,比如通过函数指针进行调用时,确定函数如何调用将会变得困难。例如,在面向对象语言中进行动态分派或在动态语言的解释器中查找函数表时,以下说明一个实例
extern int (*foo)(int);
extern int (*bar)(int);

void go(bool foobar) {
    int (*f)(int) = foobar ? foo : bar;
    f(2); 
    f(3);
}
  • 此代码中,我们只知道 foo 和 bar 会以参数2,3调用,但重写代码并不简单,因为我们在对f的调用中要实用不同的函数。 在这里,我们必须要构建一个包装函数来检查参数并根据需要调度到特化版本,或者在违反接口时发出错误信号,以保持相同的二进制级别的接口。
  • 这里有两个潜在位置执行动态检查,在调用点或者函数定义处,各有不同的好处
2.3.1 重写客户端

第一种选择是重写客户端,即在调用点执行检查。
此时,库只导出特化函数(foo_2 和 foo_3 ),但客户端同时需要非特化的。为此,我们必须通过测试其参数并将其分配到合适的特化函数中来实现通用函数。如在客户端模块实现foo如下

extern int foo_2();
extern int foo_3();

inline int foo(int x) {
    if (x == 2) return foo_2(); // foo(2)
    if (x == 3) return foo_3(); // foo(3)
    exit(1);
}
  • 对客户端进行部分估值将导致该调用被内联,并且在x是静态已知的情况下删除条件。对于调用不符合接口的代码,将执行fall-through并发出错误信号
2.3.2 重写库
  • 另一种选择是重写库中的目标函数,这对于强制执行,无法静态验证的 库的接口很有用
    。它还可以在单独模块中进行使用,以解决间接调用的困难那,为此,OCCAM复制了旧的foo实现并进行重写
  • 我们将导出的函数foo替换为一个检查参数的函数(以强制执行foo只能用2,3调用),并相应的进行委托或终止客户端。 由于oldfoo在库中是内部的,有static修饰,模块内的简化将对其进行特化并最终从执行文件中删除通用版本
int foo(int x) {
    if (x == 2) return oldfoo(2);
    if (x == 3) return oldfoo(3);
    exit(1);
}
static int oldfoo(int x) { /* foo code */ }

3 实现

  • 我们实现了我们描述的所有技术,并应用于多个实例,以了解在实际应用中实用简化技术的实用性

  • 我们的代码作为一组LLVM编译器传递实现的。我们通过一组Python包装器实用LLVM opt程序运行它。这意味着简化的每个简短都会产生一个新的程序文件(或接口文件)。虽然这种方法不如纯C但不实体化中间结果的效率高,但它能够灵活地尝试其它转化和特化的启发式方法,我们的代码发布提供了一个教程,演示了我们如何使用我们的工具对应用程序进行简化

  • 为了创建程序的bit码,我们开发了一组脚本来包装现有的构建工具,这是因为大型应用程序的构建过程通常使用各种工具,如autoconf,libtool,cmake,make。 虽然这些工具支持各种平台和配置,但不支持将LLVM bit码作为目标架构

  • 为了与检查依赖项的现有构建脚本集成,我们的工具将每个命令翻译成两个命令(如调用gcc或ld)。

    • 第一个编译产生一个修改的版本来获取LLVMbit码
    • 第二个生成ELF标准版本,生成ELF对象确认构建成功,并确保所有的依赖选项都存在。
  • 虽然在构建速度方面不太理想,但我们的脚本相当健壮,能够编译大型程序和库,如PHP,SQLite,uClibc

4 例子研究

  • 首先回顾我们的目标

  • 首先,我们希望减少代码的功能,但这不一定意味着代码的减少;相反,它可能意味着减少可能的执行次数,复制一个函数会增加代码大小,但不会增加复杂性。同样地。在一个参数上特化一个函数,可以通过限制传递的值来减少其可能的行为。为了衡量这一点,我们可以查看应用程序在简化前后的调用图。

  • 次要目标是代码大小的减少和配置复杂性。

    • 大型库支持在粗粒度上启用功能,例如PHP中支持MySQL。
    • 简化为库用户提供了一种自动化方式,来精确地选择它们想要地功能。为衡量这一点,我们可以将简化与现有技术进行比较,例如使用库地静态链接(仅使用存档中的必要对像)
  • 我们的例子研究包括两个web服务器,nweb[3],thttpd[9],及两个web应用程序使用的PHP解释器

nweb

  • nweb是一个静态内容Web服务器,只有200多行C代码,它的大小和架构并不是简化的理想对象(小且配置少),但其可以展示简化的实际效果。 因为nweb仅依赖于libc,且非常简单,uClibc足以满足它的需求 (libc和uClibc都是C标准库,但uClibc是面向嵌入式系统的轻量级库)
  • 由于nweb通过命令行进行配置,我们根据我们希望提供的参数对main函数进行特化。为此我们使用命令nweb 8080 /root 告诉nweb监听端口并从root提供文件
    image
  • 上图展示了简化对nweb调用图的影响,图中的粗灰色框表示特化的函数。 简化前有27个函数,简化后只有17个,其余10个被特化或内联到11个不同的函数中,每个特化都减少了至少一个参数。这些特化包括
      1. log函数被复制了三次,每次都因不同的状态可以打印日志。通用函数已被删除,特化实例的部分求值去除了怪异的控制流,变成straight-line code程序(只有顺序结构),使其可以直接调用sprint,open,write
      1. libc中设置socket的函数(bind,listen和socket)每个被特化为处理TCP请求。其中socket函数最有趣,被特化为 面向 Internet(AF_NET) 和 面向流的socket(SOCK_STREAM)
      1. libc中处理软件中断的函数(signal)被特化两次以处理SIGCHLD和SIGHUP,这些特化可以使得其他工具可以轻松地确认程序仅以有限的方式响应中断

thttpd

  • thttpd是另一个考虑的Web服务器。其有许多功能,约8500行C代码,支持CGI,在启动时使用chroot,在内存中缓存,使用htpasswd身份验证。除了核心的libc功能外,thttpd还连接到libcrypt以保护其.htpasswd文件,httpd可以在运行时通过命令行参数配置,也可以在编译时使用一个头文件进行配置,该头文件同构一组49个宏控制代码生成。这些宏包括我们希望在简化中公开的,如.htpasswd的路径存储在AUTH_FIL宏中
  • 对thttpd进行简化会产生125个特化函数,主要分为三类
    • 定时器创建(tmr_create)的特化:所有这些调用都会被特化,包括在计时器到期时运行的函数指针。 这可以防止数据攻击。
    • 缓冲区分配(httpd_realloc_str):一个调用扩展了用于存储标头的缓冲区大小,该调用对前领个参数进行了特化,因为二者都是全局变量的地址 。httpd_realloc_str(&header, &maxheader, sizeof(headerstr) + strlen(realm) + 3);
    • 错误代码的特化。 因为我们的特化框架不支持可变参数函数,因此我们无法特化错误函数中对 snprintf的调用。 然而,常量的传播可以进行,因此,可以通过额外的特化来注入这些常量所需的特化

PHP

  • PHP [14] 是一个用于网站的流行编程语言。它附带了一个庞大的标准库,涵盖了从简单的字符串和日期操作到像chroot这样的POSIX系统调用的各种功能。这种“一揽子式”方法对于快速启动新应用程序非常有用。同时,这也为攻击提供了广泛的攻击面,特别是对于使用eval的应用程序。为了解决这个问题,我们可以对托管应用程序不需要的 PHP 运行时功能进行剪裁。
  • 为了演示这一点,我们构建了一个minblog[13]专用的PHP解释器,这是一个用PHP编写的小型博客框架。我们用我们的工具链自动地构建PHP解释器的LLVM二进制位码。在编译后,我们需要确定miniblog需要的接口,这里有两个复杂问题
      1. 首先PHP以文本形式提供给PHP解释器,这意味着我们无法使用LLVM通道来确定运行应用程序所需要的接口。PHP的静态评估超出了本文的范围。因此我们使用正则表达式来捕捉与PHP函数调用语法相对应的文本模式。之后,我们人工地检查结果,这个过程可以由语义的静态分析和在程序运行时记录跟踪的工具来替代。即便使用这些工具,我们认为由手动审核此列表仍然是有用的,以查看底层系统所依赖的内容。
      • 从静态分析我们可以确定miniblog依赖于PHP标准库中地52个函数,包括包括字符串和文件操作以及 MySQL 函数。为了比较,我们编译了可以使用 PHP 的编译选项配置的最小解释器(包括 PHP 标准库和 MySQL 扩展)的解释器。这包含 1029 个函数,包括可能危险的函数,如 system 和 mail。一些被消除的函数包含过去 PHP 版本中的漏洞(CVE-2011-1148 和 CVE-2010-2191)。这些漏洞在已经进行了精简的 PHP 部署中将不再存在。
      1. 第二个问题是PHP解释器的架构。PHP解释器讲库函数实现存储在一个可以动态调度的表中。因此,通过此表中的函数进行的所有调用都太过保守,所有函数都有可能是目标。即使我们可以使用类似 Smowton [12] 开发的技术将 PHP 代码拉入部分评估期间,使用 LLVM 优化器很难实现解决通过此表调用所需的部分评估。取而代之的是,我们注意此表的结构并使用第 2.3 节中描述的技术修改函数。在标准的 PHP 解释器中,每个库例程 Xxx 都在函数 zif_Xxx 中实现。因此,我们可以轻松编写钩子,将未使用的函数重写为错误。
  • 对简化miniblog,生成了997个形如zif_system(?) => fail的重写,处于审计和调试的目的,我们实现fail函数时记录了违规的函数调用

5 评估

性能

  • 我们比较了使用专门化和不使用专门化构建的每个服务器在请求率上的表现,以了解运行时的影响。

  • 性能以每秒请求数来衡量,用于提供单个小型静态页面。

  • 为了控制网络带宽,我们在 localhost:8080 上执行了测试。

  • 我们使用 Apache 基准测试工具 ab 来生成请求。

  • 我们运行了 40 次试验,每次对服务器进行 5000 次请求。

  • 测试在一台轻度负载的 Intel Core 2 四核桌面上运行,时钟频率为 2.4 GHz,配备 4 GB 的 RAM 和 Linux 2.6.38,并使用 TuxOnIce 补丁集

  • 为了进行公平比较,基线在链接之前使用LLVM-O3进行优化,两个版本都静态链接到uClibc-0.9.32,使用OCCAM构建并使用uClibc构建的标准配置选项进行优化(使用-O2)。

  • 下图显示了nweb和thttpd的比较结果
    image

  • nweb中差异在样本中变化较大,不具有统计意义

  • thttpd中,简化版本在95%的置信度上高于基线版本。这种性能改善可能有两个原因造成

    • 简化使用-O3作为其部分求值器,而标准配置使用-O2。这种差异可能会导致简化版本的代码更快。
    • 函数的复制暴露了常量,创造了更多的编译时优化机会

代码大小

  • 我们在简化中使用的激进特化策略导致httpd模块大小增加了45%,如下图示,这是部分评估技术已知的问题,其中大部分由于报错函数特化参数的复制引起的。这个问题可以使用更保守地启发式搜索解决,如果产生了显著的差异,其只会保留一个特化,

image

  • libcrypt和libc模块的二进制大小在简化后几乎没有增加,这是因为大多数函数都是叶子例程(例如字符串操作)或它们立即调用操作系统。因此,在这些单独的模块中,很少有机会内联或特化函数。额外的大小是由于对库的特化调用减去了静态链接器引入,但thttpd模块未使用的额外函数和常量。在高度优化的低级别库中,这些都不是显著的,因为档案中的每个对象通常只定义了最小数量的函数。

image

  • 许多常见的库,如libc,通常被编译为共享对象(或系统等效物),而不是静态链接。正常的共享对象包含整个库,与这样的库动态链接的任何应用程序都会引入大量不相关的功能。通过在构建共享对象之前应用精简,我们可以大大减少这种情况。

image

  • 图9展示了应用第4节技术于PHP的结果。
    • min表示使用PHP编译配置选项可以构建的最小PHP模块。它的大小为5.5 MB,其中4.0 MB(用虚线标记)是解释器(没有导出的PHP函数)。剩下的1.5 MB是标准库。
    • +mysql表示添加了MySQL支持的二进制文件。它需要额外的4%存储(不包括MySQL库)。
    • +mysql-sys是添加了一个策略以阻止对11个危险的PHP函数的调用后的二进制文件。
    • miniblog表示在对miniblog的接口进行精简之后得到的解释器版本。在模块内精简之后,LLVM比特码的大小从5.8 MB缩小到4.2 MB,减少了27%;这只比核心解释器大5.5%。从精简中减小PHP解释器二进制大小的效果类似于我们在thttpd案例中看到的libc共享对象的最小化(如图8所示)。

6 结论

  • 我们开发了OCCAM工具,用于将应用程序特化到其部署环境
  • 我们解释了为什么我们的工具可以精确地从应用程序中移除功能
  • 我们还认为,少量的手动工作可以用于解决优于信息的不完整引起的困难(如,不知道解释器正在运行的程序或不精确的别名分析)