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;
}

調試的入口函數在clang-cc.cpp2150行的main函數。該函數會處理輸入的編譯選項,

確定用戶告訴clang要執行什麼動作,所有的動作選項在clang-cc-cpp文件中定義。

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&(Consumer))
SC-&>InitializeSema(S);

if (ExternalASTSource *External = Ctx.getExternalSource()) {
if (ExternalSemaSource *ExternalSema =
dyn_cast&(External))
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&::iterator
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語法按圖索驥依次調用相應的處理函數即可,此時會轉入

這個函數裡面使用了一個大大的switch語句用於處理各種各樣的情況。

比如第一個case tok::semi,那麼就相當於下列代碼,一個單獨的;在函數外面,這個地方就會調用Diag(Tok, diag::ext_top_level_semi)函數,報告一個錯誤,直接返回。

;

我們不關心這個,如果題主有興趣,可以自己測試一下效果。

一直往下走就到了下面的步驟了

調用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是隱式的?

TAG:C | 編譯原理 | 編譯器 | Clang | LLVM |