Clang前端使用LLVM Pass示例
Posted 吴建明
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Clang前端使用LLVM Pass示例相关的知识,希望对你有一定的参考价值。
Clang前端使用LLVM Pass示例
https://mp.weixin.qq.com/s/e3e4a7ei61O99-JUWjDbnA
Objective-C在函数hook的方案比较多,但通常只实现了函数切片,也就是对函数的调用前或调用后进行hook,这里介绍一种利用llvm pass进行静态插桩的另外一种思路,希望起到抛砖引玉的作用,拿来实现更多有意思的功能。
1. Objective-C中的常见的函数Hook实现思路
Objective-C是一门动态语言,具有运行时的特性,所以能选择的方案比较多,常用的有:method swizzle,message forward(aspectku),libffi,fishhook。但列举的这些方案只能实现函数切片,也就是在函数的调用前或者调用后进行Hook,但比如想在这函数的逻辑中插入桩函数(如下),常见的hook思路就没办法实现了。
- (NSInteger)foo:(NSInteger)num
NSInteger result = 0;
if (num > 0)
// 往这里插入一个桩函数:__hook_func_call(int,...)
result = num + 1;
else
// 往这里插入一个桩函数:__hook_func_call(int,...)
result = num + 2;
return result;
为了解决上述问题,接下来介绍如何利用在编译的过程中修改对应的文件,实现把桩函数插入到指定的函数实现中。例如以上的函数,插入桩函数之后的效果(在函数打个断点,然后查看汇编代码,就能看到对应的自定义桩函数)。
那么如何自定义Clang命令,利用llvm Pass实现对函数的静态插桩,下面分为两部分,一部分是llvm Pass,另外一部分是自定义Clang的编译参数。两者合起来实现这个功能。
2. 什么是LLVM Pass
LLVM Pass是一个框架设计,是LLVM系统里重要的组成部分,一系列的Pass组合,构建了编译器的转换和优化部分,抽象成结构化的编译器代码。LLVM Pass分为两种Analysis pass(分析流程)和Transformation pass(变换流程)。前者进行属性和优化空间相关的分析,同时产生后者需要的数据结构。两都都是LLVM编译流程,并且相互依赖。
由于 LLVM 良好的模块化,因此直接写一个优化遍来实现优化算法的方法是可行
的,也是相对容易的。编写 pass 的流程是:
1)挑选测试用例 foo.c 作为 LLVM 编译器的输入。
2)利用 clang 前端生成 LLVM 中间表示 foo.ll,通过 LLVM 后端的 CodeGen 生
成 Target 代码(Target 是目标平台)。命令是 clang -emit-llvm foo.c –S –o foo.ll,需要参考的文档可能包括 LLVM Command Guide。
3)生成目标平台的汇编代码,命令是 llc foo.ll –march=Target –o foo.s,参考文档
Writing an LLVM Backend[46],The LLVM Target-Independent Code Generator[47]。
4)使用汇编器和链接器,将 foo.s 编译成平台可执行 exe 文件。执行测试程序的
执行时间。
5)用 oprofile 等性能分析工具对程序做 profiling,找出程序的热点,也就是程序
的性能瓶颈,看汇编中哪段代码耗时比较多,有可提升的空间。
6)在分析 foo.s 后,找出程序的缺陷,分析一般形式,提出改进后的目标代码
foo_opt.s。
7)找出与热点代码相对应的 IR,在对 IR 实现理解的基础上,结合改进的目标代
码,写出改进后的 IR。这是最关键的一步,因为 IR 到目标代码之间还要进行很多的优
化、转化,必须对程序以及 IR 进行足够的分析,才能知道什么样的 IR 可以生成期望的
汇编代码。这需要参考一些 LLVM 的文档,包括 LLVM Language Reference Manual,
LLVM’s Analysis and Transform Passes。
8)编写 LLVM 转化 Pass,参考文档 LLVM Programmer’s Manual,LLVM Coding
Standards,Doxygen generated documentation,Writing an LLVM Pass。
该过程在图 4.7 中给出。通过上面的步骤就可以实现一个优化遍。该优化算法最重
要解决的问题就是如何使数组地址能够实现自增,以及在何处插入 PHI 结点。
图4.7 编写pass流程
常见的应用场景有代码混淆 、单测代码覆盖率、代码静态分析等。
3. 编译过程
这里“插桩”的思路就是利用OC编译过程中,使用自定义的Pass(这里使用的是transformation pass),来篡改IR文件。比如上述的代码,如果不加入自定义的Pass(左图)加入自定义的Pass(右图)编译出来的IR文件,可以看到两者在对应的基础块不同的地方。
4. LLVM IR文件的描述
LLVM IR(Intermediate Representation)直译过来是“中间表示”,它是连接编译器中前端和后端的桥梁,它使得LLVM可以解析多种源语言,并为多个目标机器生成代码。前端产生IR,而后端消费它。更多的介绍看这个视频LLVM IR Tutorial。
5. 下载LLVM
苹果fork分支https://github.com/apple/llvm-project选择一个新apple/main那个分支即可。
clone下来之后,在编译之前,要实现想要的效果,需要处理两个问题:
6. 写自定义的Pass
1)编写插桩的代码
也就是llvm pass,这里主要是要插入代码,所以用的是transformation pass
在llvm/include/llvm/Transforms/新增一个文件夹(InjectFuncCall),然后上面放着LLVM Pass的头文件声明
新建头文件:
namespace llvm
class InjectFuncCallPass : public PassInfoMixin
public:
/// 构造函数
/// AllowlistFiles 白名单
/// BlocklistFiles 黑名单
explicit InjectFuncCallPass(const std::vector &AllowlistFiles,const std::vector &BlocklistFiles)
if (AllowlistFiles.size() > 0)
Allowlist = SpecialCaseList::createOrDie(AllowlistFiles, *vfs::getRealFileSystem());
if (BlocklistFiles.size() > 0)
Blocklist = SpecialCaseList::createOrDie(BlocklistFiles, *vfs::getRealFileSystem());
PreservedAnalyses run(Module &M, ModuleAnalysisManager &MAM);
bool runOnModule(llvm::Module &M);
private:
std::unique_ptr Allowlist;
std::unique_ptr Blocklist;
;
// namespace llvm
在llvm/lib/Transforms新增一个文件夹(InjectFuncCall),然后上面放着对应的LLVM Pass的cpp文件新建cpp文件:llvm/lib/Transforms/InjectFuncCall/InjectFuncCall.cpp
using namespace llvm;
bool InjectArgsFuncCallPass::runOnModule(Module &M)
bool Inserted = false;
auto &CTX = M.getContext();
for (Function &F : M)
if (F.empty())
continue;;
if (F.isDeclaration())
continue;
if (F.getLinkage() == GlobalValue::AvailableExternallyLinkage)
continue;
if (isa(F.getEntryBlock().getTerminator()))
continue;;
if (Allowlist && !Allowlist->inSection("Inject-Args-Stub", "fun", F.getName()))
continue;
if (Blocklist && Blocklist->inSection("Inject-Args-Stub", "fun", F.getName()))
continue;
IntegerType *IntTy = Type::getInt32Ty(CTX);
PointerType* PointerTy = PointerType::get(IntegerType::get(CTX, 8), 0);
FunctionType *FuncTy = FunctionType::get(Type::getVoidTy(CTX), IntTy, /*IsVarArgs=*/true);
FunctionCallee FuncCallee = M.getOrInsertFunction("__afp_capture_arguments", FuncTy); // 取到一个callee
for (auto &BB : F)
SmallVector<value*, 16=""> CallArgs;
for (Argument &A : F.args())
CallArgs.push_back(&A);
Builder.CreateCall(FuncCallee, CallArgs);
Inserted = true;
return Inserted;
PreservedAnalyses InjectArgsFuncCallPass::run(Module &M,
ModuleAnalysisManager &MAM)
bool Changed = runOnModule(M);
return (Changed ? llvm::PreservedAnalyses::none()
: llvm::PreservedAnalyses::all());
2)CMake相关声明和配置
llvm/utils/gn/secondary/llvm/lib/Transforms/InjectArgsFuncCall/BUILD.gn中需要添加以下声明,才会创建一个对应的静态库。
static_library("InjectFuncCall")
output_name = "LLVMInjectFuncCall"
deps = [
"//llvm/lib/Analysis",
"//llvm/lib/IR",
"//llvm/lib/Support",
]
sources = [ "InjectFuncCall.cpp" ]
llvm/utils/gn/secondary/llvm/lib/Passes/BUILD.gn添加一行:“//llvm/lib/Transforms/InjectFuncCall”
"//llvm/lib/Transforms/Scalar",
"//llvm/lib/Transforms/Utils",
"//llvm/lib/Transforms/Vectorize",
"//llvm/lib/Transforms/InjectFuncCall",
]
sources = [
"PassBuilder.cpp",
llvm/lib/Transforms/CMakeLists.txt添加一行代码。cmake声明工程新增一个子目录。
add_subdirectory(ObjCARC)
add_subdirectory(Coroutines)
add_subdirectory(CFGuard)
add_subdirectory(InjectArgsFuncCall)
llvm/lib/Passes/CMakeLists.txt添加一行代码。声明Pass Build会链接 “InjectFuncCall” COMPONENTS
add_llvm_component_library(LLVMPasses
PassBuilder.cpp
PassBuilderBindings.cpp
PassPlugin.cpp
StandardInstrumentations.cpp
ADDITIONAL_HEADER_DIRS
$LLVM_MAIN_INCLUDE_DIR/llvm
$LLVM_MAIN_INCLUDE_DIR/llvm/Passes
DEPENDS
intrinsics_gen
LINK_COMPONENTS
AggressiveInstCombine
Analysis
Core
Coroutines
InjectArgsFuncCall
IPO
InstCombine
ObjCARC
Scalar
Support
Target
TransformUtils
Vectorize
Instrumentation
)
7. 自定义Clang命令
如何让Clang识别到自定义的命令和根据需要要加载对应的代码呢,需要修改以下几处地方。
在llvm-project/clang/include/clang/Driver/Options.td文件里面:
1)添加命令到Driver
文件很长,一般加在sanitize相关的配置后面。搜索end-fno-sanitize* flags,往下一行插入。
// 开始自定义的命令到Driver
def inject_func_call_stub_EQ : Joined<["-","--"],"add-inject-func-call=">, Flags<[NoXarchOption]>,HelpText<"Add Inject Func Call">;
def inject_func_call_allowlist_EQ : Joined<["-","--"],"add-inject-allowlist=">, Flags<[NoXarchOption]>,HelpText<"Enable Inject Func Call From AllowList">;
def inject_func_call_blocklist_EQ : Joined<["-","--"],"add-inject-blocklist=">, Flags<[NoXarchOption]>,HelpText<"Disable Inject Func Call From BlockList">;
def inject_func_call : Flag<["-","--"],"add-inject-func-call">, Flags<[NoXarchOption]>, Alias, AliasArgs<["none"]>, HelpText<"[None] Add Inject Func Call.">;
// 结束自定义的命令到Driver
2)添加命令到Fronted cc1
//===----------------------------------------------------------------------===//
// 自定义插桩 Options
//===----------------------------------------------------------------------===//
def inject_func_call_type : Joined<["-"],"inject_func_call_type=">, HelpText<"CC1 add args stub [bb,func]">;
def inject_func_call_allowlist : Joined<["-"],"inject_func_call_allowlist=">, HelpText<"CC1 add args from allow list">;
def inject_func_call_blocklist : Joined<["-"],"inject_func_call_blocklist=">,
llvm-project/clang/lib/Driver/ToolChains/Clang.cpp
3)添加Driver到Fronted之间的命令链接
在ConstructJob这个函数里面添加Driver到Fronted之间的命令链接
void Clang::ConstructJob(Compilation &C, const JobAction &JA,const InputInfo &Output,
const InputInfoList &Inputs,const ArgList &Args,
const char *LinkingOutput) const
...
...
const SanitizerArgs &Sanitize = TC.getSanitizerArgs();
Sanitize.addArgs(TC, Args, CmdArgs, InputType);
/// 添加Driver 到Fronted之间的命令的链接
if(const Arg *arg = Args.getLastArg(options::OPT_inject_func_call_stub_EQ))
StringRef val = arg->getValue();
if (val != "none")
CmdArgs.push_back(Args.MakeArgString("-inject_func_call_type=" + Twine(val)));
StringRef allowedFile = Args.getLastArgValue(options::OPT_inject_func_call_allowlist_EQ);
llvm::errs().write_escaped("Clang:allowedFile:") << allowedFile << \'\\\\n\';
CmdArgs.push_back(Args.MakeArgString("-inject_func_call_allowlist=" + Twine(allowedFile)));
StringRef blockFile = Args.getLastArgValue(options::OPT_inject_func_call_blocklist_EQ);
llvm::errs().write_escaped("Clang:blockFile:") << blockFile << \'\\\\n\';
CmdArgs.push_back(Args.MakeArgString("-inject_func_call_blocklist=" + Twine(blockFile)));
...
...
这文件/llvm-project/clang/lib/Frontend/CompilerInvocation.cpp中处理第四步。
4)参数赋值给Option
把解析逻辑中,真正拿到clang传进来的参数赋值给Option,需要给Option新增几个变量。
在对应的文件/clang/include/clang/Basic/CodeGenOptions.h
/// type of inject func call std::string InjectFuncCallOption; /// inject func allow list std::vector InjectFuncCallAllowListFiles; /// inject func block list std::vector InjectFuncCallBlockListFiles;bool CompilerInvocation::ParseCodeGenArgs(CodeGenOptions &Opts, ArgList &Args, InputKind IK, DiagnosticsEngine &Diags, const llvm::Triple &T, const std::string &OutputFile, const LangOptions &LangOptsRef) ...for (const auto &Arg : Args.getAllArgValues(OPT_inject_args_stub_type)) StringRef Val(Arg); Opts.InjectArgsOption = Args.MakeArgString(Val); Opts.InjectArgsAllowListFiles = Args.getAllArgValues(OPT_inject_args_stub_allowlist); Opts.InjectArgsBlockListFiles = Args.getAllArgValues(OPT_inject_args_stub_blocklist);...
5)将自定义的Pass添加到Backend
在emit assembly的时机,判断Option,然后执行Model Pass Manager 的add Pass操作。
对应的文件/clang/lib/CodeGen/BackendUtil.cpp
#include "llvm/Transforms/InjectArgsFuncCall/InjectArgsFuncCall.h"
// 最后添加 Inject Args Function Pass。
if (CodeGenOpts.InjectArgsOption.size() > 0)
MPM.addPass(InjectArgsFuncCallPass(CodeGenOpts.InjectArgsAllowListFiles, CodeGenOpts.InjectArgsBlockListFiles));
8. 编译llvm
上述的配置和代码都搞完之后,接下来编译,编译的过程直接看github的readme,安装必要的工具cmake,najia等。
cd llvm-project
// 新建一个build文件夹来生成工程
mkdir build
cd build
// -G Xcode会cmake出来一个xcode工程,也可以选择ninja
cmake -DLLVM_ENABLE_PROJECTS=clang -G Xcode ../llvm
// 执行结束后,会在build文件夹生成完整的工程目录
目前LLVM,只能用Legacy Build System。所以需要在文件→项目设置→构建系统里面切换一下。
9. 执行结果验证
1)生成IR文件调试效果
打开llvm的工程,选择clang的target,设置Clang的运行参数。
2)把上述的的路径替换成自己的路径
// 指定使用new pass manager,llvm里面有两套写自定pass的接口,现在是使用新的接口。
-fexperimental-new-pass-manager
// 启动功能,以基础块级别地插入函数
-add-inject-func-call=bb
// 设置白名单,只有在白名单里面的文件/函数才会插桩
-add-inject-allowlist=$(SRCROOT)/config/allowlist.txt
// 设置黑名单,黑名单里指定的文件/函数会忽略掉
-add-inject-blocklist=$(SRCROOT)/config/blocklist.txt
3)白名单&黑名单
简单的格式:
#指定对应的section
[InjectFuncCallSection]
# 指定对应的文件
src:/OC-Hook-Demo/OC-Hook-Demo/Foo.m
# 指定对应的函数名,*号可支持模糊匹配
func:*foo*
白名单和黑名单是参考Clang Sanitizer配置文件的格式。
参考文献链接
https://mp.weixin.qq.com/s/e3e4a7ei61O99-JUWjDbnA
使用 LLVM 处理异常时出错
【中文标题】使用 LLVM 处理异常时出错【英文标题】:Error in exception handling with LLVM 【发布时间】:2012-01-29 17:26:07 【问题描述】:我正在尝试使用 CLANG++ 作为前端和后端作为 LLVM 来编译 C++ 代码。 版本是3.0。 异常处理似乎有问题。每当代码抛出异常时,程序就会终止并显示“抛出异常后终止”的消息。
这是我尝试使用 CLANG ++ 的示例代码之一。
struct A ;
struct B : virtual A ;
struct C : virtual A ;
struct D : virtual A ;
struct E : private B, public C, private D ;
extern "C" void abort ();
void fne (E *e)
throw e;
void check(E *e)
int caught;
caught = 0;
try fne(e);
catch(A *p) caught = 1; if (p != e) abort();
catch(...) abort();
if (!caught) abort();
caught = 0;
try fne(e);
catch(B *p) abort ();
catch(...) caught = 1;
if (!caught) abort();
caught = 0;
try fne(e);
catch(C *p) caught = 1; if (p != e) abort();
catch(...) abort();
if (!caught) abort();
caught = 0;
try fne(e);
catch(D *p) abort ();
catch(...) caught = 1;
if (!caught) abort();
return;
int main ()
E e;
check (&e);
check ((E *)0);
return 0;
我对 LLVM 很陌生,所以对它不太了解。它也有与 LLVM 生成异常处理表相关的任何内容。 对于任何代码,上述问题都会继续存在。 我已经在 Linux 机器上编译了上面的代码。 我还尝试将 printf 放在每个 catch 子句上,但没有响应。所以似乎在抛出异常时,没有找到匹配的异常捕获,并导致调用终止函数
【问题讨论】:
在底部阅读此答案的注意:***.com/a/8883505/733152 您遗漏了最重要的一点 - 哪个操作系统? 我修改了评论。在linux机器上 @rahul:与其调用abort
,不如打印一条消息更有趣,这样您就可以在一次测试中知道每种情况下采用的路径。请注意,在确定其基数时,我不太确定抛出空指针的行为......它很可能会调用未定义的行为。
@MatthieuM。已经尝试在每个 catch 子句中添加注释,但似乎异常不仅仅被捕获,而是直接发送到终止函数,即它没有找到任何匹配的异常捕获。
【参考方案1】:
看到您的其他问题...如果您使用的是 arm/linux - 那么这样的结果是可以预期的。对 EH 的支持还没有结束,所以可能会被任意破坏。
【讨论】:
以上是关于Clang前端使用LLVM Pass示例的主要内容,如果未能解决你的问题,请参考以下文章