|
作者:remyliu针对业务问题,本文研究了多种计算引擎实现方案,并基于Clang/LLVM实现了一个C/C++解释器,同时还探讨了相关的Clang编译技术在实现过程中的应用。业务面临的问题特征计算系统的演进从工程角度来看,对日志流量进行分析是安全业务研发的重要内容。如果将与“坏人”进行安全对抗比作一场长期持久的战争,那么特征计算系统就是对抗“坏人”的重要武器系统。该系统的功能是消费日志流,进行分析计算,并输出特征信息。在传统模式下,各个特征计算模块分散、无管理、缺乏标准化,难以与其他武器系统对接,导致特征开发效率低下,进而使特征计算武器系统的威力不足。下图是特征计算模块的开发流程:流程漫长费时效率低,满足不了安全武器系统的快速响应打击“坏人”的需求。为了解决上述问题,我们研发了新一代的特征计算系统,架构图如下:在新系统中,我们将计算逻辑脚本化,极大的简化了开发流程,并且做了大量的标准化工作。例如现在需要计算一个特征数据z,计算逻辑是对输入日志流的数据x和y的求和,开发人员只需要在Web页面编辑脚本:z=x+y,然后点击发布脚本就可迅速上线,快速的输出特征。在上述的架构中,执行引擎执行用户编辑的计算逻辑,如z=x+y,对输入数据进行计算,输出需要的特征,是系统的核心组件。特征计算引擎探索执行引擎的实现有多种方案可选,如下图所示的6种方案。每个方案都有各自的优劣,实际工程可以根据需求进行选择或组合。在业界,许多选择使用Python引擎、Lua引擎或两者的组合来执行用户编辑的Python脚本或Lua脚本。下面描述了各个方案并列出了各方案的特点:微信安全采用的是一个自研的DSL引擎,并在此基础上扩展。原因是性能相对高,并且已被其他重要安全系统使用验证。DSL(Domain-SpecificLanguage)是用于特定领域的编程语言,例如SQL就是一种DSL。我们自研DSL引擎,实际上是开发了一种自定义的编程语言,使用这种编程语言来编写特征计算逻辑。要实现一种编程语言,当然要实现这种语言的编译器和执行器,下面将介绍DSL引擎的实现和存在的问题。微信特征计算引擎:DSL引擎实现下图实现展示了微信自研DSL语言的实现,首先定义了词法描述文件和语法描述文件,采用Lex和Yacc生成词法分析器Lexer和语法解析器Parser,在这里Parser的输出逆波兰表达式,存储在内存中,然后解释执行表达式。整个DSL的引擎可以分为2部分:编译和执行,编译1次,然后对每条输入数据解释执行编译后的表达式。DSL引擎的问题在业务接入和运营过程中发现3个主要的问题:DSL新语言推广学习成本高自研DSL是一门新的语言,业务不熟悉使用,业务同学从原来的C++开发计算特征,转为使用DSL,存在大量疑问,需要大量的研发支持,尽管已经提供了丰富的文档支持。这无疑是公司内推广/公司外开源的阻碍,在缺少研发的大力支持下,大家愿意学习新的DSL语言吗?使用业务通用熟悉的语言,可以更好的提升影响力,减少接入阻碍,需要的研发支持也更少。前面也提到特征计算系统采用的是一个自研的DSL引擎,并在此基础上扩展,为什么原来DSL语言不存在上述问题。因为原来DSL用于安全策略场景,主要是做逻辑判断和条件判断,例如支持+-*/和ifelse等简单操作即可,很容易上手,反而不需要复杂的语言特性。但是特征计算场景,侧重于计算,需要大量的计算函数,库函数,rpc调用等,需要的语言语法特性复杂的多,因为扩展的DSL也变得复杂,由此诞生了上述的问题。大量重复实现已有的库实现一门可用性好的编程语言,除了实现语言本身,需要需要实现大量的基础库,例如需要实现字符串string库,http库,protobuf库,vector和map等数据结构,自研DSL也一样,需要耗费大量精力在重新实现这些库上,而且随业务需求还一直在更新,不能完备。并且自研的库函数使用风格也和C++库使用有较大差别,学习成本高。下面是DSL语言和库与C++的对比,微信后台有成熟的C++基建,大家很熟悉C/C++语法。其他方面的问题DSL编译过程中无通用的中间表示,无法使用业界已有的程序优化算法,所以性能仍然不是很高。DSL的编译报错提示不友好不准确,因为语法解析器Parser采用的是Yacc工具生成,Yacc使用的是LALR算法,该算法缺陷之一是编译报错提示不够准确友好,实际使用过程中也是如此,业务同学也是常咨询“这段DSL代码哪里错了?”。另外一个是扩展性较差,例如我们想基于DSL的parser实现一个类似clangd的代码补全和提示工具,提升DSL脚本开发体验,几乎很难实现,因为DSL的编译器实现紧耦合没有模块化,我们只能基于很原始的字符串匹配来实现代码补全提示。探索新引擎方案C++执行引擎微信后台主要使用C++作为编程语言,基础设施基本是以C++模块构建的,并积累了丰富的C++库。在安全业务中,一开始就选择了使用C++语言进行特征计算。如果将脚本语言也采用C++,业务同学可以熟练地使用,并且可以兼容现有的C++库和标准库,无需重新开发各种库。然而,C++是一种静态编译语言,是否能改为解释执行呢?我们进行了调研,并基于Clang前端和LLVMJIT技术实现了一个C++执行引擎,即一个C++解释器。其结构如下:DSL引擎面对的问题C++引擎都可以完美的解决,C/C++语言容易接入学习成本低,开源易提升影响力;支持的库丰富无需重复开发;最好的LLVM编译优化和JIT执行带来了和二进制执行一样的高性能,基于Clang前端因此有世界上最友好的C/C++编译报错提示,同样得益于Clang和LLVM模块话带来了极强的扩展性。举几个例子说明C++引擎的扩展性,例如我们可以基于Clang的前端库实现类型clangd的代码补全提示。采用这个结构还能快速的支持其他语言,例如rust语言作为开发语言;除了JIT执行,还能扩展生成WebAssembly,通过v8执行。引擎实现:C/C++解释器ccintC/C++是静态编译语言,但C/C++能否解释执行呢?答案是Yes,本文基于Clang和LLVM,不到500行代码,实现了C/C++解释器ccint,ccint源代码在GitHub可获取。其结构如下图所示:C/C++文件被Clang前端经过预处理,词法分析,语法分析,语义检查,编译成LLVM中间表示,即LLVMIR。注意Clang前端并不是Clang二进制程序,而是Clang编译器提供的前端库,LLVMIR经过LLVM优化器,根据优化级别生成优化后的LLVMIR存储在内存中,常见的优化有常量传播,常量折叠,死代码删除,循环向量化等等。优化后的LLVMIR被LLVMORCJIT执行,输出结果。JIT的执行使用了LLVM后端代码生成技术,输入LLVMIR输出二进制指令到内存,然后调用指定的函数符号执行。使用ccint解释器输出"helloworld"/* main.cpp */#include #include #include void ccint_main() { std::vector vec = {"hello", " world\n"}; for (auto &s : vec) { printf("%s", s.c_str()); }}$ ./ccint main.cpphello world上面的例子使用标准库的vector类和string类以及printf函数,解释器执行函数ccint_main,可以看到解释器很好的支持了C/C++标准库。ccint解释器还有有如下的特性支持完整的C++11/C++14/C++17语法;支持标准库/动态库/静态库;采用了JIT技术因此和C/C++二进制有相同的性能;模块化编译和执行分离,方便使用到业务上。ccint解释器在GitHub还有展示动态库静态库和指定头文件搜索路径例子,可以参考。ccint灵感来源于cling,cling是一个基于Clang和LLVM的交互式C/C++解释器,由欧洲核子研究中心开发,用于处理大型强子对撞机LHC的实验数据和验证实验模型,目前已处理EB级别的实验数据。然而直接使用cling并不必要,因为cling自身的代码已经达到了3万行以上,其中大部分代码是为了适配物理实验领域的需求。此外cling对Clang和LLVM进行了较大的修改,并未合并到LLVM主线,这将需要大量的后续维护投入。参考cling的实现思路,借助于Clang和LLVM这两个强大的工具,我们只需编写很少的代码(几百行)就能实现功能丰富的C/C++解释器。后文将依次具体探讨实现C/C++引擎使用到的Clang前端技术。初识LLVMLLVM(Low-LevelVirtualMachine)是一个编译器开发工具集,和虚拟机(VirtualMachine)没任何关系。LLVM主要包括如下工具和库:一个源语言无关,目标架构无关的编译优化器,一个目标架构无关代码生成器,C/C++编译器Clang,LLDB调试器,LLD连接器,libc++库等,其中编译优化器和代码生成器是LLVM的核心。为什么需要LLVM?LLVM解决了什么问题?传统的结构是三段式,由前端,优化器,后端组成,并且紧耦合,如果新实现一个编程语言或者新增一个指令集ISA,都需要重新实现这三段,而且优化器不独立,程序优化即需要考虑语言特征,又需要考虑机器特性,难以专注优化算法本身。LLVM将传统的三段式结构中优化阶段单独提取出来,并引入了一个通用的代码中间表示LLVMIR,这样前端研发人员只需要关注SourceCode到LLVMIR的过程,专注前端的相关的算法如新的parser算法和语义检查;而编译优化研发人员只需要专注优化算法的开发,因为中间表示LLVMIR和源代码无关,指令集架构ISA无关。后端研发只需要专注适配新的ISA,优化代码生成框架,优化指令选择,指令调度,寄存器分配等后端算法。大家术业有专攻,极大的繁荣了LLVM生态。如果需要研发新的编程语言,例如研发Rust语言,只需要研发语言的前端,就可以适配所有ISA。如果需要增加新的ISA,例如新指令集架构RISC-V,只需要采用LLVMTarget-IndependentCodeGenerator开发一个新的后端,RISC-V后端就可以支持所有的语言。如果需要新增新的编译优化算法,只需往CommonOptimizer加入新算法,不需要了解语言特征,也不需要了解架构特性。ClangClang是LLVM项目中一个C家族语言编译前端,支持C,C++,ObjectiveC/C++,OpenCL,CUDA等的编译,Clang的设计之初就注重模块化,各个子模块都提供了库,能基于这些库实现一些非常多个工具,如常用的C++代码linter工具clang-tidy代码补全工具clangd,Clang的报错提示也非常的友好,这两方面相对GCC都有巨大的优势。日常我们使用Clang包含两方面含义:Clang驱动器和Clang前端,后续将分别介绍这两方面内容,并重点讨论Clang前端。Clang驱动器日常使用的Clang工具就是一个驱动器,驱动整个编译的流水线,将C/C++编译成二进制,如下图Clang驱动Clang编译前端Frontend,汇编器Assembler,连接器Linker等。以一个例子说明int factorial(int n) { if (n
|
|