Clang調試CUDA代碼全過程
有空再進行編輯,最近有點忙,抱歉
使用的llvm4.0+Clang4.0的版本,依據的是上次發的llvm4.0和clang4.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的源碼,可以從調試clang的main函數作為入口開始。
使用命令進入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.
因此Clang的main函數位於base_dir/tools/driver/driver.cpp文件中
使用n單步運行,到了
340 bool ClangCLMode = false;
(gdb)
341 if (TargetAndMode.second == "--driver-mode=cl" ||
的時候,從名字上看這個條件if語句用來判斷是否是ClangCLMode。我們已知CL是Windows上的標准C++編譯器,而clang是一個多端開源編譯器,同樣支持Windows平台,因此這一步是判斷環境是否為windows的CL環境
然后一直沒有執行程序塊,猜想正確
然后繼續單步調試到
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.
- 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 child是gdb中的一個選項,主要用法是
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相關的BeginSourceFile、Execute、EndSourceFile這三個函數內
其中BeginSourceFile函數內判斷了輸入是AST樹還是源碼,這里的輸入是IK_CUDA
204 if (Input.getKind() == IK_AST) {
(gdb) p Input.getKind()
$1 = clang::IK_CUDA
后邊完成的是setVirtualFileSystem、createFileManager、createSourceManager、createPreprocessor、createASTContext、CreateWrappedASTConsumer
尤其是在CreateWrappedASTConsumer中,這里邊對前端Frontend的Plugin進行了檢測,如果檢測到了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負責EndSourceFileAction和clearOutputFiles