一、gdb动调clang
文章开始之前,先通过gd动调clang来看一看整个过程是什么样的。
开始调试
我这里调试的是release版本的clang(不想再编译了),显然提示了找不到clang的调试符号。我建议使用debug版的clang来调试,不然就看不到断点处的源码了,这对调试还是有影响的(有些函数下不了断点,不知道是不是这个原因)。
直接把断点下在main函数处
重要的来了!clang在执行过程中,会fork子进程,如果直接进行跟踪会出现如下结果:
因此我们需要通过如下指令在gdb中设置跟踪子进程:
1
| set follow-fork-mode child
|
然后启动clang并运行我们的Pass,指令如下:
1
| run -mllvm -mypass ~/Desktop/test.cpp -o ~/Desktop/test_debug_clang.ll
|
此时我们还需要对自己的Pass下断点:
继续运行直到命中我们下的Pass断点指令如下:
成功断在了预期位置处,此时我们查看函数调用堆栈,指令如下:
从上面的函数调用堆栈可以明了的看出Pass从被加载到被执行的整个过程(实际上,它还是缺少了一些函数)。
接下来我们结合源码来仔细分析一下这个流程。
二、从clang到Pass加载与执行的流程
2.1 clang: 从源码到IR
2.1.1 main
clang
的入口位于clang/tools/driver/driver.cpp
中的main
函数。
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 37 38
| int main(int Argc, const char **Argv) { ... auto FirstArg = llvm::find_if(llvm::drop_begin(Args), [](const char *A) { return A != nullptr; }); if (FirstArg != Args.end() && StringRef(*FirstArg).startswith("-cc1")) { if (MarkEOLs) { auto newEnd = std::remove(Args.begin(), Args.end(), nullptr); Args.resize(newEnd - Args.begin()); } return ExecuteCC1Tool(Args); } ... Driver TheDriver(Path, llvm::sys::getDefaultTargetTriple(), Diags); ... if (!UseNewCC1Process) { TheDriver.CC1Main = &ExecuteCC1Tool; llvm::CrashRecoveryContext::Enable(); } std::unique_ptr<Compilation> C(TheDriver.BuildCompilation(Args)); int Res = 1; bool IsCrash = false; if (C && !C->containsError()) { SmallVector<std::pair<int, const Command *>, 4> FailingCommands; Res = TheDriver.ExecuteCompilation(*C, FailingCommands); ... ... } ... }
|
其中第25行的BuildCompilation
函数以及第31行的ExecuteCompilation
函数,它们的进一步跟进请参考谁说不能与龙一起跳舞:Clang / LLVM (3) - 知乎 (zhihu.com)。简单来说,一开始的命令行参数并不会满足代码中第6行的要求(即没有-cc1
),从而一开始不会执行ExecuteCC1Tool
函数,但通过一系列操作,最终还是执行了ExecuteCC1Tool
函数。
ExecuteCC1Tool
的具体实现在clang/tools/driver/driver.cpp
中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| static int ExecuteCC1Tool(SmallVectorImpl<const char *> &ArgV) { ... StringRef Tool = ArgV[1]; void *GetExecutablePathVP = (void *)(intptr_t)GetExecutablePath; if (Tool == "-cc1") return cc1_main(makeArrayRef(ArgV).slice(1), ArgV[0], GetExecutablePathVP); if (Tool == "-cc1as") return cc1as_main(makeArrayRef(ArgV).slice(2), ArgV[0], GetExecutablePathVP); if (Tool == "-cc1gen-reproducer") return cc1gen_reproducer_main(makeArrayRef(ArgV).slice(2), ArgV[0], GetExecutablePathVP); ... }
|
在上一小节中,我们知道了第一个参数是-cc1
,因此这里会调用cc1_main
函数。
2.1.3 cc1_main
cc1_main
的具体实现在clang/tools/driver/cc1_main.cpp
中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| int cc1_main(ArrayRef<const char *> Argv, const char *Argv0, void *MainAddr) { ... std::unique_ptr<CompilerInstance> Clang(new CompilerInstance()); IntrusiveRefCntPtr<DiagnosticIDs> DiagID(new DiagnosticIDs()); ... ... { llvm::TimeTraceScope TimeScope("ExecuteCompiler"); Success = ExecuteCompilerInvocation(Clang.get()); } ... }
|
这里主要是创建clang
实例,调用ExecuteCompilerInvocation
函数开始编译目标源代码。
2.1.4 clang::ExecuteCompilerInvocation
ExecuteCompilerInvocation
函数的声明在clang/include/clang/FrontendTool/ExecuteCompilerInvocation.h
中,具体实现在clang/lib/FrontendTool/ExecuteCompilerInvocation.cpp
中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| bool ExecuteCompilerInvocation(CompilerInstance *Clang) { ... Clang->LoadRequestedPlugins();
... std::unique_ptr<FrontendAction> Act(CreateFrontendAction(*Clang)); if (!Act) return false; bool Success = Clang->ExecuteAction(*Act); ... return Success; }
|
这里主要是创建FrontendAction
对象并执行ExecuteAction
函数。
2.1.5 clang::CompilerInstance::ExecuteAction
CompilerInstance
类的声明在clang/include/clang/Frontend/CompilerInstance.h
中,具体实现在clang/lib/Frontend/CompilerInstance.cpp
中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| bool CompilerInstance::ExecuteAction(FrontendAction &Act) { ... for (const FrontendInputFile &FIF : getFrontendOpts().Inputs) { if (hasSourceManager() && !Act.isModelParsingAction()) getSourceManager().clearIDTables(); if (Act.BeginSourceFile(*this, FIF)) { if (llvm::Error Err = Act.Execute()) { consumeError(std::move(Err)); } Act.EndSourceFile(); } } ... ... return !getDiagnostics().getClient()->getNumErrors(); }
|
这里通过BeginSourceFile
函数加载源文件到内存中了,然后调用了FrontendAction
类的Execute
函数进行编译。
2.1.6 clang::FrontendAction::Execute
FrontendAction
类的声明在clang/include/clang/Frontend/FrontendAction.h
中,具体实现在clang/lib/Frontend/FrontendAction.cpp
中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| llvm::Error FrontendAction::Execute() { CompilerInstance &CI = getCompilerInstance();
if (CI.hasFrontendTimer()) { ... ExecuteAction(); } else ExecuteAction();
... return llvm::Error::success(); }
|
该函数进一步调用ASTFrontendAction
类的ExecuteAction
函数。
2.1.7 clang::ASTFrontendAction::ExecuteAction
这个函数在开头的函数调用堆栈图中并没有出现,然而实际上确实调用了(真不明白这个是什么原因),如下图所示:
那么就来看一下这个函数的源码。ASTFrontendAction
类的声明在clang/include/clang/Frontend/FrontendAction.h
中,具体实现在clang/lib/Frontend/FrontendAction.cpp
中。
1 2 3 4 5 6 7 8
| void ASTFrontendAction::ExecuteAction() { ... if (!CI.hasSema()) CI.createSema(getTranslationUnitKind(), CompletionConsumer); ParseAST(CI.getSema(), CI.getFrontendOpts().ShowStats, CI.getFrontendOpts().SkipFunctionBodies); }
|
主要是创建语义分析器,调用 ParseAST
方法,开始解析抽象语法树(AST)。
2.1.8 clang::ParseAST
ParseAST
函数的声明在clang/include/clang/Parse/ParseAST.h
中,具体实现在clang/lib/Parse/ParseAST.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 34 35 36 37 38 39
| void clang::ParseAST(Sema &S, bool PrintStats, bool SkipFunctionBodies) { ... ASTConsumer *Consumer = &S.getASTConsumer(); std::unique_ptr<Parser> ParseOP(new Parser(S.getPreprocessor(), S, SkipFunctionBodies)); Parser &P = *ParseOP.get(); ... S.getPreprocessor().EnterMainSourceFile(); ExternalASTSource *External = S.getASTContext().getExternalSource(); if (External) External->StartTranslationUnit(Consumer); bool HaveLexer = S.getPreprocessor().getCurrentLexer(); if (HaveLexer) { llvm::TimeTraceScope TimeScope("Frontend"); P.Initialize(); Parser::DeclGroupPtrTy ADecl; Sema::ModuleImportState ImportState; EnterExpressionEvaluationContext PotentiallyEvaluated(S, Sema::ExpressionEvaluationContext::PotentiallyEvaluated); for (bool AtEOF = P.ParseFirstTopLevelDecl(ADecl, ImportState); !AtEOF; AtEOF = P.ParseTopLevelDecl(ADecl, ImportState)) { if (ADecl && !Consumer->HandleTopLevelDecl(ADecl.get())) return; } }
for (Decl *D : S.WeakTopLevelDecls()) Consumer->HandleTopLevelDecl(DeclGroupRef(D)); Consumer->HandleTranslationUnit(S.getASTContext());
... }
|
这部分真正是对源码进行语法树构建,并通过HandleTranslationUnit
函数交给AST Consumer
处理。
2.1.9 clang::BackendConsumer::HandleTranslationUnit
BackendConsumer
类的声明在clang/include/clang/CodeGen/CodeGenAction.h
中,具体实现在clang/lib/CodeGen/CodeGenAction.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
| void BackendConsumer::HandleTranslationUnit(ASTContext &C) { { ... Gen->HandleTranslationUnit(C); ... } ... ... if (LinkInModules(getModule())) return; for (auto &F : getModule()->functions()) { if (const Decl *FD = Gen->GetDeclForMangledName(F.getName())) { auto Loc = FD->getASTContext().getFullLoc(FD->getLocation()); auto NameHash = llvm::hash_value(F.getName()); ManglingFullSourceLocs.push_back(std::make_pair(NameHash, Loc)); } } ... EmbedBitcode(getModule(), CodeGenOpts, llvm::MemoryBufferRef()); EmitBackendOutput(Diags, HeaderSearchOpts, CodeGenOpts, TargetOpts, LangOpts, C.getTargetInfo().getDataLayoutString(), getModule(), Action, FS, std::move(AsmOutStream), this); ... }
|
该函数主要是记录了模块中函数的函数名哈希值和声明位置信息,最后调用EmitBackendOutput
函数生成中间代码。
什么是模块?
在LLVM中,”模块(Module)”通常是指一个编译单元或一个源代码文件被编译后生成的中间表示(IR,Intermediate Representation)的集合。在 LLVM 中,每个模块都是一个独立的单元,包含了函数、全局变量、类型定义等信息,可以被独立地优化和编译。
2.1.10 clang::EmitBackendOutput
EmitBackendOutput
函数声明在clang/include/clang/CodeGen/BackendUtil.h
,具体实现在clang/lib/CodeGen/BackendUtil.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
| void clang::EmitBackendOutput(DiagnosticsEngine &Diags, const HeaderSearchOptions &HeaderOpts, const CodeGenOptions &CGOpts, const clang::TargetOptions &TOpts, const LangOptions &LOpts, StringRef TDesc, Module *M, BackendAction Action, std::unique_ptr<raw_pwrite_stream> OS) { ... ... EmitAssemblyHelper AsmHelper(Diags, HeaderOpts, CGOpts, TOpts, LOpts, M); if (CGOpts.LegacyPassManager) AsmHelper.EmitAssemblyWithLegacyPassManager(Action, std::move(OS)); else AsmHelper.EmitAssembly(Action, std::move(OS)); if (AsmHelper.TM) { std::string DLDesc = M->getDataLayout().getStringRepresentation(); ... } }
|
最终通过BackendConsumer
将AST转换成了IR代码,之后CGOpts.LegacyPassManager
标志选择执行新版本的EmitAssemblyWithLegacyPassManager
或是旧版本的EmitAssembly
。
Clang 的后端消费者(BackendConsumer)是 Clang 的一部分,它负责将 Clang 前端产生的抽象语法树(AST)转换为 LLVM 的中间表示(IR)
2.2 Pass加载
很奇怪,这一部分的EmitAssemblyHelper
类的函数无法触发断点,且提示没有加载进来该函数。
2.2.1 EmitAssemblyHelper::EmitAssemblyWithLegacyPassManager
该函数同样也在clang/lib/CodeGen/BackendUtil.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
| void EmitAssemblyHelper::EmitAssemblyWithLegacyPassManager( ... DebugifyCustomPassManager PerModulePasses; ... PerModulePasses.add(createTargetTransformInfoWrapperPass(getTargetIRAnalysis())); legacy::FunctionPassManager PerFunctionPasses(TheModule); PerFunctionPasses.add(createTargetTransformInfoWrapperPass(getTargetIRAnalysis())); CreatePasses(PerModulePasses, PerFunctionPasses); ... legacy::PassManager CodeGenPasses; CodeGenPasses.add(createTargetTransformInfoWrapperPass(getTargetIRAnalysis())); ... {... PerFunctionPasses.doInitialization(); for (Function &F : *TheModule) if (!F.isDeclaration()) PerFunctionPasses.run(F); PerFunctionPasses.doFinalization(); } {... PerModulePasses.run(*TheModule); } {... CodeGenPasses.run(*TheModule); } ... }
|
这部分代码主要是创建出两个重要的Pass
管理器:PerModulePasses
、PerFunctionPasses
。然后调用CreatePasses
函数创建Pass
并添加到对应的Pass
管理器的执行队列中(详见2.2.2小节)。之后就是调用PerFunctionPasses.run(F)
、PerModulePasses.run(*TheModule)
、CodeGenPasses.run(*TheModule)
来执行Pass
(详见2.3小节,以PerModulePasses.run
函数为例进行讲解)。
2.2.2 EmitAssemblyHelper::CreatePasses
CreatePasses
函数同样也在clang/lib/CodeGen/BackendUtil.cpp
中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| void EmitAssemblyHelper::CreatePasses(legacy::PassManager &MPM, legacy::FunctionPassManager &FPM) { ... PassManagerBuilderWrapper PMBuilder(TargetTriple, CodeGenOpts, LangOpts); ... ... ... ... ... PMBuilder.populateFunctionPassManager(FPM); PMBuilder.populateModulePassManager(MPM); }
|
最后调用的populateModulePassManager
、populateFunctionPassManager
函数将Pass
添加到对应管理器的执行队列中(详见2.2.3小节,以populateModulePassManager
函数为例进行讲解)。
2.2.3 PassManagerBuilder::populateModulePassManager
populateModulePassManager
函数在llvm/lib/Transforms/IPO/PassManagerBuilder.cpp
中。这个函数想必大家都很熟悉,因为在之前的OLLVM
移植、编写自己的Pass
都需要在这里面进行添加。
1 2 3 4 5
| void PassManagerBuilder::populateModulePassManager(legacy::PassManagerBase &MPM) { MPM.add(createAnnotation2MetadataLegacyPass()); ... }
|
到这里,可以认为我们的Pass
已经创建好了,不再进行进一步深究。
2.3 Pass执行
2.3.1 PassManager::run
PerModulePasses.run
在llvm/lib/IR/LegacyPassManager.cpp
中。
1 2 3
| bool PassManager::run(Module &M) { return PM->run(M); }
|
最终调用PassManagerImpl::run
函数
2.3.2 llvm::legacy::PassManagerImpl
PassManagerImpl
类的定义在llvm/include/llvm/IR/LegacyPassManager.h
中,具体实现在llvm/lib/IR/LegacyPassManager.cpp
中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| bool PassManagerImpl::run(Module &M) { ... for (ImmutablePass *ImPass : getImmutablePasses()) Changed |= ImPass->doInitialization(M); initializeAllAnalysisInfo(); for (unsigned Index = 0; Index < getNumContainedManagers(); ++Index) { Changed |= getContainedManager(Index)->runOnModule(M); M.getContext().yield(); } for (ImmutablePass *ImPass : getImmutablePasses()) Changed |= ImPass->doFinalization(M);
return Changed; }
|
该函数主要是对模块执行所有被安排执行的Pass
,对应代码中第15~19行,关键函数为runOnModule
。
什么是不可变Pass?
在 LLVM 中,Pass(通常称为优化 Pass 或者分析 Pass)是指一种对 LLVM IR 进行转换或者分析的模块。Passes 可以用于执行各种任务,例如优化代码、收集统计信息、生成调试信息等。Passes 通常根据其行为被分为两类:可变 Pass 和不可变 Pass。
- 可变 Pass(Mutable Pass):可变 Pass 是指在执行过程中可以修改 LLVM IR 的 Pass。这意味着它们可以插入、删除或修改指令、函数、基本块等内容。可变 Pass 通常用于优化编译过程,例如执行指令调度、函数内联等。
- 不可变 Pass(Immutable Pass):不可变 Pass 是指在执行过程中不会修改 LLVM IR 的 Pass。它们只会读取 IR 并执行一些分析或者只读的转换。不可变 Pass 通常用于收集信息、生成报告、进行静态分析等任务。
2.3.3 FPPassManager::runOnModule
FPPassManager
类的定义在llvm/include/llvm/IR/LegacyPassManagers.h
中,具体实现在llvm/lib/IR/LegacyPassManager.cpp
中。
1 2 3 4 5 6 7 8
| bool FPPassManager::runOnModule(Module &M) { bool Changed = false; for (Function &F : M) Changed |= runOnFunction(F); return Changed; }
|
调用runOnFunction
函数对模块中的函数进行Pass操作。
2.3.4 FPPassManager::runOnFunction
runOnFunction
函数具体实现在llvm/lib/IR/LegacyPassManager.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 34 35
| bool FPPassManager::runOnFunction(Function &F) { if (F.isDeclaration()) return false; ... Module &M = *F.getParent(); populateInheritedAnalysis(TPM->activeStack);
...
const StringRef Name = F.getName(); llvm::TimeTraceScope FunctionScope("OptFunction", Name); for (unsigned Index = 0; Index < getNumContainedPasses(); ++Index) { FunctionPass *FP = getContainedPass(Index); bool LocalChanged = false; ... initializeAnalysisImpl(FP); { PassManagerPrettyStackEntry X(FP, F); TimeRegion PassTimer(getPassTimer(FP)); #ifdef EXPENSIVE_CHECKS uint64_t RefHash = FP->structuralHash(F); #endif LocalChanged |= FP->runOnFunction(F); ... } ... } return Changed; }
|
最后通过runOnFunction
执行对应的Pass
(代码中第28行),在当前例子中,也就是执行MyPass
的runOnFunction
函数。
三、结语
以上就是。简而言之,首先clang
会先将我们的目标源码转成AST语法树,然后再通过ASTConsumer
换成IR
代码。之后加载通过CreatePasses
函数创建Pass并加入到执行队列中,后续会调用我们熟知的populateModulePassManager
,这里注册过我们自定义的Pass
。最后就是Pass
执行,主要还是通过对应的Pass
管理器的runOnModule
函数来进行的,它这里面会直接调用我们自定义Pass
的runOnModule
函数。值得一提的是,Pass
的加载与执行的操作都是在EmitAssemblyWithLegacyPassManager
或EmitAssembly
函数中调用和完成的。
参考:
【Linux】GDB保姆级调试指南(什么是GDB?GDB如何使用?)_linux gdb标准输入-CSDN博客
对LLVM Pass进行Debug_vscode llvm pass开发-CSDN博客
谁说不能与龙一起跳舞:Clang / LLVM (3) - 知乎 (zhihu.com)
llvm学习(二十):动态注册Pass的加载过程(上) | LeadroyaL’s website
(3 封私信 / 1 条消息) Clang里面真正的前端是什么? - 知乎 (zhihu.com)
https://juejin.cn/post/6844903591115767821