誰說不能與龍一起跳舞:Clang / LLVM (3)
而我基於的代碼版本是之前在系列第一篇文章提到過的版本,SVN trunk 277462,是當時寫文章時的Clang / LLVM 最新版。而編譯 Clang / LLVM 的流程,可以參看Clang的官網:Clang - Getting Started。至於代碼閱讀的工具則見仁見智,我目前使用的工具是Jetbrains的CLion.
對於分析類似Clang / LLVM 這樣的大型軟體代碼,我認為首先是有宏觀的理論部分支撐(如我之前的第一篇和第二篇文章),知曉了整體的設計框架與流程,然後再抓住核心點而不糾纏於細節,這樣是比較好的,有效率的做法。同時,需要記住任何一個程序都有一個入口(main函數),而這也將會是我們的分析入口,所以我們首先要看的就是main函數。而Clang的入口則位於tools/clang/tools/driver/driver.cpp中(這裡我使用的是相對路徑,即相對於llvm源代碼根路徑),如下所示:
如之前所說,我們會遇到很多細節的代碼,但是我們在最開始分析的時候不應該留戀於此,而應該抓住我們想要的,那就是Driver是如何把我們讀取到的我們輸入的編譯選項等東西(如何讀取的呢?那就是main函數的argc, argv參數的作用了),進行 Parse / Pipeline / Bind / Translate / Execute 等過程,並且在這過程中是如何從外部Driver層真正的走入到Clang核心前端層(即 cc1 )中,從而執行編譯等過程。所以,我們問題的核心點則在於了兩個,從代碼層面:- Driver是如何組建這一整套自動化流程的(Parse / Pipeline...)
- Driver是如何把這一整套處理完畢後,交給真正的Clang編譯前端 ( 即 cc1 )
正是如此,我們需要關注的兩個關鍵詞:Driver / cc1.
而在main函數中,370行位置,我們發現了有關cc1的東西:
// Handle -cc1 integrated tools, even if -cc1 was expanded from a responsen // file.n auto FirstArg = std::find_if(argv.begin() + 1, argv.end(),n [](const char *A) { return A != nullptr; });n if (FirstArg != argv.end() && StringRef(*FirstArg).startswith("-cc1")) {n // If -cc1 came from a response file, remove the EOL sentinels.n if (MarkEOLs) {n auto newEnd = std::remove(argv.begin(), argv.end(), nullptr);n argv.resize(newEnd - argv.begin());n }n return ExecuteCC1Tool(argv, argv[1] + 4);n }n
我們關注這個if條件,可以發現第一個條件是FirstArg != argv.end(),FirstArg是從我們輸入的參數列表中取出第一個不為空的參數。而我們正常情況下,如clang++ a.cpp,是能滿足要求的。那麼第二個條件是判定取出來的是不是-cc1,如果是,則代表我們進入了cc1模式,執行cc1流程,但是很顯然我們在第一次走到這裡的時候,是不滿足的(你也可以通過main函數直接快速掃描上下文代碼來驗證),所以我們應該知道,我們肯定會在後面的某個時候再過來。
接下來,我們找到Driver相關的關鍵字,那就是448行開始:
Driver TheDriver(Path, llvm::sys::getDefaultTargetTriple(), Diags);n SetInstallDir(argv, TheDriver, CanonicalPrefixes);nn insertTargetAndModeArgs(TargetAndMode.first, TargetAndMode.second, argv,n SavedStrings);nn SetBackdoorDriverOutputsFromEnvVars(TheDriver);nn std::unique_ptr<Compilation> C(TheDriver.BuildCompilation(argv));n int Res = 0;n SmallVector<std::pair<int, const Command *>, 4> FailingCommands;n if (C.get())n Res = TheDriver.ExecuteCompilation(*C, FailingCommands);n
我們在這裡初始化了Driver,並且調用BuildCompilation函數,然後生成一個Compilation,而Compilation是什麼呢?我們進入這個Class
/// Compilation - A set of tasks to perform for a single drivern/// invocation.nclass Compilation {n /// The driver we were created by.n const Driver &TheDriver;nn /// The default tool chain.n const ToolChain &DefaultToolChain;nn /// A mask of all the programming models the host has to support in then /// current compilation.n unsigned ActiveOffloadMask;nn /// Array with the toolchains of offloading host and devices in the order theyn /// were requested by the user. We are preserving that order in case the coden /// generation needs to derive a programming-model-specific semantic out ofn /// it.n std::multimap<Action::OffloadKind, const ToolChain *>n OrderedOffloadingToolchains;nn /// The original (untranslated) input argument list.n llvm::opt::InputArgList *Args;nn /// The driver translated arguments. Note that toolchains may perform theirn /// own argument translation.n llvm::opt::DerivedArgList *TranslatedArgs;nn /// The list of actions weve created via MakeAction. This is not accessiblen /// to consumers; its here just to manage ownership.n std::vector<std::unique_ptr<Action>> AllActions;nn /// The list of actions. This is maintained and modified by consumers, vian /// getActions().n ActionList Actions;nn /// The root list of jobs.n JobList Jobs;nn /// Cache of translated arguments for a particular tool chain and boundn /// architecture.n llvm::DenseMap<std::pair<const ToolChain *, const char *>,n llvm::opt::DerivedArgList *> TCArgs;nn /// Temporary files which should be removed on exit.n llvm::opt::ArgStringList TempFiles;nn /// Result files which should be removed on failure.n ArgStringMap ResultFiles;nn /// Result files which are generated correctly on failure, and which shouldn /// only be removed if we crash.n ArgStringMap FailureResultFiles;nn /// Redirection for stdout, stderr, etc.n const StringRef **Redirects;nn /// Whether were compiling for diagnostic purposes.n bool ForDiagnostics;n
那麼,我們看代碼的話很容易發現我們在系列第二篇文章提到的幾個概念:ArgList / Actions / Jobs... 所以,我們應該能產生一個直覺:那就是Parse / Pipeline...等過程很大可能與這個Class息息相關。
那麼我們首先再次把Driver層的那幅圖片拿出來刷新一下我們有關Parse / Pipeline...等過程的記憶,因為接下來的都與它息息相關。
我們這裡尤其需要記住Input / Output是什麼,如Pipeline配合HostInfo吃的ArgList & Args,吐出的是Actions,然後Bind / Translate是配合了ToolChain / Tools 吃掉Actions,吐出Jobs。Execute則是拿到Jobs,返回Result Code。
OK,讓我們繼續跟蹤代碼下去。既然,Compilation class這麼重要,我們是如何產生這個Compilation class的unique_ptr對象的呢?是通過Driver的BuildCompilation函數。於是,我們則跟進去這個函數看到底發生了一些什麼事情。
若你在Mac使用CLion的話,你可以很容易使用command + B 跳轉到這個函數的定義(其他平台或者IDE也有類似的操作,一般叫做Go to Declaration / Definition),此函數的一些定義如下:
Compilation *Driver::BuildCompilation(ArrayRef<const char *> ArgList) {nn// ......nnInputArgList Args = ParseArgStrings(ArgList.slice(1));nn // Silence driver warnings if requestedn Diags.setIgnoreAllWarnings(Args.hasArg(options::OPT_w));nn // -no-canonical-prefixes is used very early in main.n Args.ClaimAllArgs(options::OPT_no_canonical_prefixes);nn // Ignore -pipe.n Args.ClaimAllArgs(options::OPT_pipe);nn // Extract -ccc args.n //n // FIXME: We need to figure out where this behavior should live. Most of itn // should be outside in the client; the parts that arent should have propern // options, either by introducing new ones or by overloading gcc ones like -Vn // or -b.n CCCPrintPhases = Args.hasArg(options::OPT_ccc_print_phases);n CCCPrintBindings = Args.hasArg(options::OPT_ccc_print_bindings);n if (const Arg *A = Args.getLastArg(options::OPT_ccc_gcc_name))n CCCGenericGCCName = A->getValue();n CCCUsePCH =n Args.hasFlag(options::OPT_ccc_pch_is_pch, options::OPT_ccc_pch_is_pth);n // FIXME: DefaultTargetTriple is used by the target-prefixed calls to as/ldn // and getToolChain is const.n if (IsCLMode()) {nn//......n
從這裡我們能發現一個函數叫做ParseArgStrings,把傳進來的參數解析成InputArgsList,這對應了我們之前在第二篇文章提到的哪個過程的一部分呢?那就是Parse,當然這裡還不是全部,這只是一部分,後面還有一系列的if條件來處理相關的地方,如:
for (const Arg *A : Args.filtered(options::OPT_B)) {n A->claim();n PrefixDirs.push_back(A->getValue(0));n }n if (const Arg *A = Args.getLastArg(options::OPT__sysroot_EQ))n SysRoot = A->getValue();n if (const Arg *A = Args.getLastArg(options::OPT__dyld_prefix_EQ))n DyldPrefix = A->getValue();n if (Args.hasArg(options::OPT_nostdlib))n UseStdLib = false;nn if (const Arg *A = Args.getLastArg(options::OPT_resource_dir))n ResourceDir = A->getValue();nn if (const Arg *A = Args.getLastArg(options::OPT_save_temps_EQ)) {n SaveTemps = llvm::StringSwitch<SaveTempsMode>(A->getValue())n .Case("cwd", SaveTempsCwd)n .Case("obj", SaveTempsObj)n .Default(SaveTempsCwd);n }n
比如我們比較熟悉的options::OPT_nostdlib(在下文我會解釋為什麼代碼里叫這個),即 -nostdlib,不使用標準庫。以及:
// Perform the default argument translations.n DerivedArgList *TranslatedArgs = TranslateInputArgs(*UArgs);n
該函數如注釋所述,主要是翻譯一些參數,感興趣的童鞋可以點進去TranslateInputArgs查看。
根據我們在第二篇文章所述,我們在經過Parse的過程以後,我們將得到相關的ArgList & Args(如上圖所示),然後進入第二個過程Pipeline: 根據具體的編譯選項,構建不同的Compiler Action。而在第二篇文章,為了演示這一過程,我使用了-ccc-print-phases來列印這一過程。那麼,你在這裡也能看到對應的代碼了:
CCCPrintPhases = Args.hasArg(options::OPT_ccc_print_phases);n
所以這也是我強調的理論部分,不然我們很難理解 CCCPrintPhases這個名稱以及其作用。而這裡我們有options::OPT_ccc_print_phases,這一個則是來源於一個名叫Options.td的TableGen文件,在編譯Clang 的時候,然後利用table-gen工具生成一個.inc的C++文件。那麼所有的選項,在代碼層面都為options::OPT_XXXXX。如-ccc_print_phase,則是options:: OPT_ccc_print_phases,在Options.td文件中定義如下:
def ccc_print_phases : Flag<["-"], "ccc-print-phases">, InternalDebugOpt,n HelpText<"Dump list of actions to perform">;n
最後在生成的Options.inc文件中(注意,這個.inc文件不在源代碼中,而在我們的build目錄中):
#ifdef PREFIXn#define COMMA ,nPREFIX(prefix_0, {nullptr})nPREFIX(prefix_1, {"-" COMMA nullptr})n// ...nOPTION(prefix_1, "ccc-print-phases", ccc_print_phases, Flag, internal_debug_Group, INVALID, nullptr, DriverOption | HelpHidden | CoreOption, 0,n "Dump list of actions to perform", nullptr)n
而OPTION的宏在哪裡定義的呢?在Options.h中:
namespace options {n/// Flags specifically for clang options. Must not overlap withn/// llvm::opt::DriverFlag.nenum ClangFlags {n DriverOption = (1 << 4),n LinkerInput = (1 << 5),n NoArgumentUnused = (1 << 6),n Unsupported = (1 << 7),n CoreOption = (1 << 8),n CLOption = (1 << 9),n CC1Option = (1 << 10),n CC1AsOption = (1 << 11),n NoDriverOption = (1 << 12)n};nnenum ID {n OPT_INVALID = 0, // This is not an option ID.n#define OPTION(PREFIX, NAME, ID, KIND, GROUP, ALIAS, ALIASARGS, FLAGS, PARAM, n HELPTEXT, METAVAR) OPT_##ID,n#include "clang/Driver/Options.inc"n LastOptionn#undef OPTIONn };n}nnllvm::opt::OptTable *createDriverOptTable();n}n}n
從這裡也能看出來,為什麼我們會適用options::,因為我們把選項放在了options的namespace中。這裡也能看出來選項為什麼是OPT_開頭,是因為我們定義了OPTION的宏在這裡是OPT_##ID,即把OPT_與ID連接起來。那麼我們這裡的-ccc_print_phases的ID就是ccc_print_phases,所以就是OPT_ ccc_print_phases. 那麼這也給我們進行了一個示範,如我們要增加一個選項的話,我們應該放在哪裡,應該如何處理。我們將在下一篇文章中來進行這件事情,非常的簡單。
讓我們回到Pipeline,Pipeline是配合HostInfo拿到相關的ArgsList & Args,根據不同的編譯選項,進行不同的Compiler Action。
// Owned by the host.n const ToolChain &TC = getToolChain(n *UArgs, computeTargetTriple(*this, DefaultTargetTriple, *UArgs));nn // The compilation takes ownership of Args.n Compilation *C = new Compilation(*this, TC, UArgs.release(), TranslatedArgs);nn if (!HandleImmediateArgs(*C))n return C;nn // Construct the list of inputs.n InputList Inputs;n BuildInputs(C->getDefaultToolChain(), *TranslatedArgs, Inputs);nn // Construct the list of abstract actions to perform for this compilation. Onn // MachO targets this uses the driver-driver and universal actions.n if (TC.getTriple().isOSBinFormatMachO())n BuildUniversalActions(*C, C->getDefaultToolChain(), Inputs);n elsen BuildActions(*C, C->getArgs(), Inputs, C->getActions());nn if (CCCPrintPhases) {n PrintActions(*C);n return C;n }nn BuildJobs(*C);nn return C;n
從這裡,首先我們會拿到不同平台的ToolChain,如Linux / Darwin / Win32等,然後放入到Compilation對象中。然後可以看到我們注重的BuildUniversalActions(Mac OS X)/ BuildActions (Others Platform),即Pipeline的結果,並且這裡表明了我們如果有CCCPrintPhases,就會執行PrintActions(*C),即我們在第二篇文章演示的-ccc-print-phases的效果。而在BuildActions之前還有一個BuildInputs,則對應於我們說過的特殊Action:InputAction。在那裡則會檢測編譯選項(如-x)和文件後綴名等,從而讓編譯器知道目前編譯的是什麼源文件,如是C還是C++,感興趣的童鞋可以跟進去查看。
那麼,我們接下來分析BuildActions。對於Mac的BuildUniversalActions,其本質依然會去調用BuildActions.
// Collect the list of architectures. Duplicates are allowed, but should onlyn // be handled once (in the order seen).n //.......nn // When there is no explicit arch for this platform, make sure we still bindn // the architecture (to the default) so that -Xarch_ is handled correctly.n if (!Archs.size())n Archs.push_back(Args.MakeArgString(TC.getDefaultUniversalArchName()));nn ActionList SingleActions;n BuildActions(C, Args, BAInputs, SingleActions);n
它這裡多做了什麼呢?那就是BindArchAction.
對於BuildActions,我們需要關注的就是我們編譯器到底會執行到哪一步,是預處理,是只編譯,還是包括了鏈接過程等。那麼,我們看這裡的代碼:
void Driver::BuildActions(Compilation &C, DerivedArgList &Args,n const InputList &Inputs, ActionList &Actions) const {n llvm::PrettyStackTraceString CrashInfo("Building compilation actions");nn if (!SuppressMissingInputWarning && Inputs.empty()) {n Diag(clang::diag::err_drv_no_input_files);n return;n }nn Arg *FinalPhaseArg;n phases::ID FinalPhase = getFinalPhase(Args, &FinalPhaseArg);n
這裡有一個很關鍵的函數,叫做getFinalPhase,即我們最後會走到哪一個Phase. 我們跳進去看。
// -{E,EP,P,M,MM} only run the preprocessor.n if (CCCIsCPP() || (PhaseArg = DAL.getLastArg(options::OPT_E)) ||n (PhaseArg = DAL.getLastArg(options::OPT__SLASH_EP)) ||n (PhaseArg = DAL.getLastArg(options::OPT_M, options::OPT_MM)) ||n (PhaseArg = DAL.getLastArg(options::OPT__SLASH_P))) {n FinalPhase = phases::Preprocess;nn // -{fsyntax-only,-analyze,emit-ast} only run up to the compiler.n } else if ((PhaseArg = DAL.getLastArg(options::OPT_fsyntax_only)) ||n (PhaseArg = DAL.getLastArg(options::OPT_module_file_info)) ||n (PhaseArg = DAL.getLastArg(options::OPT_verify_pch)) ||n (PhaseArg = DAL.getLastArg(options::OPT_rewrite_objc)) ||n (PhaseArg = DAL.getLastArg(options::OPT_rewrite_legacy_objc)) ||n (PhaseArg = DAL.getLastArg(options::OPT__migrate)) ||n (PhaseArg = DAL.getLastArg(options::OPT__analyze,n options::OPT__analyze_auto)) ||n (PhaseArg = DAL.getLastArg(options::OPT_emit_ast))) {n FinalPhase = phases::Compile;nn // -S only runs up to the backend.n } else if ((PhaseArg = DAL.getLastArg(options::OPT_S))) {n FinalPhase = phases::Backend;nn // -c compilation only runs up to the assembler.n } else if ((PhaseArg = DAL.getLastArg(options::OPT_c))) {n FinalPhase = phases::Assemble;nn // Otherwise do everything.n } elsen FinalPhase = phases::Link;n
我們這裡可以發現一些什麼事情呢?那就是默認會做所有的事情,包括走向鏈接,若是你使用了-E選項,則只會走到預處理phases::Preprocess,若是-c,則是phases::Assemble等等,這對應了我們之前第二篇文章的這一張圖:
所以,這就是我們這幅圖的代碼來源。
接下來,讓我們走下面的一個函數BuildJobs(*C); 這個顧名思義,是創建一系列的Jobs。由我們之前的圖能想到,我們是在Bind / Tranlate 階段,配合Toolchain / Tools拿到Actions,然後根據不同的Action,做不同的Bind / Translate. 如你的Action是Assembler,然後你需要Assembler,於是這個Assembler應該是哪一個呢?就是這裡來做。而之前在BuildAction我們能看到,什麼時候會走到Assembler呢?至少是-c。
我們要達到的目的是找到為完成這個Action選擇的Bind工具,如Assemble對應這個平台的Assembler。現在讓我們進入BuildJobs函數:
// Set of (Action, canonical ToolChain triple) pairs weve built jobs for.nstd::map<std::pair<const Action *, std::string>, InputInfo> CachedResults;nfor (Action *A : C.getActions()) {n// If we are linking an image for multiple archs then the linker wantsn // -arch_multiple and -final_output <final image name>. Unfortunately, thisn // doesnt fit in cleanly because we have to pass this information down.n //n // FIXME: This is a hack; find a cleaner way to integrate this into then // process.n const char *LinkingOutput = nullptr;n if (isa<LipoJobAction>(A)) {nif (FinalOutput)nLinkingOutput = FinalOutput->getValue();n elsen LinkingOutput = getDefaultImageName();n }nn BuildJobsForAction(C, A, &C.getDefaultToolChain(),n /*BoundArch*/ nullptr,n /*AtTopLevel*/ true,n /*MultipleArchs*/ ArchNames.size() > 1,n /*LinkingOutput*/ LinkingOutput, CachedResults,n /*BuildForOffloadDevice*/ false);n}n
可以看到,我們調用了BuildJobsForAction,跟進去
std::pair<const Action *, std::string> ActionTC = {A, TriplePlusArch};n auto CachedResult = CachedResults.find(ActionTC);n if (CachedResult != CachedResults.end()) {n return CachedResult->second;n }n InputInfo Result = BuildJobsForActionNoCache(n C, A, TC, BoundArch, AtTopLevel, MultipleArchs, LinkingOutput,n CachedResults, BuildForOffloadDevice);n CachedResults[ActionTC] = Result;n
調用了BuildJobsForActionNoCache,跟進去
//...n const ActionList *Inputs = &A->getInputs();nn const JobAction *JA = cast<JobAction>(A);n ActionList CollapsedOffloadActions;nn const Tool *T =n selectToolForJob(C, isSaveTempsEnabled(), embedBitcodeEnabled(), TC, JA,n Inputs, CollapsedOffloadActions);n//...n
這裡有一個selectToolForJob,是不是感覺很像?因為我們要的就是Tool。於是我們跟進去。
在這個函數裡面我們找到了這個函數:
// Otherwise use the tool for the current job.n if (!ToolForJob)n ToolForJob = TC->SelectTool(*JA);n
我們跟進去,實現代碼是:
if (getDriver().ShouldUseClangCompiler(JA)) return getClang();n Action::ActionClass AC = JA.getKind();n if (AC == Action::AssembleJobClass && useIntegratedAs())n return getClangAs();n return getTool(AC);n
這裡有一個是useIntegratedAs,即集成彙編器,這是否會讓我們想起我們在第二篇提到的這段話:
首先,我們可以看到編譯選擇的是clang, 然後鏈接選擇的是darwin::Linker。然後是不是很吃驚彙編過程和彙編器去哪裡了?其實在Mac平台下,Clang使用了內置彙編器,integrated-as。在產生LLVM IR以後,調用了內置彙編器,然後直接生成.o,好處就是減掉了生成彙編文件和調用目標彙編器的開銷。怎麼讓這個彙編出來呢?那就是使用-fno-integrated-as,告訴clang不要使用內置彙編器。
感興趣的同學可以跟進去useIntegratedAs函數查看究竟。這裡我們就抓大頭,查看getTool函數。
Tool *ToolChain::getTool(Action::ActionClass AC) const {n switch (AC) {n case Action::AssembleJobClass:n return getAssemble();nn case Action::LinkJobClass:n return getLink();nn case Action::InputClass:n case Action::BindArchClass:n case Action::OffloadClass:n case Action::LipoJobClass:n case Action::DsymutilJobClass:n case Action::VerifyDebugInfoJobClass:n llvm_unreachable("Invalid tool kind.");nn case Action::CompileJobClass:n case Action::PrecompileJobClass:n case Action::PreprocessJobClass:n case Action::AnalyzeJobClass:n case Action::MigrateJobClass:n case Action::VerifyPCHJobClass:n case Action::BackendJobClass:n return getClang();n }nn llvm_unreachable("Invalid tool kind.");n}n
我們找到這裡,是不是會有會心一笑的感覺?我們繼續抓大頭,跟進去getClang。
if (!Clang)n Clang.reset(new tools::Clang(*this));n return Clang.get();n
public:n // CAUTION! The first constructor argument ("clang") is not arbitrary,n // as it is for other tools. Some operations on a Tool actually testn // whether that tool is Clang based on the Tools Name as a string.n Clang(const ToolChain &TC) : Tool("clang", "clang frontend", TC, RF_Full) {}n bool hasGoodDiagnostics() const override { return true; }n bool hasIntegratedAssembler() const override { return true; }n bool hasIntegratedCPP() const override { return true; }n bool canEmitIR() const override { return true; }nn void ConstructJob(Compilation &C, const JobAction &JA,n const InputInfo &Output, const InputInfoList &Inputs,n const llvm::opt::ArgList &TCArgs,n const char *LinkingOutput) const override;n
走到這一步以後,我們這時候如計算機的stack一樣,我們已經走到頭了,我們需要的是再回溯,看是否會有調用工具的方法,尤其是我們這裡第一直覺就應該是調用ConstructJob,因為我們的理論告訴我們,我們在拿到Action以後,選擇完相關的工具,吐出的是Job。
而這裡,若我們回溯到BuildJobsForActionNoCache函數,我們會發現有這樣的代碼:
if (CCCPrintBindings && !CCGenDiagnostics) {n llvm::errs() << "# "" << T->getToolChain().getTripleString() << "n << " - "" << T->getName() << "", inputs: [";n for (unsigned i = 0, e = InputInfos.size(); i != e; ++i) {n llvm::errs() << InputInfos[i].getAsString();n if (i + 1 != e)n llvm::errs() << ", ";n }n llvm::errs() << "], output: " << Result.getAsString() << "n";n } else {n T->ConstructJob(C, *JA, Result, InputInfos,n C.getArgsForToolChain(TC, BoundArch), LinkingOutput);n }n
這正是我們所需要的。
/// ConstructJob - Construct jobs to perform the action p JA,n /// writing to p Output and with p Inputs, and add the jobs ton /// p C.n ///n /// param TCArgs - The argument list for this toolchain, with anyn /// tool chain specific translations applied.n /// param LinkingOutput - If this output will eventually feed then /// linker, then this is the final output name of the linked image.n virtual void ConstructJob(Compilation &C, const JobAction &JA,n const InputInfo &Output,n const InputInfoList &Inputs,n const llvm::opt::ArgList &TCArgs,n const char *LinkingOutput) const = 0;n
所以之前的Clang的ConstructJob是覆寫了這個純虛函數。那麼我們跟進去Clang覆寫的那個函數,這是一個非常大的函數,我這裡則做什麼呢?提取一個重要的東西:
void Clang::ConstructJob(Compilation &C, const JobAction &JA,n const InputInfo &Output, const InputInfoList &Inputs,n const ArgList &Args, const char *LinkingOutput) const {n n //......nn // Invoke ourselves in -cc1 mode.n //n // FIXME: Implement custom jobs for internal actions.n CmdArgs.push_back("-cc1");nn // Add the "effective" target triple.n CmdArgs.push_back("-triple");n CmdArgs.push_back(Args.MakeArgString(TripleStr));n//......n// Finally add the compile command to the compilation.n if (Args.hasArg(options::OPT__SLASH_fallback) &&n Output.getType() == types::TY_Object &&n (InputType == types::TY_C || InputType == types::TY_CXX)) {n auto CLCommand =n getCLFallback()->GetCommand(C, JA, Output, Inputs, Args, LinkingOutput);n C.addCommand(llvm::make_unique<FallbackCommand>(n JA, *this, Exec, CmdArgs, Inputs, std::move(CLCommand)));n } else if (Args.hasArg(options::OPT__SLASH_fallback) &&n isa<PrecompileJobAction>(JA)) {n // In /fallback builds, run the main compilation even if the pch generationn // fails, so that the main compilations fallback to cl.exe runs.n C.addCommand(llvm::make_unique<ForceSuccessCommand>(JA, *this, Exec,n CmdArgs, Inputs));n } else {n C.addCommand(llvm::make_unique<Command>(JA, *this, Exec, CmdArgs, Inputs));n }n
在這裡發現了什麼呢?那就是我們的-cc1,這就是我們加入-cc1的地方,然後通過addCommand函數加入到C的序列之中進行執行。而這一次由於我們有了-cc1,所以我們會進入到我們所說的ExecuteCC1Tool函數中。那麼,我們就會問了,都已經跑到ExecuteCC1Tool函數檢測條件代碼後面了,怎麼又能再次進入driver的main呢?那麼,我們稍後來說。我們先繼續跟蹤我們的代碼,我們完成了一系列的ConstructJob以後,即我們構建完了Job以後,我們應該進入到下一個階段,Execute,即執行這一系列的Jobs.
這個過程我們則又需要大量的回溯,一直回溯到最開始driver.cpp的main函數的位置:
std::unique_ptr<Compilation> C(TheDriver.BuildCompilation(argv));n int Res = 0;n SmallVector<std::pair<int, const Command *>, 4> FailingCommands;n if (C.get())n Res = TheDriver.ExecuteCompilation(*C, FailingCommands);n
也就是說我們已經創建出來了這個Compilation,準備工作也做好了,我們需要做的是下面的ExecuteCompilation。
在這個函數中,我們發現了這行代碼:
// Set up response file names for each command, if necessaryn for (auto &Job : C.getJobs())n setUpResponseFiles(C, Job);nn C.ExecuteJobs(C.getJobs(), FailingCommands);n
我們繼續跟蹤下去
for (const auto &Job : Jobs) {n const Command *FailingCommand = nullptr;n if (int Res = ExecuteCommand(Job, FailingCommand)) {n FailingCommands.push_back(std::make_pair(Res, FailingCommand));n // Bail as soon as one command fails, so we dont output duplicate errorn // messages if we die on e.g. the same file.n return;n }n }n
我們發現了ExecuteCommand,記得我們之前把-cc1加入後,即是我們要執行的過程加入到command中,所以我們要保持一個警覺。我們繼續跟蹤進去
std::string Error;n bool ExecutionFailed;n int Res = C.Execute(Redirects, &Error, &ExecutionFailed);n
我們繼續跟蹤Execute函數
// Save the response file in the appropriate encodingn if (std::error_code EC = writeFileWithEncoding(n ResponseFile, RespContents, Creator.getResponseFileEncoding())) {n if (ErrMsg)n *ErrMsg = EC.message();n if (ExecutionFailed)n *ExecutionFailed = true;n return -1;n }nn return llvm::sys::ExecuteAndWait(Executable, Argv.data(), /*env*/ nullptr,n Redirects, /*secondsToWait*/ 0,n /*memoryLimit*/ 0, ErrMsg, ExecutionFailed);n
我們調用了這個llvm::sys::ExecuteAndWait函數,若我們稍微有一點UNIX的編程背景,看到ExecuteAndWait這樣的名詞會想到什麼呢?多進程。有了多進程,你覺得我們的-cc1能不能再回到main函數前面?可以。
那我們跟蹤進去看看這個ExecuteAndWait是否如我們所想
ProcessInfo PI;n if (Execute(PI, Program, args, envp, redirects, memoryLimit, ErrMsg)) {n if (ExecutionFailed)n *ExecutionFailed = false;n ProcessInfo Result = Wait(n PI, secondsToWait, /*WaitUntilTerminates=*/secondsToWait == 0, ErrMsg);n return Result.ReturnCode;n }n
而ProcessInfo是什麼呢?
/// @brief This struct encapsulates information about a process.nstruct ProcessInfo {n#if defined(LLVM_ON_UNIX)n typedef pid_t ProcessId;n#elif defined(LLVM_ON_WIN32)n typedef unsigned long ProcessId; // Must match the type of DWORD on Windows.n typedef void * HANDLE; // Must match the type of HANDLE on Windows.n /// The handle to the process (available on Windows only).n HANDLE ProcessHandle;n#elsen#error "ProcessInfo is not defined for this platform!"n#endifn
這下是否明了了呢?的確是如我們所猜想的一樣,與進程有關。那麼為了進一步確認,我們繼續跟蹤進去Execute函數
// Create a child process.n int child = fork();n switch (child) {n // An error occurred: Return to the caller.n case -1:n MakeErrMsg(ErrMsg, "Couldnt fork");n return false;nn // Child process: Execute the program.n case 0: {n // Redirect file descriptors...n if (redirects) {n // Redirect stdinn//.....n // Execute!n std::string PathStr = Program;n if (envp != nullptr)n execve(PathStr.c_str(),n const_cast<char **>(args),n const_cast<char **>(envp));n elsen execv(PathStr.c_str(),n const_cast<char **>(args));n // If the execve() failed, we should exit. Follow Unix protocol andn // return 127 if the executable was not found, and 126 otherwise.n // Use _exit rather than exit so that atexit functions and staticn // object destructors cloned from the parent process arentn // redundantly run, and so that any data buffered in stdio buffersn // cloned from the parent arent redundantly written out.n _exit(errno == ENOENT ? 127 : 126);n }n
fork / execve / execv,的確如我們所猜想的一樣了。那麼在執行Command的時候,發現工具鏈是Clang,則會走入到ExecuteCC1Tool中。那麼,讓我們走入到ExecuteCC1Tool函數中去吧。
void *GetExecutablePathVP = (void *)(intptr_t) GetExecutablePath;n if (Tool == "")n return cc1_main(argv.slice(2), argv[0], GetExecutablePathVP);n if (Tool == "as")n return cc1as_main(argv.slice(2), argv[0], GetExecutablePathVP);nn // Reject unknown tools.n llvm::errs() << "error: unknown integrated tool " << Tool << "n";n return 1;n
cc1as_main會什麼時候走進去?那就是我們使用內置彙編器直接編譯彙編文件的時候。我們這裡則關注cc1_main,在這個函數中會有一系列的初始化,如診斷引擎DiagnoseEngine, 編譯目標Target平台等,但是我們則關心最核心的一些東西:
std::unique_ptr<CompilerInstance> Clang(new CompilerInstance()); n//...... n// Execute the frontend actions.n Success = ExecuteCompilerInvocation(Clang.get());n // If any timers were active but havent been destroyed yet, print theirn // results now. This happens in -disable-free mode.n llvm::TimerGroup::printAll(llvm::errs());n
不好意思,有一個非核心的,最後一句代碼則是我們之前有說過的-ftime-report統計相關的,我想把代碼與之前所說的Execute的-ftime-report對應起來。那麼前面的代碼則是核心的執行核心前端的action. 那麼是怎麼執行的呢?讓我偷一個懶,請參考我這個回答:
Clang裡面真正的前端是什麼? - 編譯器 - 知乎
我在這裡則是從我們這裡的ExecuteCC1Tool開始講起來的。
所以,這裡我們稍微總結一下,在閱讀大型代碼時,我們要記得有理論支撐我們,然後抓大頭,不要迷失在細節中,這樣才能事半功倍。在下一節中,我們將會為Clang實現一個我們自定義的選項,來為我們的Driver分析做一個收尾。
推薦閱讀:
※LLVM中如何獲取程序的控制流圖CFG?
※llvm memcpy的本質?
※LLVM國內的開發者需要Social一下嗎?
※怎麼將二進位代碼轉換為中間代碼(IR)呢?
※如何理解LLVM的PassManager系統的實現?