-
-
[原创] 从LLVM的Pass插件加载流程分析NewPassManager无法解析opt模板类参数现象
-
发表于: 2024-6-20 16:03 2132
-
本文以Clang/LLVM Commit d9f89f4d16663d5012e5c09495f3b30ece3d2362 为样品分析
正好对应NDK版本号:26.2.11394342
在早期研究PassPlugin的时候(大概是Clang/LLVM版本14的时候)。
如果是LegacyPass,那么对于如cl::opt
这种模版类参数,无论是clang使用-mllvm -fla
还是opt使用-fla
,都可以无误识别,并且传递模版类参数。
但是对于NewPass,在14版本的时候,无论是clang还是opt,无论是尝试何种方案,都是无法传递cl::opt
模版类参数的。
这种情况在更高版本(15版本及以后)有所好转,虽然clang依然无法在NewPass插件中识别cl::opt
模版类参数,至少opt是可以了。
本文着重探究LegacyPass和NewPass之间,插件加载的机制,以探明为什么cl::opt
模版类参数会无法传递的问题。
For LegacyPass
我们已知LegacyPass情况下,clang和opt都可以加载
cl::opt
模版类参数
那么接下来我们来探究一下LegacyPass插件的加载机制,以及cl::opt
模版类参数被解析识别的流程
clang
对于clang
来说,加载LegacyPass插件的命令如下:
1 2 3 4 | clang++ \ -Xclang -load -Xclang /path/to/libPass .so \ -mllvm -fla \ main.cpp -o main.out |
我们需要搞清楚的是,-load
参数是如何加载插件的,以及-mllvm -fla
是如何被识别的。
我在LeadroyaL前辈的开拓之下,继续进行研究。
clang可执行文件分析
就好像LeadroyaL前辈分析的一样,在我们正常调用clang的时候,实际上真正的编译器是clang -cc1
,它才是真正的编译器。
clang的可执行文件源码位于clang/tools/driver
目录下。
在这条commit之前,流程都是直接main函数开始分析,在这条commit之后,main函数改为clang_main,其他没什么变化。
从此处开始,对参数做了初步处理,如果第一个参数是-cc1
则进入ExecuteCC1Tool的逻辑,而ExecuteCC1Tool的逻辑,则在我们正常调用clang的时候,陷入cc1_main函数中去。
至于cc1_main中更具体的相关分析,我们下面再谈,因为这需要与后文的一些内容相互关联。-load
加载插件流程
在LeadroyaL前辈的研究中,他根据-load
参数的help内容关键词,追溯到了Options.td文件,遂止。
观察同目录下的CMakeLists.txt文件,差不多这样:123set
(LLVM_TARGET_DEFINITIONS Options.td)
tablegen(LLVM Options.inc
-
gen
-
opt
-
parser
-
defs)
add_public_tablegen_target(ClangDriverOptions)
实际上,这是利用了LLVM的TableGen系统,将Options.td文件生成为了Options.inc文件。
Options.inc文件存在于你的build
目录下的tools/clang/include/clang/Driver
目录下
其中关于-load
参数的内容如下:12OPTION(prefix_1, llvm::StringLiteral(
"load"
), load, Separate, INVALID, INVALID, nullptr, CC1Option | FC1Option | NoDriverOption, 0,
"Load the named plugin (dynamic shared object)"
,
"<dsopath>"
, nullptr)
那么接下来的问题就在于,该头文件中的宏定义是如何被源码引用的。
全局搜索Options.inc,有两个关键发现。- 我们在DriverOptions中找到了OPTION的宏定义,可以观察到第三个参数ID会被拼接有
OPT_
字符。12345678static
constexpr OptTable::Info InfoTable[] = {
#define OPTION(PREFIX, NAME, ID, KIND, GROUP, ALIAS, ALIASARGS, FLAGS, PARAM, \
HELPTEXT, METAVAR, VALUES) \
{PREFIX, NAME, HELPTEXT, METAVAR, OPT_##ID, Option::KIND##Class, \
PARAM, FLAGS, OPT_##GROUP, OPT_##ALIAS, ALIASARGS, VALUES},
#include "clang/Driver/Options.inc"
#undef OPTION
};
- 我们在CompilerInvocation源码文件中找到了对该文件的大量引用,并且很轻易的就发现了可疑代码:
这一段代码八成就是在处理传入的参数了,追过去查看123456789if
((Args.hasArg(OPT_fsycl_is_device) || Args.hasArg(OPT_fsycl_is_host)) &&
!Args.hasArg(OPT_sycl_std_EQ)) {
// If the user supplied -fsycl-is-device or -fsycl-is-host, but failed to
// provide -sycl-std=, we want to default it to whatever the default SYCL
// version is. I could not find a way to express this with the options
// tablegen because we still want this value to be SYCL_None when the user
// is not in device or host mode.
Opts.setSYCLVersion(LangOptions::SYCL_Default);
}
OPT_fsycl_is_device
相关内容,在Options.inc中找到如下内容:
fsycl_is_device是ID,OPT_fsycl_is_device是ID被拼接后的结果,在源码中以OPT_fsycl_is_device的形式被调用,与上文追溯的宏定义结构吻合,相互印证,一切合理起来了。12OPTION(prefix_1, llvm::StringLiteral(
"fsycl-is-device"
), fsycl_is_device, Flag, INVALID, INVALID, nullptr, CC1Option | NoDriverOption, 0,
"Generate code for SYCL device."
, nullptr, nullptr)
综上所述,结合宏定义的ID字符串拼接以及CompilerInvocation源码中的调用示范,对于-load
参数,他在源码中被调用的形式应当是OPT_load
。 - 接下来全局搜索
OPT_load
,发现一处调用OPT_load
的地方:ParseFrontendArgs:
发现1Opts.Plugins = Args.getAllArgValues(OPT_load);
-load
参数加载的插件,全部都被存储到了Opts.Plugins
中,而这个结果,也是较为符合LeadroyaL前辈在博文中的结论的,即:Clang->getFrontendOpts().Plugins中存放动态加载library。
得到阶段性结论:虽然我们不知道 -load 的实现在哪里,但它的作用是将传参结果放到了 Plugins 里,加载 library 已结束,但 pass 并没有加载或执行。
可以看到,该函数有调用到1234567891011121314151617181920void
CompilerInstance::LoadRequestedPlugins() {
// Load any requested plugins.
for
(
const
std::string &Path : getFrontendOpts().Plugins) {
std::string Error;
if
(llvm::sys::DynamicLibrary::LoadLibraryPermanently(Path.c_str(), &Error))
getDiagnostics().Report(diag::err_fe_unable_to_load_plugin)
<< Path << Error;
}
// Check if any of the loaded plugins replaces the main AST action
for
(
const
FrontendPluginRegistry::entry &Plugin :
FrontendPluginRegistry::entries()) {
std::unique_ptr<PluginASTAction> P(Plugin.instantiate());
if
(P->getActionType() == PluginASTAction::ReplaceAction) {
getFrontendOpts().ProgramAction = clang::frontend::PluginAction;
getFrontendOpts().ActionName = Plugin.getName().str();
break
;
}
}
}
getFrontendOpts().Plugins
来处理加载进去的插件信息,并且调用LoadLibraryPermanently
来处理加载流程。 - 至此,
-load
参数是如何加载动态库插件的一整套流程已经分析完毕,我们接下来还差一点收尾工作,也就是分析ParseFrontendArgs和LoadRequestedPlugins在cc1_main中被具体触发的流程。- 先来看ParseFrontendArgs,因为常规来说,得是先处理
-load
加载的参数信息,再去具体执行LoadLibraryPermanently流程。
全局搜索ParseFrontendArgs,发现CreateFromArgsImpl函数有调用到ParseFrontendArgs,再去追溯CreateFromArgsImpl函数,发现CreateFromArgs函数有调用到CreateFromArgsImpl,再回头看一眼cc1_main
发现,cc1_main函数较早阶段就已经调用了CreateFromArgs函数,最终形成逻辑闭环。 - 再来看看LoadRequestedPlugins函数,发现ExecuteCompilerInvocation有调用该函数,并且ExecuteCompilerInvocation直接在cc1_main紧接着CreateFromArgs后面一点,就被调用了。
- 最终,
-load
加载动态库的完整逻辑链形成闭环。
- 先来看ParseFrontendArgs,因为常规来说,得是先处理
- 我们在DriverOptions中找到了OPTION的宏定义,可以观察到第三个参数ID会被拼接有
-mllvm -fla
识别流程
这个其实反而非常简单,cl::ParseCommandLineOptions函数就是用来处理这种模版参数的。
如果去cc1_main中苦寻,是找不到该函数的,但是如果刚才细心的话,就会发现ExecuteCompilerInvocation函数中有包含cl::ParseCommandLineOptions,并且人家连注释都给写好了......
由于LegacyPass已经在LLVM15版本被完全移除
我们这里当然也无法再去通过传递特殊的参数去触发LegacyPass了,只是做一个流程分析。
具体的LegacyPass和NewPass触发机制问题,这里不做讨论,后续另开课题。
对于LegacyPass来说
由于模版参数的解析cl::ParseCommandLineOptions函数是在插件加载函数LoadRequestedPlugins后发生的
当cl::ParseCommandLineOptions被调用时,插件已经完全加载,内部的模版参数亦自然被加载
因此,模版参数,类似于-mllvm -fla
完全可以被clang识别。
opt
对于opt
来说,加载LegacyPass插件的命令如下:
1 | opt -load /path/to/libPass .so - split -S main.ll -o main_opt.ll |
opt的可执行文件源码位于llvm/tools/opt
目录下。
- opt的
-load
如何加载插件并且解析模版参数
参考借鉴于这篇pipermail发现:
对于opt而言,-load
去加载LegacyPass是依赖于本条参数
本条模版参数,会在每次传入123456#ifndef DONT_GET_PLUGIN_LOADER_OPTION
// This causes operator= above to be invoked for every -load option.
static
cl::opt<PluginLoader,
false
, cl::parser<std::string>>
LoadOpt(
"load"
, cl::value_desc(
"pluginfilename"
),
cl::desc(
"Load the specified plugin"
));
#endif
-load
参数的时候,自动构造PluginLoader并且加载插件。
并且由于本条模版参数是全局静态的,会优先于opt的main函数加载。
因此,在opt的main中,当cl::ParseCommandLineOptions被调用时。
LegacyPass插件都已经加载完善,插件内部的模版参数自然可以被识别。
对于LegacyPass来说
由于PluginLoader的构造加载依然永远优先于cl::ParseCommandLineOptions函数
因此,模版参数,类似于-mllvm -fla
完全可以被opt识别。
For NewPass
我们已知NewPass情况下,clang不可以加载
cl::opt
模版类参数
而opt在15版本及以后可以解析模版参数,在此之前则和clang一样,不可解析模版参数
下面我们开始进行探究
clang
对于clang
来说,加载NewPass插件的命令如下:
1 2 | clang++ -fpass-plugin= /path/to/libPass .so \ test /hello .cpp -o test /hello .out |
由于clang对New PM无法加载模版参数,所以这里没有相关模板参数
下面我们就来分析一下New PM在clang中的加载流程
首选,全局搜索OPT_fpass_plugin_EQ
,定位到此处。
观察上下文,发现这个不过是一个参数转发,转发了-fpass-plugin
参数内容,下面一点还有转发-mllvm
和-Xclang
的相关内容
仔细追寻调用链,发现也确实能和clang/tools/driver
对应上,但是参数转发并不是插件加载触发机制,似乎到这里就卡住了思路。
再次拜读LeadroyaL的文章发现关于NewPM有如下代码片段
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()); } } |
上下追寻一下调用逻辑,发现此处确实是NewPass插件的加载地点。
上述代码被RunOptimizationPipeline调用,而RunOptimizationPipeline被EmitAssembly调用,EmitAssembly是如何被clang/tools/driver
调用上的,暂时不去追究,单看EmitAssembly内部逻辑,就可以知道为什么clang的NewPass插件内的模版参数无法被识别解析了。
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 | void EmitAssemblyHelper::EmitAssembly(BackendAction Action, std::unique_ptr<raw_pwrite_stream> OS) { TimeRegion Region(CodeGenOpts.TimePasses ? &CodeGenerationTime : nullptr); setCommandLineOpts(CodeGenOpts); bool RequiresCodeGen = actionRequiresCodeGen(Action); CreateTargetMachine(RequiresCodeGen); if (RequiresCodeGen && !TM) return ; if (TM) TheModule->setDataLayout(TM->createDataLayout()); // Before executing passes, print the final values of the LLVM options. cl::PrintOptionValues(); std::unique_ptr<llvm::ToolOutputFile> ThinLinkOS, DwoOS; RunOptimizationPipeline(Action, OS, ThinLinkOS); RunCodegenPipeline(Action, OS, DwoOS); if (ThinLinkOS) ThinLinkOS->keep(); if (DwoOS) DwoOS->keep(); } |
NewPass插件加载是在RunOptimizationPipeline内实现的,而模版参数解析是在setCommandLineOpts中发生的。
setCommandLineOpts中包含llvm::cl::ParseCommandLineOptions用作模版参数的解析。
由于模版参数的解析发生在了NewPass插件加载之前,因此,NewPass插件内部的模版参数,无法被clang识别。
opt
对于opt
来说,加载New Pass插件的命令如下:
1 2 3 | opt --load-pass-plugin= /path/to/libPass .so \ -wave-goodbye -S test /hello .ll -o test /hello .ll \ -passes=goodbye |
opt
在LLVM15版本之后可以实现加载模版参数,上述命令中的-wave-goodbye
便是模版参数
下面我们来分析一下LLVM15版本之后为什么可以正常加载解析模版参数
opt
加载插件并解析模版参数逻辑
加载插件应当是下面这段代码:123456789101112131415static
cl::list<std::string>
PassPlugins(
"load-pass-plugin"
,
cl::desc(
"Load passes from plugin library"
));
// ......
// ......
SmallVector<PassPlugin, 1> PluginList;
PassPlugins.setCallback([&](
const
std::string &PluginPath) {
auto
Plugin = PassPlugin::Load(PluginPath);
if
(!Plugin) {
errs() <<
"Failed to load passes from '"
<< PluginPath
<<
"'. Request ignored.\n"
;
return
;
}
PluginList.emplace_back(Plugin.get());
});
上述代码中,首先定义了一个全局模版参数,用于接受加载插件命令的解析。
而在这段话之后,就紧随着:12cl::ParseCommandLineOptions(argc, argv,
"llvm .bc -> .bc modular optimizer and analysis printer\n"
);
加载插件和解析模版参数的流程一目了然,无需多做解释。
为什么LLVM15版本之前无法解析模版参数
我们追溯一下llvm/tools/opt
的历史提交,发现一条很有意思的提交和这个现象有关系。
那么这个提交是干什么的呢?大概有什么作用?
可以理解为:在这份提交之前,是先有cl::ParseCommandLineOptions解析参数,然后再于NewPMDriver中存在插件加载流程的。
而在这份提交之后,经过一次Revert和Reland,被正式应用下来之后,才是我们目前看到的,先加载插件,后进行解析参数。
这就是为什么从LLVM15之后,opt可以接受插件的模版参数被解析了,因为cl::ParseCommandLineOptions在插件加载之后发生。
Refer
InternalsManual
New PM, opt and command line options
LeadroyaL
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课