Clang調試CUDA代碼

Clang調試CUDA代碼全過程

有空再進行編輯,最近有點忙,抱歉html

使用的llvm4.0+Clang4.0的版本,依據的是上次發的llvm4.0clang4.0源碼安裝的教程http://www.javashuo.com/article/p-dsjpastg-ma.html前端

其中Clang的源碼位於llvm-4.0.0.src/tools/clang/文件夾中,在本文中,咱們的base_dir就是此目錄,即base_dir=llvm-4.0.0.src/tools/clanglinux

Clang LLVM 的一個編譯器前端,是使用C++開發的一個優秀軟件。所以分析clang的源碼,能夠從調試clangmain函數做爲入口開始。windows

使用命令進入gdb模式多線程

$gdb  ./clang++app

設置輸入參數函數

(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:這個設置僅當次有效,若是須要長期有效,請修改配置文件)ui

(gdb) set follow-fork-mode childthis

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

相關文章
相關標籤/搜索