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(), "\n", 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_dir\lib\FrontendTool\ExecuteCompilerInvocation.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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM