本文是个人发表在个人博客的两篇文章的合集,介绍 llvm 动态加载 pass 的原理细节。
背景
不知道什么时候,不知道在哪看到,说llvm的 PassManager 更新了。
2021年有读者反馈llvm14加载pass失败,我当时没空处理,而且认为是读者自身问题。
2022年5月7日,immortal学弟使用 llvm13 prebuilt library 加载pass,不报错也没输出。刚巧我一直没验证过 prebuilt的可用性,本地复现后,发现llvm12 prebuilt是完全没问题的,llvm13就不行,才发现可能是版本更替引起的。
于是,找到了高版本 llvm 对 Pass Manager 的变更,https://releases.llvm.org/13.0.0/docs/ReleaseNotes.html#changes-to-the-llvm-ir , https://releases.llvm.org/14.0.0/docs/ReleaseNotes.html#changes-to-the-llvm-ir ,确认了该问题。
对于 llvm 13 和 14,临时解决方案是使用 -flegacy-pass-manager
,让之前写的 pass 继续生效。
最终,只剩一个问题,如何正确使用新版的 PassManager。在这之前,我需要先对 legacy pass manager 做一个总结。
近期 Pass Manager 的变更
版本 |
默认行为 |
可选参数 |
llvm5~llvm12 |
使用 LegacyPassManager |
-fno-experimental-new-pass-manager 启用 NewPassManager |
llvm13~llvm14 |
使用 NewPassManager |
-flegacy-pass-manager 启用 LegacyPassManager |
llvm15(开发中) |
使用 NewPassManager |
可能移除 LegacyPassManager |
旧版新版并存已经5年了,很多开发者在 opt 里进行 transform,新版的API长期也没人适配clang。
llvm13 是 2021年10月发布的,过去半年了,居然没人来更新相关的方案,属实不应该。
LegacyPassManager 动态加载过程
本文带大家从源码的角度,分析从命令行输入,到pass动态注册的整个过程,以之前常用做测试的 clang -Xclang -load -Xclang libSkeletonPass.so test.c
为例。
环境准备,见第十九篇 ( https://leadroyal.cn/p/2206/ ),肉眼读源码很费劲,结合调试,可以更快找到核心代码位置,更加理解整个逻辑
clang 和 clang -cc1
clang
并不仅仅是c语言前端,它是一个编译器合集,包括了编译、优化、链接的各个过程,可以使用 -v
参数简单观察一下编译全程的细节。它会调用clang -cc1
,clang -cc1
是真正编译器。
clang -Xclang -load -Xclang libSkeletonPass.so test.c
的 -Xclang
就是将参数传递给 clang -cc1
,最终调用命令是:clang -cc1 -load libSkeletonPass.so test.c
,而load功能是 clang -cc1
提供的。
二者的help内容也是完全不一样的,使用 -Xclang
进行参数传递。
1 2 3 4 5 6 7 | clang - - help
...
- Xclang <arg> Pass <arg> to the clang compiler
clang - cc1 - - help
...
- load <dsopath> Load the named plugin (dynamic shared object )
|
clang
的可执行文件位于:clang/tools/driver/driver.cpp
,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 | int main( int argc_, const char * * argv_) {
/ / * * * * * * * * * *
if (FirstArg ! = argv.end() && StringRef( * FirstArg).startswith( "-cc1" )) {
/ / If - cc1 came from a response file , remove the EOL sentinels.
if (MarkEOLs) {
auto newEnd = std::remove(argv.begin(), argv.end(), nullptr);
argv.resize(newEnd - argv.begin());
}
return ExecuteCC1Tool(argv);
}
/ / * * * * * * * * * *
}
|
当第一个参数是 -cc1
时,走一套逻辑,否则,走另一套逻辑。
此时,问题转化为:clang -cc1 -load libSkeletonPass.so test.c
的 load 功能在哪实现。
load功能在哪实现
这里盲猜,肯定是用到命令行参数解析技术、动态加载技术,与PassManager有关,朝着相关方向思考。
根据 help 内容搜索关键字,在 clang/include/clang/Driver/Options.td
中找到,它是个描述文件,生成头文件的名字已超出我的认知范围,这条路行不通,但生成的头文件一定是某种格式的。
1 2 | def load : Separate<[ "-" ], "load" >, MetaVarName< "<dsopath>" >,
HelpText< "Load the named plugin (dynamic shared object)" >;
|
根据编写 Pass 的API,找到 RegisterStandardPasses
的实现,位于llvm/include/llvm/Transforms/IPO/PassManagerBuilder.h
。
1 2 3 | static RegisterStandardPasses RegisterMyPass(
PassManagerBuilder::EP_EarlyAsPossible,registerSkeletonPass
);
|
1 2 3 4 5 6 7 8 | class RegisterStandardPasses {
PassManagerBuilder::GlobalExtensionID ExtensionID;
public:
RegisterStandardPasses(PassManagerBuilder::ExtensionPointTy Ty,
PassManagerBuilder::ExtensionFn Fn) {
ExtensionID = PassManagerBuilder::addGlobalExtension(Ty, std::move(Fn));
}
|
调用了 PassManagerBuilder::addGlobalExtension(Ty, std::move(Fn))
,传参为生效时刻、注册回调函数,并返回插件ID。
实现在 llvm/lib/Transforms/IPO/PassManagerBuilder.cpp
中。
将生效时刻、注册回调函数放到 GlobalExtensions
里,之后我们下断点,观察栈回溯,发现很多收获。
1 2 3 4 5 6 7 8 9 | llvm::PassManagerBuilder::addGlobalExtension(llvm::PassManagerBuilder::ExtensionPointTy, std::function<…>) PassManagerBuilder.cpp: 225
xxxxxxx
[Inlined] llvm::sys::DynamicLibrary::HandleSet::DLOpen DynamicLibrary.inc: 28
llvm::sys::DynamicLibrary::getPermanentLibrary DynamicLibrary.cpp: 154
[Inlined] llvm::sys::DynamicLibrary::LoadLibraryPermanently DynamicLibrary.h: 87
clang::ExecuteCompilerInvocation ExecuteCompilerInvocation.cpp: 209
cc1_main cc1_main.cpp: 240
ExecuteCC1Tool driver.cpp: 330
xxxxx
|
可以得知,Clang->getFrontendOpts().Plugins
中存放动态加载library。
得到阶段性结论:虽然我们不知道 -load
的实现在哪里,但它的作用是将传参结果放到了 Plugins 里,加载 library 已结束,但 pass 并没有加载或执行。此时,问题转化为了:GlobalExtensions
存放的注册时机和回调函数,是何时被 LegacyPassManager 加载的。
GlobalExtensions何时被LegacyPassManager读取
搜索 GlobalExtensions
的引用,很容易找到一处 llvm/lib/Transforms/IPO/PassManagerBuilder.cpp
:
1 2 3 4 5 6 7 | void PassManagerBuilder::addExtensionsToPM(ExtensionPointTy ETy,
legacy::PassManagerBase &PM) const {
/ / * * * * * * * * *
for (unsigned i = 0 , e = Extensions.size(); i ! = e; + + i)
if (Extensions[i].first = = ETy)
Extensions[i].second( * this, PM);
}
|
显然这里将 PassManager 传递到了回调函数中,API也是我们非常熟悉的,在恰当的时机,将PassManager 传递给用户自定义的处理函数,用户可以使用 PassManager 的 API将 pass 添加进去。
同样,调试,观察代码和栈回溯:
我随手使用的是llvm11版,默认不使用 NewPassManager,走 else 分支,也就是 LegacyPassManager。甚至还找到了 NewPassManager 的处理逻辑。
1 2 3 4 | void PassManagerBuilder::populateFunctionPassManager(
legacy::FunctionPassManager &FPM) {
addExtensionsToPM(EP_EarlyAsPossible, FPM);
FPM.add(createEntryExitInstrumenterPass());
|
我这个案例走的是 EmitAssembly -> CreatePass -> populateFunctionPassManager,触发的是 EP_EarlyAsPossible ,这个注册时机有多个,搜索 addExtensionsToPM 可以找到其他的 pass 被注册的时机,不是本文的重点。
此时,所有问题都解决了。
llvm13以上的版本呢?
不出所料只是简单修改了判定条件,clang/lib/CodeGen/BackendUtil.cpp
文件同样的位置:
1 2 3 4 | if (!CGOpts.LegacyPassManager)
AsmHelper.EmitAssemblyWithNewPassManager(Action, std::move(OS));
else
AsmHelper.EmitAssembly(Action, std::move(OS));
|
LegacyPassManager 的总结
- clang 命令行传参,传给 clang -cc1
- clang -cc1 解析
-load libSkeletonPass.so
,注册时机和回调函数被存储到 GlobalExtensions
- 初步编译后,触发优化部分
EmitAssembly -> CreatePass -> populateFunctionPassManager
调用链,最终调用到 addExtensionsToPM
,访问 GlobalExtensions
,如果当前时机和注册时机相同,就调用回调函数
- 回调函数的入参为
legacy::PassManagerBase &PM
,调用 addPass
即可将自定义的 Pass 加入到优化流程中。
预告
上文提到了,我偶然发现了 CGOpts.ExperimentalNewPassManager
这个配置项,显然就是 llvm13以上使用的 NewPassManager 了,整好省下了重新编译的时间。因此,下一篇将介绍,如何在 llvm11 上,使用 NewPassManager 动态加载用户的 Pass 的原理。
本文主要讲 clang 使用 New Pass Manager 动态加载Pass的原理。
从 EmitAssemblyWithNewPassManager 开始
上回我们提到,llvm11 通过一个配置项,可以开启新版的 PassManager,此刻,我并不知道新版的怎么用,我想通过阅读代码来找到灵感。
进入分支
1 2 3 4 | if (CGOpts.ExperimentalNewPassManager)
AsmHelper.EmitAssemblyWithNewPassManager(Action, std::move(OS));
else
AsmHelper.EmitAssembly(Action, std::move(OS));
|
添加 -fexperimental-new-pass-manager
来进入另一个分支,断点调试。
代码非常清晰,EmitAssemblyWithNewPassManager
中:
1 2 3 4 5 6 7 8 9 10 | / / Attempt to load pass plugins and register their callbacks with PB.
for (auto &PluginFN : CodeGenOpts.PassPlugins) {
auto PassPlugin = PassPlugin::Load(PluginFN);
if (PassPlugin) {
PassPlugin - >registerPassBuilderCallbacks(PB);
} else {
Diags.Report(diag::err_fe_unable_to_load_plugin)
<< PluginFN << toString(PassPlugin.takeError());
}
}
|
调试时发现 PassPlugins
默认是空的,由
1 | Opts.PassPlugins = Args.getAllArgValues(OPT_fpass_plugin_EQ);
|
进行赋值,因此需要传参:fpass-plugin=libPass.so
这里有两个函数,一个是加载plugin,一个是触发 Callback,并传递关键对象 PassBuilder。
PassPlugin::Load
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | Expected<PassPlugin> PassPlugin::Load(const std::string &Filename) {
std::string Error;
auto Library =
sys::DynamicLibrary::getPermanentLibrary(Filename.c_str(), &Error);
if (!Library.isValid())
return make_error<StringError>(Twine( "Could not load library '" ) +
Filename + "': " + Error,
inconvertibleErrorCode());
PassPlugin P{Filename, Library};
intptr_t getDetailsFn =
(intptr_t)Library.SearchForAddressOfSymbol( "llvmGetPassPluginInfo" );
if (!getDetailsFn)
/ / If the symbol isn't found, this is probably a legacy plugin, which is an
/ / error
return make_error<StringError>(Twine( "Plugin entry point not found in '" ) +
Filename + "'. Is this a legacy plugin?" ,
inconvertibleErrorCode());
P.Info = reinterpret_cast<decltype(llvmGetPassPluginInfo) * >(getDetailsFn)();
if (P.Info.APIVersion ! = LLVM_PLUGIN_API_VERSION)
return make_error<StringError>(
Twine( "Wrong API version on plugin '" ) + Filename + "'. Got version " +
Twine(P.Info.APIVersion) + ", supported version is " +
Twine(LLVM_PLUGIN_API_VERSION) + "." ,
inconvertibleErrorCode());
if (!P.Info.RegisterPassBuilderCallbacks)
return make_error<StringError>(Twine( "Empty entry callback in plugin '" ) +
Filename + "'.'" ,
inconvertibleErrorCode());
return P;
}
|
代码逻辑简单而且很好理解,就是获取 Pass 信息并做简单检查:
- 首先加载library
- 然后寻找
llvmGetPassPluginInfo
的符号
- 然后调用
llvmGetPassPluginInfo
并且获得返回,返回类型应为 PassPluginLibraryInfo
,存放到 Info
字段中。
- 检查
APIVersion
和 LLVM_PLUGIN_API_VERSION
- 检查
RegisterPassBuilderCallbacks
不为空
registerPassBuilderCallbacks
1 2 3 4 | / / / Invoke the PassBuilder callback registration
void registerPassBuilderCallbacks(PassBuilder &PB) const {
Info.RegisterPassBuilderCallbacks(PB);
}
|
这个 Info
就是该插件的信息,由外部加载的Library提供,调用其回调函数,函数传参为 PassBuilder &PB
的索引。
显然,这个 PassBuilder 就是新版的,对应之前的 legacy::PassManagerBase。没想到逻辑这么短,一下子就理清楚了。
简单适配
搜索关键词,llvmGetPassPluginInfo,在官方example里找到一个非常适合学习的,llvm/examples/Bye/Bye.cpp
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | / * Legacy PM Registration * /
static llvm::RegisterStandardPasses RegisterBye(
llvm::PassManagerBuilder::EP_VectorizerStart,
[](const llvm::PassManagerBuilder &Builder,
llvm::legacy::PassManagerBase &PM) { PM.add(new LegacyBye()); });
/ * New PM Registration * /
llvm::PassPluginLibraryInfo getByePluginInfo() {
return {LLVM_PLUGIN_API_VERSION, "Bye" , LLVM_VERSION_STRING,
[](PassBuilder &PB) {
PB.registerVectorizerStartEPCallback(
[](llvm::FunctionPassManager &PM,
llvm::PassBuilder::OptimizationLevel Level) {
PM.addPass(Bye());
});
PB.registerPipelineParsingCallback(
[](StringRef Name, llvm::FunctionPassManager &PM,
ArrayRef<llvm::PassBuilder::PipelineElement>) {
if (Name = = "goodbye" ) {
PM.addPass(Bye());
return true;
}
return false;
});
}};
}
extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo
llvmGetPassPluginInfo() {
return getByePluginInfo();
}
|
它同时适配了 LegacyPassManager 和 NewPassManager,和阅读代码的结论相互印证。
网上找到两个有用的链接,分别是:https://github.com/banach-space/llvm-tutor 和 https://groups.google.com/g/llvm-dev/c/e_4WobR9WP0 。前者有一些 llvmGetPassPluginInfo
的实践,可惜它用的是 opt
,我用 clang 时无法触发;后者和我日常测试的需求相似,也给了我较大的帮助。
简而言之,要想适配 NewPassManager,就需要深刻理解 PassBuilder &PB
这个对象的用法,它的 API 实在太多太复杂,编写本文时还没有研究透彻。
临时demo
暂时抄groups的讨论:动态注册使用这段,在llvm13上可以成功,但是在 llvm 11 上不成功,原因未知,将来有空研究一下。
1 2 3 4 5 6 | PB.registerPipelineStartEPCallback(
[](ModulePassManager &MPM, OptimizationLevel Level) {
FunctionPassManager FPM;
FPM.addPass(HelloWorld());
MPM.addPass(createModuleToFunctionPassAdaptor(std::move(FPM)));
});
|
1 2 3 4 5 | $LLVM_HOME / bin / clang - - version
clang version 13.0 . 0 (https: / / github.com / llvm / llvm - project / 24c8eaec9467b2aaf70b0db33a4e4dd415139a50 )
$ $LLVM_HOME / bin / clang - fpass - plugin = / tmp / llvm - pass - tutorial / b / skeleton / libSkeletonPass.so / tmp / test.c
registerPipelineStartEPCallback
I saw a function called main!
|
未完待续。。。
NewPassManager 还有很多值得探索的地方,我会尽快找到一个 NewPassManager 完美的适配方案。
[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界
最后于 2022-6-15 19:12
被LeadroyaL编辑
,原因: