首页
社区
课程
招聘
[原创] 从LLVM的Pass插件加载流程分析NewPassManager无法解析opt模板类参数现象
发表于: 2024-6-20 16:03 2132

[原创] 从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文件,差不多这样:

    1
    2
    3
    set(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参数的内容如下:

    1
    2
    OPTION(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,有两个关键发现。

    1. 我们在DriverOptions中找到了OPTION的宏定义,可以观察到第三个参数ID会被拼接有OPT_字符。
      1
      2
      3
      4
      5
      6
      7
      8
      static 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
      };
    2. 我们在CompilerInvocation源码文件中找到了对该文件的大量引用,并且很轻易的就发现了可疑代码:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      if ((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中找到如下内容:
      1
      2
      OPTION(prefix_1, llvm::StringLiteral("fsycl-is-device"), fsycl_is_device, Flag, INVALID, INVALID, nullptr, CC1Option | NoDriverOption, 0,
          "Generate code for SYCL device.", nullptr, nullptr)
      fsycl_is_device是ID,OPT_fsycl_is_device是ID被拼接后的结果,在源码中以OPT_fsycl_is_device的形式被调用,与上文追溯的宏定义结构吻合,相互印证,一切合理起来了。
      综上所述,结合宏定义的ID字符串拼接以及CompilerInvocation源码中的调用示范,对于-load参数,他在源码中被调用的形式应当是OPT_load
    3. 接下来全局搜索OPT_load,发现一处调用OPT_load的地方:ParseFrontendArgs:
      1
      Opts.Plugins = Args.getAllArgValues(OPT_load);
      发现-load参数加载的插件,全部都被存储到了Opts.Plugins中,而这个结果,也是较为符合LeadroyaL前辈在博文中的结论的,即:

      Clang->getFrontendOpts().Plugins中存放动态加载library。
      得到阶段性结论:虽然我们不知道 -load 的实现在哪里,但它的作用是将传参结果放到了 Plugins 里,加载 library 已结束,但 pass 并没有加载或执行。

      从这一线索追溯,我们查到了LoadRequestedPlugins函数
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      void 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来处理加载流程。
    4. 至此,-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加载动态库的完整逻辑链形成闭环。
  • -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是依赖于本条参数
    1
    2
    3
    4
    5
    6
    #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加载插件并解析模版参数逻辑
    加载插件应当是下面这段代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    static 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());
    });

    上述代码中,首先定义了一个全局模版参数,用于接受加载插件命令的解析。
    而在这段话之后,就紧随着:

    1
    2
    cl::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直播授课

最后于 2024-6-20 19:14 被Ssage泓清编辑 ,原因:
收藏
免费 0
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//