從Oclint開始接觸Clang編譯

Artillery發表於2018-01-18

##前置工作

  • Oclint(靜態程式碼分析工具)
  • Xcpretty(格式化輸出工具)
    • 安裝: sudo gem install -n /usr/local/bin xcpretty
  • Cmake(編譯工具,這裡用來構建LLVM和Xcode工程)
    • 來源:cmake.org/download/
    • 安裝:執行cmake圖形介面程式,在左上角的選項欄中選擇Tools,點選How to install for Command Line Use,官方給出了三種安裝cmake command line tool的方法,即終端能夠識別cmake命令的方法。我選擇了官方給出的第二種方法,即複製sudo"/Applications/CMake.app/Contents/bin/cmake-gui" --install 命令到終端,然後執行
  • Ninja(比cmake更小的編譯工具,這裡用來構建LLVM)
    • 來源:github.com/ninja-build…
    • 安裝:將下好的Ninja解壓, 拷貝到一個系統目錄中 /usr/bin 來完成安裝(此處有坑,該資料夾許可權獲取:重啟Mac,按住command+R,進入recovery模式。選擇開啟Utilities下的終端,輸入:csrutil disable並回車,然後正常重啟Mac即可).

##前言 Clang是llvm的編譯器前端,非常適合進行原始碼分析.目前開源的oclint就是基於clang進行的程式碼靜態檢查.工作中遇到了一些問題需要進行程式碼分析,所以學習了一下相關知識.我們日常用的clang有這兩種: 1.Xcode內部自帶的Clang Static Analyzer(簡稱CSA) 2.OCLint 這兩者都是基於Clang的前端編譯,CSA由於被內建,所以使用起來比較方便,但是可用的檢查規則比較少,只有16條,大部分是核心向的功能例如空指標檢測,型別轉換檢測,空判斷檢測,記憶體洩漏檢測,無法檢測程式碼風格,可擴充套件性比較差。 OCLint可用的檢查規則有70+條,比OSA支援的規則多很多,還支援自定義規則,所以選擇了OCLint。

##準備開發環境

  • 安裝Oclint,得到的目錄結構如下:
.
├── README.md
├── oclint-core
├── oclint-driver
├── oclint-metrics
├── oclint-reporters
├── oclint-rules
└── oclint-scripts

複製程式碼
  • cd進入oclint-scripts檔案加,執行./make。大約30分鐘後編譯完成,大概過程是下載LLVM、clang的原始碼,編譯LLVM、clang與OCLint的預設規則。

  • 編譯成功後就可以寫規則並且編譯執行了。為了方便,OCLint提供了一個叫scaffoldRule的指令碼程式,它在oclint-rules目錄下。我們通過他傳入要生成的規則名,級別,型別,指令碼就會在目錄oclint-rules/rules/custom/自動幫我們生成一個模板程式碼,並且加入編譯路徑中。舉個例子:

# 生成一個名為BGTestRule型別是ASTVisitor的規則模板
python scaffoldRule BGTestRule -t ASTVisitor

複製程式碼

生成兩個檔案:

# CMakeLists.txt 是對規則BGTestRule的編譯描述,由make程式在編譯時使用。

├── custom
│   ├── CMakeLists.txt
│   └── BGTestRule.cpp

複製程式碼
  • 接著就可以對新新增的內容進行編譯了,不過相比於重新執行./make來說有一個更加優雅的辦法,就是將規則相關的內容整合成一個Xcode工程,並且我們的每個規則都是一個scheme,編譯時可以只選擇編譯那個選擇的規則生成對應的dylib。很簡單,OCLint工程使用CMakeLists的方式維護各個檔案的依賴關係,我們可以使用CMake自帶的功能將這些CMakeLists生成一個xcodeproj工程檔案。
# 在OCLint原始碼目錄下建立一個資料夾,我這裡命名為oclint-xcoderules

mkdir oclint-xcoderules
cd oclint-xcoderules

# 建立一個指令碼(程式碼如下段),並執行它(我寫的方便修改引數,其實裡面就一句命令,直接執行也行)(PS:剛建立的檔案是沒有執行許可權的,不要忘了chmod)

./create-xcode-rules.sh
複製程式碼

指令碼內容:

#! /bin/sh -e

cmake -G Xcode -D CMAKE_CXX_COMPILER=../build/llvm-install/bin/clang++  -D CMAKE_C_COMPILER=../build/llvm-install/bin/clang -D OCLINT_BUILD_DIR=../build/oclint-core -D OCLINT_SOURCE_DIR=../oclint-core -D OCLINT_METRICS_SOURCE_DIR=../oclint-metrics -D OCLINT_METRICS_BUILD_DIR=../build/oclint-metrics -D LLVM_ROOT=../build/llvm-install/ ../oclint-rules
複製程式碼

執行

sh ./create-xcode-rules.sh
複製程式碼

於是我們就得到了Xcode工程:

├── CMakeCache.txt
├── CMakeFiles
├── CMakeScripts
├── OCLINT_RULES.build
├── OCLINT_RULES.xcodeproj
├── cmake_install.cmake
├── create-xcode-rules.sh
├── lib
├── rules
└── rules.dl
複製程式碼

開啟OCLINT_RULES.xcodeproj:

