• Clang调试CUDA代码


    Clang调试CUDA代码全过程

    有空再进行编辑,最近有点忙,抱歉

    使用的llvm4.0+Clang4.0的版本,依据的是上次发的llvm4.0clang4.0源码安装的教程https://www.cnblogs.com/jourluohua/p/9554995.html

    其中Clang的源码位于llvm-4.0.0.src/tools/clang/文件夹中,在本文中,我们的base_dir就是此目录,即base_dir=llvm-4.0.0.src/tools/clang

    Clang LLVM 的一个编译器前端,是使用C++开发的一个优秀软件。因此分析clang的源码,可以从调试clangmain函数作为入口开始。

    使用命令进入gdb模式

    $gdb  ./clang++

    设置输入参数

    (gdb) set args apxy.cu -o apxy --cuda-gpu-arch=sm_50 --cuda-path=/usr/local/cuda -L/usr/local/cuda/lib64/  -lcudart_static -ldl -lrt -pthread

    这里有个很关键的点,gdb必须设置为子线程模式,否则一直停留在父线程上,无法进入想要的函数。(Notice:这个设置仅当次有效,如果需要长期有效,请修改配置文件)

    (gdb) set follow-fork-mode child

    main函数上设置断点

    (gdb) b main

    提示Breakpoint 1 at 0x1be2b27: file base_dir/tools/driver/driver.cpp, line 308.

    因此Clangmain函数位于base_dir/tools/driver/driver.cpp文件中

    使用n单步运行,到了

    340   bool ClangCLMode = false;

    (gdb)

    341   if (TargetAndMode.second == "--driver-mode=cl" ||

    的时候,从名字上看这个条件if语句用来判断是否是ClangCLMode。我们已知CLWindows上的标准C++编译器,而clang是一个多端开源编译器,同样支持Windows平台,因此这一步是判断环境是否为windowsCL环境

    然后一直没有执行程序块,猜想正确

    然后继续单步调试到

    374   if (FirstArg != argv.end() && StringRef(*FirstArg).startswith("-cc1")) {

    (gdb)

    383   bool CanonicalPrefixes = true;

    374行是判断是否有一个参数是-cc1,这个很明显是一个gcc中常用的参数,在编译文件的时候,很多时候是一个默认的参数,这里的if判断应该为真才对。在使用clang++直接编译普通cpp文件的时候,确实为真,但是,在这里,比较奇怪的是,程序块并没有执行,条件为假,这个是调试过程中一个很奇怪的地方。忽略这里的问题,继续向下。

    (gdb)

    456   std::unique_ptr<Compilation> C(TheDriver.BuildCompilation(argv));

    这里出现了第一个关键function,这个function使用我们设置的args建立了Compilation,我们做的就是编译器的源码分析,因此这里应该是一个关键的部分。s进入这个函数。

    Compilation *Driver::BuildCompilation(ArrayRef<const char *> ArgList)  函数头是这样的,位于base_dir/lib/Driver/driver.cpp中。

    简略的扫一遍代码,发现,里边先是进行了InputArg的解析,然后根据这些args进行分析到底是采用了什么编译参数。

    单步到

      // Perform the default argument translations.

      DerivedArgList *TranslatedArgs = TranslateInputArgs(*UArgs);

     

      // Owned by the host.

      const ToolChain &TC = getToolChain(

          *UArgs, computeTargetTriple(*this, DefaultTargetTriple, *UArgs));

     

      // The compilation takes ownership of Args.

      Compilation *C = new Compilation(*this, TC, UArgs.release(), TranslatedArgs);

    按照注释中的意思,这里解析完了参数,建立了所有hostDevice上的Compilation,如果需要关注host端到底解析到了什么参数,需要关注这之前的代码

    继续单步向下,遇到

    // Populate the tool chains for the offloading devices, if any.

      CreateOffloadingDeviceToolChains(*C, Inputs);

    这个function从注释来看,应该是建立从设备(slave device或者说offloading device)

    跟进去这个函数,发现函数头是

    void Driver::CreateOffloadingDeviceToolChains(Compilation &C,  InputList &Inputs)

    // We need to generate a CUDA toolchain if any of the inputs has a CUDA type.

    从中间的代码也可以清晰的发现,这里是建立NVIDIA CUDA代码选项的函数

    llvm::Triple CudaTriple(HostTriple.isArch64Bit() ? "nvptx64-nvidia-cuda" : "nvptx-nvidia-cuda");

    这个函数退出后,回到BuildCompilation

    // Construct the list of abstract actions to perform for this compilation. On

      // MachO targets this uses the driver-driver and universal actions.

      if (TC.getTriple().isOSBinFormatMachO())

        BuildUniversalActions(*C, C->getDefaultToolChain(), Inputs);

      else

    BuildActions(*C, C->getArgs(), Inputs, C->getActions());

    根据注释中的内容,这个地方对linux的代码生成(之前已经判断出了windows代码的生成,如果是windows上代码的生成,不会走到这里),进行了一个区分,将其分成了普通linux代码和macOS,其中macOS 上,使用BuildUniversalActions函数建立Actions

    执行完该函数,返回到main函数

    457   int Res = 0;

    (gdb)

    458   SmallVector<std::pair<int, const Command *>, 4> FailingCommands;

    (gdb)

    459   if (C.get())

    (gdb)

    460     Res = TheDriver.ExecuteCompilation(*C, FailingCommands);

     

    从函数名字上看这里应该是执行了编译过程,跟进去这个函数看一下。

    int Driver::ExecuteCompilation(

        Compilation &C,

        SmallVectorImpl<std::pair<int, const Command *>> &FailingCommands) {

      // Just print if -### was present.

      if (C.getArgs().hasArg(options::OPT__HASH_HASH_HASH)) {

        C.getJobs().Print(llvm::errs(), " ", true);

        return 0;

      }

      // If there were errors building the compilation, quit now.

      if (Diags.hasErrorOccurred())

        return 1;

      // Set up response file names for each command, if necessary

      for (auto &Job : C.getJobs())

        setUpResponseFiles(C, Job);

      C.ExecuteJobs(C.getJobs(), FailingCommands);

      // Remove temp files.

    1. CleanupFileList(C.getTempFiles());

    // If the command succeeded, we are done.

      if (FailingCommands.empty())

        return 0;

    从函数体上看,先获取参数,之后执行Jobs,然后清理临时文件,是一个非常正常的流程,奇怪的是,执行到这里后if (FailingCommands.empty()),条件为真,判断失败的FailingCommands是否为空后,就直接返回到main函数了,中间经历了比较长的过程,如果没有设置set follow-fork-mode child这个选项,就一直没法进入真正的编译过程,在之前的了解中,clang中应该有一个parseAST()的函数用来生成AST树,但是如果不加set follow-fork-mode child这个选项,就是无法到达这里,即使在这里添加断点也无效。这个问题是使用gdb调试clang编译CUDA源码的主要难点。

    set follow-fork-mode childgdb中的一个选项,主要用法是

    set follow-fork-mode [parent|child]  用来调试多线程程序中,当子线程建立时,是留在父进程还是进入子进程。在默认情况下,一直留在父进程,我们的调试过程中,就会导致程序无法真正的进入编译的进程中,导致调试失败。可以说,clang编译CUDA程序的时候,先建立编译器,然后再执行编译器的时候,是通过fork子进程的方式来新启动一个编译器的。

    跟进去子进程,重新进入main函数。

    这次又将参数解析了一遍,然后运行到了

    376     if (MarkEOLs) {

    (gdb)

    380     return ExecuteCC1Tool(argv, argv[1] + 4);

    这次运行到了ExecuteCC1Tool这个函数中,这就和我们之前想象的一样了

    之后进入了cc1_main这个函数中,此函数位于base_dir/tools/driver/cc1_main.cpp

    一进来就是新建一个编译实例

    173   std::unique_ptr<CompilerInstance> Clang(new CompilerInstance());

    从这里来看,实例化了一个CompilerInstance,这个CompilerInstance是整个编译器中主要的成员,其类图如下所示:

     

    从代码上看,在

    197   bool Success = CompilerInvocation::CreateFromArgs(

    绑定了调用过程,在

    221   Success = ExecuteCompilerInvocation(Clang.get());

    执行了编译器的调用过程,ExecuteCompilerInvocation函数属于clang类中,这个类位于base_dirlibFrontendToolExecuteCompilerInvocation.cpp中,然后在

    246   std::unique_ptr<FrontendAction> Act(CreateFrontendAction(*Clang));

    中创建了FrontedAction,在后边的

    249   bool Success = Clang->ExecuteAction(*Act);

    执行了FrontendAction,进入了base_dir/lib/Frontend/CompilerInstance.cpp中的bool CompilerInstance::ExecuteAction(FrontendAction &Act)中,在执行到

    914   if (getLangOpts().CUDA && !getFrontendOpts().AuxTriple.empty()) {

    (gdb) p getLangOpts().CUDA

    $1 = 1

    这里,我们可以肯定的说,编译器的编译选项中是将其当做CUDA程序来编译,继续执行

    同时,在917行的地方,同样验证了我们的猜想

    917     TO->HostTriple = getTarget().getTriple().str();

    (gdb) p getTarget().getTriple().str()

    $3 = "nvptx64-nvidia-cuda"

    之后执行到line946开始做真正的源码的分析等工作

      for (const FrontendInputFile &FIF : getFrontendOpts().Inputs) {

        // Reset the ID tables if we are reusing the SourceManager and parsing

        // regular files.

        if (hasSourceManager() && !Act.isModelParsingAction())

          getSourceManager().clearIDTables();

     

        if (Act.BeginSourceFile(*this, FIF)) {

          Act.Execute();

          Act.EndSourceFile();

        }

      }

    前边的应该都没有做太多的工作,主要的应该在Act相关的BeginSourceFileExecuteEndSourceFile这三个函数内

    其中BeginSourceFile函数内判断了输入是AST树还是源码,这里的输入是IK_CUDA

    204   if (Input.getKind() == IK_AST) {

    (gdb) p Input.getKind()

    $1 = clang::IK_CUDA

    后边完成的是setVirtualFileSystemcreateFileManagercreateSourceManagercreatePreprocessorcreateASTContextCreateWrappedASTConsumer

    尤其是在CreateWrappedASTConsumer中,这里边对前端FrontendPlugin进行了检测,如果检测到了Plugin,需要进行Action的添加,这些Action被叫做AfterConsumers

    现在执行结束这个函数,返回到bool CompilerInstance::ExecuteAction

     if (Act.BeginSourceFile(*this, FIF)) {

          Act.Execute();

          Act.EndSourceFile();

    }

    进入Execute中,函数代码位于base_dir/lib/Frontend/FrontendAction.cpp中,函数原型是

    void ASTFrontendAction::ExecuteAction(),这个里边最重要的就是最后ParseAST( CI.getSema(), CI.getFrontendOpts().ShowStats,CI.getFrontendOpts().SkipFunctionBodies);

    这里就是建立AST树的地方

    之后的EndSourceFile负责EndSourceFileActionclearOutputFiles

  • 相关阅读:
    MySQL严格模式总结
    python筛选关键字---error
    将pem证书转换成p12格式证书
    ## 游戏网关源码赏析
    pid获取
    顺序io_磁盘队列
    nsq源码阅读3_编译nsqd
    nsq源码阅读2_核心数据结构
    nsq源码阅读1_目录结构
    如何设计Mqtt的Retain?
  • 原文地址:https://www.cnblogs.com/jourluohua/p/9630463.html
Copyright © 2020-2023  润新知