Clang 解析錯誤和報錯的機制?
請各位帶路的大大科普一下 Clang 錯誤解析流程和報錯流程以及涉及的函數。
謝邀!該問題留了好多天了,最近都忙著在寫寄存器分配器呢。
高亮預警!!!本文另外一個彩蛋就是能讓clang編譯器輸出中文的錯誤提示信息。感興趣的直接往下拉即可。不知道您對clang了解有多少呢? 此處我就假設你剛開始接觸clang了。
在理解Clang的錯誤解析流程之前,最好能對clang的driver有個大概的印象,
知道從命令行輸入參數之後,clang會經過哪些處理流程,然後最終執行到所需要的pass。這部分可以看一下 @藍色 的回答Clang裡面真正的前端是什麼? - 編譯器 - 知乎, 和我之前的回答最近想分析的一下gcc的源代碼,但不知從何出入手。請大神們指教指教,非常感謝。? - 知乎用戶的回答 - 知乎。從而對clang的整體框架有個大概的印象及了解。閑話少說,下面開始我的正文。
以學習為目的,最好是先看一下clang早期的版本,最新的版本太繞了。之前的回答是基於clang 3.0,此處的回答將會基於clang 2.6。因為這個版本結構更簡單,更加適合作為學習目的。我的運行平台是64bit Ubuntu 14.04.5, 編譯Clang 2.6的是g++ 4.4,cmake 3.8最新版。如果只需要關注LLVM的報錯機制以及錯誤處理流程,可以指定ParseSyntaxOnly模式,沒必要執行後端邏輯。
為了追蹤Clang語法處理和錯誤解析邏輯,我們使用如下命令:./build/bin/clang test/err.c -fsyntax-only (表示只執行語法解析和語義檢查) -v (顯示所有的參數)
得到所有的命令參數:
"/home/xlous/Development/compiler/llvm2.6/build/bin/clang-cc" -triple
x86_64-unknown-linux-gnu -fsyntax-only -disable-free -main-file-name err.c
--relocation-model static --disable-fp-elim --unwind-tables=1 --mcpu=x86-64
--fmath-errno=1 -v -fdiagnostics-show-option -x c test/err.c(此處還需要在eclipse cdt中
更改為test case的絕對路徑)
注意:當你在調試的時候,只需要把上述命令替換成你本地機器的輸出結果即可。
為了方便調試,所有的調試工作都是在eclipse cdt neon版本完成的。當然你也可以
使用gdb。如果使用gdb的話,那麼可以直接複製上述命令到終端執行,沒必要更改絕對路徑,只不過加上前綴gdb --args即可。將上述命令放入到eclipse cdt的Debug configuration中的Arguments窗口,同時在Main選項中設置C/C++ Application為本地機器中對應目錄下的clang-cc。所以最終的控制台命令就是:-triple x86_64-unknown-linux-gnu -fsyntax-only -disable-free
-main-file-name /home/xlous/Development/compiler/llvm2.6/test/err.c
--relocation-model static --disable-fp-elim --unwind-tables=1 --mcpu=x86-64
--fmath-errno=1 -v -fdiagnostics-show-option -x c /home/xlous/Development/compiler/llvm2.6/test/err.c
err.c測試文件中的內容如下所示:
/**
* This file used for demostrating how the LLVM to
* report error warning.
*/
int fun()
{
int x = 1 // error: expected ";" at end of declaration
return x;
}
enum ProgActions {
RewriteObjC, // ObjC-&>C Rewriter.
RewriteBlocks, // ObjC-&>C Rewriter for Blocks.
RewriteMacros, // Expand macros but not #includes.
RewriteTest, // Rewriter playground
FixIt, // Fix-It Rewriter
HTMLTest, // HTML displayer testing stuff.
EmitAssembly, // Emit a .s file.
EmitLLVM, // Emit a .ll file.
EmitBC, // Emit a .bc file.
EmitLLVMOnly, // Generate LLVM IR, but do not
EmitHTML, // Translate input source into HTML.
ASTPrint, // Parse ASTs and print them.
ASTPrintXML, // Parse ASTs and print them in XML.
ASTDump, // Parse ASTs and dump them.
ASTView, // Parse ASTs and view them in Graphviz.
PrintDeclContext, // Print DeclContext and their Decls.
ParsePrintCallbacks, // Parse and print each callback.
ParseSyntaxOnly, // Parse and perform semantic analysis.
ParseNoop, // Parse with noop callbacks.
RunPreprocessorOnly, // Just lex, no output.
PrintPreprocessedInput, // -E mode.
DumpTokens, // Dump out preprocessed tokens.
DumpRawTokens, // Dump out raw tokens.
RunAnalysis, // Run one or more source code analyses.
GeneratePTH, // Generate pre-tokenized header.
GeneratePCH, // Generate pre-compiled header.
InheritanceView // View C++ inheritance for a specified class.
};
在處理完成輸入的編譯選項之後,循環處理每個輸入的文件,調用processInputFile函數。該執行邏輯會一直運行,直到ParseAST.cpp中的ParseAST函數。
所有的解析趟都會把ParseAST.cpp文件中第32行的ParseAST函數當做一個非常重要的中間處理函數。該函數用於調用parser解析頂層聲明,並且調用相應的ASTConsumer實例(如:BackendConsumer,用於產生後端代碼,包括llvm,彙編代碼和obj文件,以及ParseSyntaxOnly模式,在終端使用選項-fsyntax-only指定)。/// ParseAST - Parse the entire file specified, notifying the ASTConsumer as
/// the file is parsed. This inserts the parsed decls into the translation unit
/// held by Ctx.
///
void clang::ParseAST(Preprocessor PP, ASTConsumer *Consumer,
ASTContext Ctx, bool PrintStats,
bool CompleteTranslationUnit) {
// Collect global stats on Decls/Stmts (until we have a module streamer).
if (PrintStats) {
Decl::CollectingStats(true);
Stmt::CollectingStats(true);
}
Sema S(PP, Ctx, *Consumer, CompleteTranslationUnit);
Parser P(PP, S);
PP.EnterMainSourceFile();
// Initialize the parser.
P.Initialize();
Consumer-&>Initialize(Ctx);
if (SemaConsumer *SC = dyn_cast&
SC-&>InitializeSema(S);
if (ExternalASTSource *External = Ctx.getExternalSource()) {
if (ExternalSemaSource *ExternalSema =
dyn_cast&
ExternalSema-&>InitializeSema(S);
External-&>StartTranslationUnit(Consumer);
}
Parser::DeclGroupPtrTy ADecl;
while (!P.ParseTopLevelDecl(ADecl)) { // Not end of file.
// If we got a null return and something *was* parsed, ignore it. This
// is due to a top-level semicolon, an action override, or a parse error
// skipping something.
if (ADecl)
Consumer-&>HandleTopLevelDecl(ADecl.getAsVal&
};
// process any TopLevelDecls generated by #pragma weak
for (llvm::SmallVector&
I = S.WeakTopLevelDecls().begin(),
E = S.WeakTopLevelDecls().end(); I != E; ++I)
Consumer-&>HandleTopLevelDecl(DeclGroupRef(*I));
Consumer-&>HandleTranslationUnit(Ctx);
if (PrintStats) {
fprintf(stderr, "
STATISTICS:
");
P.getActions().PrintStats();
Ctx.PrintStats();
Decl::PrintStats();
Stmt::PrintStats();
Consumer-&>PrintStats();
Decl::CollectingStats(false);
Stmt::CollectingStats(false);
}
該函數主要任務就是對每個top level decleration(包括變數和函數),調用parser解析得到一顆正確的語法樹(有可能是多個,比如int x, y, z;)。
既然題主只想探究clang的錯誤處理機制,那麼此處就可以不關注其他ASTConsumer了。此處clang有個小陷阱,clang會在所有的.c文件之前添加一個如下宏定義,所以在執行下述while循環的時候,需要跳過前面兩次迭代之後,才能解析fun函數的聲明。typedef __int128_t __int128_t;
typedef __uint128_t __uint128_t;
struct __va_list_tag {
unsigned int gp_offset;
unsigned int fp_offset;
void *overflow_arg_area;
void *reg_save_area;
};
typedef struct __va_list_tag __va_list_tag;
int fun() {
int x = 1;
return x;
}
while (!P.ParseTopLevelDecl(ADecl)) { 該行代碼調用parser解析頂層聲明。
邏輯從而轉入下述函數。
然後調用ParseExternalDeclaration(),解析external-declaration語法節點。此處需要祭出C89語法規範中的BNF語法了。&
| &
函數聲明:
&
變數聲明:
&
&
| &
&
| "{" &
| "{" &
& 那麼很簡單我們按照上面的BNF語法按圖索驥依次調用相應的處理函數即可,此時會轉入
| &
;
我們不關心這個,如果題主有興趣,可以自己測試一下效果。
一直往下走就到了下面的步驟了調用ParseDeclaration(Declarator::FileContext, DeclEnd); 解析變數聲明或者函數聲明,然後進入關鍵的函數:
首先調用ParseDeclarationSpecifiers(DS);解析共同的DeclSpec,然後調用ParseDeclarator(DeclaratorInfo);解析declarator。
}? & & ::= "*" & & ")"&
| "(" &
| &
| &
| &
declarator可以表示單個標識符,函數聲明器,數組聲明器,指針聲明等。
在err.c文件中,錯誤的一行 int x = 1決定了處理邏輯會進入到 處理單一標識符的情況此時,int x已經識別完成了,處理邏輯返回到ParseSimpleDeclaration函數的下列語句,解析declarator可能存在的初始化列表,比如:
int x = 1; // variable initializer list
int arr[2] = {1, 2,}; // compound initializer list
struct X
{
int a; int b;
};
struct X x = {.a = 1, .b = 2}; // aggregate initialization
我們的測試例子非常的簡單,僅僅是一個賦值表達式而已。由於我們的關注點並不在C語法,此處略過。
由於x = 1後面缺少了一個分號,將會執行到錯誤處理邏輯
在Diag(Tok, diag::err_expected_semi_declaration);語句中Tok表示當前正在處理的token,那麼在err.c文件中就是下一條語句的return。diag::err_expected_semi_declaration是錯誤提示信息的宏定義,Clang2.6中所有的錯誤定義宏都在DiagnosticParseKinds.td文件中,會由工具生成一個DiagnosticParseKinds.inc頭文件。具體內容如下:DIAG(err_expected_semi_declaration, CLASS_ERROR, (unsigned)diag::Severity::Error, "expected ";" at end of declaration", 0, SFINAE_SubstitutionFailure, false, true, 4)每一個DIAG宏定義都是用於初始化一個StaticDiagInfoRec對象,
clang中所有的StaticDiagInfoRec對象都在Diagostic.cpp文件中下列代碼聲明static const StaticDiagInfoRec StaticDiagInfo[] = {
#define DIAG(ENUM,CLASS,DEFAULT_MAPPING,DESC,GROUP,SFINAE)
{ diag::ENUM, DEFAULT_MAPPING, CLASS, SFINAE, DESC, GROUP },
#include "clang/Basic/DiagnosticCommonKinds.inc"
#include "clang/Basic/DiagnosticDriverKinds.inc"
#include "clang/Basic/DiagnosticFrontendKinds.inc"
#include "clang/Basic/DiagnosticLexKinds.inc"
#include "clang/Basic/DiagnosticParseKinds.inc"
#include "clang/Basic/DiagnosticASTKinds.inc"
#include "clang/Basic/DiagnosticSemaKinds.inc"
#include "clang/Basic/DiagnosticAnalysisKinds.inc"
{ 0, 0, 0, 0, 0, 0}
};
#undef DIAG
如果題主需要自定義錯誤顯示內容或者其他,可以在DiagnosticParseKinds.td文件中增加您需要的內容,比如:我需要把這句話的錯誤提示改為中文,我在DiagnosticParseKinds.td文件最後追加了一行錯誤提示,表示中文版的語法錯誤提示。
// 自己新增的錯誤提示
def err_expected_semi_declaration_chinese : Error&<
"您在該行聲明之後是否少了個";"或者是中文輸入法?"&>;
注意:這一步是體現神奇的地方:
然後把ParseSimpleDeclaration函數的Diag(Tok, diag::err_expected_semi_declaration);調用改成中文版的錯誤提示Diag(Tok, diag::err_expected_semi_declaration_chinese);注意:由於我們以及修改了clangDiagnosticParseKinds.td,所以需要我們重新編譯一下。編譯命令使用cmake即可,命令如下:#注意:執行該命令之前,需要在llvm源碼目錄同級目錄新建一個build目錄,存放構建結果,這是cmake和其他構建工具都推薦的out of source 構建原則,避免污染源代碼結構。
#enter build directory and clear all.
#因為修改了clangDiagnosticParseKinds.td,所以需要重新生成對應的inc頭文件,就必須重新構建而不是增量編譯。
cd build rm -rf *
cmake ../llvm/ -DCMAKE_BUILD_TYPE=Debug -DLLVM_TARGETS_TO_BUILD=X86
#執行cmake之後,就是make了。
make -j2
當編譯完成之後,重新執行下述命令就可以見證奇蹟了
clang test/err.c -fsyntax-only
推薦閱讀:
※C++或QT項目如何進行CI(Continuous Integration)?
※Visual studio中的「添加引用」是什麼意思?
※C++中的數組與指針的一個小疑惑?
※C++解析xml有什麼好用的輪子?
※為什麼Python里類中方法self是顯式的,而C++中this是隱式的?