從Oclint開始接觸Clang編譯

  • 選擇自己的規則,編譯,成功後就可以在Products下看到生成的dylib了.

  • Jenkins/Xcode 中寫入oclint的命令:

myworkspace=XXXXXX.xcworkspace # 替換workspace的名字
myscheme=XXXXX # 替換scheme的名字
xcodebuild -workspace $myworkspace -scheme $myscheme clean&&
xcodebuild -workspace $myworkspace -scheme $myscheme \
-configuration Debug \
COMPILER_INDEX_STORE_ENABLE=NO | xcpretty -r json-compilation-database -o compile_commands.json&&
oclint-json-compilation-database -e Pods -- \
-report-type pmd -o pmd.xml  \
-R 這裡是你編譯生成的dylib目錄 (ps :/oclint/oclint-xcoderules/rules.dl/Debug) \
-max-priority-1=9999 \
-max-priority-2=9999 \
-max-priority-3=9999; \
rm compile_commands.json;
if [ -f ./pmd.xml ]; then echo '-----分析完畢-----'
else echo "-----分析失敗-----"; fi

複製程式碼

##如何自定義規則 通過檢視官方文件,我們發現OCLint的原理是呼叫clang的API把一個個原始檔生成一個一個AST語法樹,然後遍歷樹中的每個節點傳入各個規則的整個過程,在Xcode8之前,Xcode各種程式碼分析外掛也是這樣的原理操作的,詳情看參考文件4,5。

###分析預設規則 下面的例子是預設規則中較為簡單的一個:判斷行長度是否超過了限制長度

class LongLineRule : public AbstractSourceCodeReaderRule
{
public:
	//過載方法  輸出規則名稱
    virtual const string name() const override
    {
        return "long line";
    }
	//過載方法  輸出規則優先順序
    virtual int priority() const override
    {
        return 3;
    }
	//過載方法  輸出規則分類
    virtual const string category() const override
    {
        return "size";
    }

	#ifdef DOCGEN
	//過載方法  輸出規則使用版本
    virtual const std::string since() const override
    {
        return "0.6";
    }
	//過載方法  輸出規則描述
    virtual const std::string description() const override
    {
        return "當某行程式碼長度過長, "
            "它很大程度上損害了可讀性。建議將長行程式碼分割成多行.";
    }
	//過載方法  輸出規則示例
    virtual const std::string example() const override
    {
        return R"rst(
	.. code-block:: cpp

    void example()
    {
        int a012345678901234567890123456789...1234567890123456789012345678901234567890123456789;
    }
        )rst";
    }
	//過載方法  輸出規則可配置引數
    virtual const std::map<std::string, std::string> thresholds() const override
    {
        std::map<std::string, std::string> thresholdMapping;
        thresholdMapping["LONG_LINE"] = "每行程式碼長度限制, 預設長度 200 .";
        return thresholdMapping;
    }
	#endif
	//核心回撥 這個回撥返回原始檔中的每一行 規則方法就寫這裡
    virtual void eachLine(int lineNumber, string line) override
    {
        int threshold = RuleConfiguration::intForKey("LONG_LINE", 200);
        int currentLineSize = line.size();
        if (currentLineSize > threshold)
        {
            string description = "該行長度: " + toString<int>(currentLineSize) +
            " 字元,預設規則限制: " + toString<int>(threshold);
            //返回閉包 此時產生一個警告
            addViolation(lineNumber, 1, lineNumber, currentLineSize, this, description);
        }
    }
};

複製程式碼

###簡單的自定義規則: 到這裡我們看新建立的規則模板就會發現,模板中這些以Visit開頭的百十個函式都是OCLint提供給開發者的回撥函式.只要在這寫回撥方法中寫規則的核心程式碼就OK啦. 示例:

	//過載方法 原始檔中所有的變數都會到這裡 傳入的引數是AST樹節點的型別
 	bool VisitVarDecl(VarDecl *declaration)
    {
        checkVarName(declaration);
        return true;
    }
    //檢測變數是否合法  
    void checkVarName(VarDecl *decl)
    {
        StringRef className = decl -> getName();
        
        //必須以小寫字母開頭
        char c = className[0];
        if (isUppercase(c))
        {
            //修正提示
            std::string tempName = className;
            tempName[0] = toUppercase(c);
            StringRef replacement(tempName);
            
            string description = tempName + "首字母應該為小寫";
            //丟擲警告
            addViolation(decl, this, description);
        }
    }

複製程式碼

分析AST語法樹

如無必要,勿增實體, 分析AST的文章好多,詳情看參考文件2,6.

###難點 規則程式碼需用C++來寫,複雜規則需要查文件->dump看AST語法樹->生成dylib->複製dylib到OCLint規則目錄下->執行檢測->檢視報告->發現問題->修改程式碼重新生成dylib,暫時還未找到解決方法.

##總結 如上所述,基於Clang的Oclint用於風格檢查,可以發現和修改的更多是一種格式上的約定和某些明顯的不容許或無效邏輯,雖可解決不少問題,但是也有其侷限性。實際工作中,一方面可限制使用某些固定的風格,更重要的是保持團隊風格的統一和規範,提高其可讀性。

##參考文件

相關文章