首页
社区
课程
招聘
[原创] llvm PassManager的变更及动态注册Pass的加载过程
2022-5-14 23:43 19917

[原创] llvm PassManager的变更及动态注册Pass的加载过程

2022-5-14 23:43
19917

本文是个人发表在个人博客的两篇文章的合集,介绍 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 -cc1clang -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 的总结

  1. clang 命令行传参,传给 clang -cc1
  2. clang -cc1 解析 -load libSkeletonPass.so ,注册时机和回调函数被存储到 GlobalExtensions
  3. 初步编译后,触发优化部分
  4. EmitAssembly -> CreatePass -> populateFunctionPassManager 调用链,最终调用到 addExtensionsToPM,访问 GlobalExtensions,如果当前时机和注册时机相同,就调用回调函数
  5. 回调函数的入参为 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 信息并做简单检查:

  1. 首先加载library
  2. 然后寻找 llvmGetPassPluginInfo 的符号
  3. 然后调用 llvmGetPassPluginInfo 并且获得返回,返回类型应为 PassPluginLibraryInfo,存放到 Info字段中。
  4. 检查 APIVersionLLVM_PLUGIN_API_VERSION
  5. 检查 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;
                });
          }};
}
 
#ifndef LLVM_BYE_LINK_INTO_TOOLS
extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo
llvmGetPassPluginInfo() {
  return getByePluginInfo();
}
#endif

它同时适配了 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编辑 ,原因:
收藏
点赞7
打赏
分享
最新回复 (2)
雪    币: 1
活跃值: (410)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
carlcurist 2022-6-12 11:40
2
0
请问作者是如何调试clang的呢?我都是通过打印log来调试代码,从来还没试过用调试器调试
雪    币: 4752
活跃值: (2923)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
LeadroyaL 1 2022-6-13 15:26
3
0
carlcurist 请问作者是如何调试clang的呢?我都是通过打印log来调试代码,从来还没试过用调试器调试[em_85]
你好,在我的另一篇博客里有提到:https://www.leadroyal.cn/p/2206/
游客
登录 | 注册 方可回帖
返回