LLVM與Clang的一些事兒

眯大帥發表於2017-12-25

在說這篇文章之前,首先我們帶入一個問題,在Xcode中我們最常使用的一個組合鍵cmd+b按下之後都進行了哪一些工作?偉大的ARC記憶體管理方式又是如何實現記憶體管理的?

又或者我不瞭解編譯過程程式碼照樣擼得飛起,摸透這晦澀難理解的東西有什麼用?

下面要開始囉嗦了

LLVM簡介-來自https://zh.wikipedia.org/wiki/LLVM

LLVM專案的發展起源於2000年伊利諾伊大學厄巴納-香檳分校維克拉姆·艾夫(Vikram Adve)與克里斯·拉特納(Chris Lattner)的研究,他們想要為所有靜態及動態語言創造出動態的編譯技術。LLVM是以BSD授權來發展的開源軟體。2005年,蘋果電腦僱用了克里斯·拉特納及他的團隊為蘋果電腦開發應用程式系統,LLVM為現今Mac OS X及iOS開發工具的一部分。

LLVM的命名最早源自於底層虛擬機器(Low Level Virtual Machine)的首字母縮寫,由於這個專案的範圍並不侷限於建立一個虛擬機器,這個縮寫導致了廣泛的疑惑。LLVM開始成長之後,成為眾多編譯工具及低階工具技術的統稱,使得這個名字變得更不貼切,開發者因而決定放棄這個縮寫的意涵,現今LLVM已單純成為一個品牌,適用於LLVM下的所有專案,包含LLVM中介碼(LLVM IR)、LLVM除錯工具、LLVM C++標準庫等。


關於swift之父加入Apple有個有趣的故事

Xcode3之前,用的是GCC
Xcode3,GCC仍然保留,但是也推出了LLVM,蘋果推薦LLVM-GCC混合編譯器,但還不是預設編譯器
Xcode4,LLVM-GCC成為預設編譯器,但GCC仍保留
Xcode4.2,LLVM3.0成為預設編譯器,純用GCC不復可能
Xcode4.6,LLVM升級到4.2版本
Xcode5,LLVM-GCC被遺棄,新的編譯器是LLVM5.0,從GCC過渡到LLVM的時代正式完成
複製程式碼

當時蘋果對Objective-C新增了許多特性,但這時的Apple使用的是當時一手遮天的GCC作為前端。GCC並不為這些新特性買賬--不給實現,因此索性後來兩者 分成兩條分支分別開發,這也造成Apple的編譯器版本遠落後於GCC的官方版本。並且GCC的程式碼耦合度太高,不好獨立,而且越是後期的版本,程式碼質量越差7,但Apple想做的很多功能(比如更好的IDE支援)需要模組化的方式來呼叫GCC,但GCC一直不給做。《GCC執行環境豁免條款 (英文版)8》從根本上限制了LLVM-GCC的開發。 所以,這種不和讓Apple一直在尋找一個高效的、模組化的、協議更放鬆的開源替代品。而UIUC的高材生Chris Lattner的LLVM顯然是一個很棒的選擇。


Clang - a C language family frontend for LLVM

Clang(發音為/ˈklæŋ/) 是一個C、C++、Objective-C和Objective-C++程式語言的編譯器前端。它採用了底層虛擬機器(LLVM)作為其後端。它的目標是提供一個GNU編譯器套裝(GCC)的替代品。作者是克里斯·拉特納,在蘋果公司的贊助支援下進行開發,而原始碼授權是使用類BSD的伊利諾伊大學厄巴納-香檳分校開原始碼許可。 Clang專案包括Clang前端和Clang靜態分析器等。

Clang的在出生之前就已經明確了他的使命——幹掉該死的GCC。有了LLVM+Clang,從此,蘋果的開發面貌煥然一新。從此擺脫了GCC的限制。客觀的說GCC是有很多的優點,例如支援多平臺,很流行,基於C無需C++編譯器即可編譯。這些優點到蘋果那就可能是缺點了,蘋果需要的是——快。這正是Clang的優點,除了快,它還有與GCC相容,記憶體佔用小,診斷資訊可讀性強,易擴充套件,易於IDE整合等等優點。有個測試資料:Clang編譯Objective-C程式碼時速度為GCC的3倍。


LLDB

GCC有個強大的診斷工具——GDB,相對應的Clang下糾錯工具就是LLDB。對於LLDB大家應該都不陌生,它繼承了GDB的優點,彌補GDB的不足。iOS開發者從gbd過渡到lldb沒有任何不適應感,最直白的原因就是lldb和gdb常用的命令很多都是一樣的,例如常用的po等。

囉嗦到此結束


iOS編譯過程

image.png

Objective-C與swift都採用Clang作為編譯器前端,編譯器前端主要進行語法分析,語義分析,生成中間程式碼,在這個過程中,會進行型別檢查,如果發現錯誤或者警告會標註出來在哪一行。

image.png

編譯器後端會進行機器無關的程式碼優化,生成機器語言,並且進行機器相關的程式碼優化,根據不同的系統架構生成不同的機器碼。

C++,Objective C都是編譯語言。編譯語言在執行的時候,必須先通過編譯器生成機器碼。

image.png

如上圖所示,在xcode按下cmd+B之後的工作流程。

預處理(Pre-process):他的主要工作就是將巨集替換,刪除註釋展開標頭檔案,生成.i檔案。 詞法分析 (Lexical Analysis):將程式碼切成一個個 token,比如大小括號,等於號還有字串等。是電腦科學中將字元序列轉換為標記序列的過程。 語法分析(Semantic Analysis):驗證語法是否正確,然後將所有節點組成抽象語法樹 AST 。由 Clang 中 Parser 和 Sema 配合完成 靜態分析(Static Analysis):使用它來表示用於分析原始碼以便自動發現錯誤。 中間程式碼生成(Code Generation):開始IR中間程式碼的生成了,CodeGen 會負責將語法樹自頂向下遍歷逐步翻譯成 LLVM IR,IR 是編譯過程的前端的輸出後端的輸入。 優化(Optimize):LLVM 會去做些優化工作,在 Xcode 的編譯設定裡也可以設定優化級別-01,-03,-0s,還可以寫些自己的 Pass,官方有比較完整的 Pass 教程: Writing an LLVM Pass — LLVM 5 documentation 。如果開啟了 bitcode 蘋果會做進一步的優化,有新的後端架構還是可以用這份優化過的 bitcode 去生成。

image.png

生成目標檔案(Assemble):生成Target相關Object(Mach-o) 連結(Link):生成 Executable 可執行檔案

經過這一步步,我們用各種高階語言編寫的程式碼就轉換成了機器可以看懂可以執行的目的碼了。


環境搭建

cd /opt
sudo mkdir llvm
sudo chown `whoami` llvm
cd llvm
export LLVM_HOME=`pwd`

git clone -b release_39 git@github.com:llvm-mirror/llvm.git llvm
git clone -b release_39 git@github.com:llvm-mirror/clang.git llvm/tools/clang
git clone -b release_39 git@github.com:llvm-mirror/clang-tools-extra.git llvm/tools/clang/tools/extra
git clone -b release_39 git@github.com:llvm-mirror/compiler-rt.git llvm/projects/compiler-rt

mkdir llvm_build
cd llvm_build
cmake ../llvm -DCMAKE_BUILD_TYPE:STRING=Release
make -j`sysctl -n hw.logicalcpu`

複製程式碼

檔案很多很大,需要下載一段時間


###Clang Static Analyzer靜態程式碼分析

clang 靜態分析是通過建立分析引擎和 checkers 所組成的架構,這部分功能可以通過 clang —analyze 命令方式呼叫。

####命令列執行 通過clang -cc1 -analyzer-checker-help可以列出能呼叫的 checker,但這些checker並不是所有都是預設開啟的

這裡使用一個預設關閉的checker-alpha.security.ArrayBoundV2作為例子進行操作
複製程式碼
$ clang -cc1 -analyzer-checker-help
 alpha.core.BoolAssignment       Warn about assigning non-{0,1} values to Boolean variables
  alpha.core.CastSize             Check when casting a malloced type T, whether the size is a multiple of the size of T
  alpha.core.CastToStruct         Check for cast from non-struct pointer to struct pointer
  alpha.core.FixedAddr            Check for assignment of a fixed address to a pointer
  alpha.core.IdenticalExpr        Warn about unintended use of identical expressions in operators
  alpha.core.PointerArithm        Check for pointer arithmetic on locations other than array elements
  alpha.core.PointerSub           Check for pointer subtractions on two pointers pointing to different memory chunks
  alpha.core.SizeofPtr            Warn about unintended use of sizeof() on pointer expressions
  alpha.cplusplus.NewDeleteLeaks  Check for memory leaks. Traces memory managed by new/delete.
  alpha.cplusplus.VirtualCall     Check virtual function calls during construction or destruction
  ...
  alpha.security.ArrayBound       Warn about buffer overflows (older checker)
  alpha.security.ArrayBoundV2     Warn about buffer overflows (newer checker)
  alpha.security.MallocOverflow   Check for overflows in the arguments to malloc()
  alpha.security.ReturnPtrRange   Check for an out-of-bound pointer being returned to callers
  ...
  core.CallAndMessage             Check for logical errors for function calls and Objective-C message expressions (e.g., uninitialized arguments, null function pointers)
  core.DivideZero                 Check for division by zero
  core.DynamicTypePropagation     Generate dynamic type information
  core.NonNullParamChecker        Check for null pointers passed as arguments to a function whose arguments are references or marked with the 'nonnull' attribute
  core.NullDereference            Check for dereferences of null pointers
  core.StackAddressEscape         Check that addresses to stack memory do not escape the function
  ...
  unix.API                        Check calls to various UNIX/Posix functions
  unix.Malloc                     Check for memory leaks, double free, and use-after-free problems. Traces memory managed by malloc()/free().
  unix.MallocSizeof               Check for dubious malloc arguments involving sizeof
  unix.MismatchedDeallocator      Check for mismatched deallocators.
  unix.cstring.BadSizeArg         Check the size argument passed into C string functions for common erroneous patterns
  unix.cstring.NullArg            Check for null pointers being passed as arguments to C string functions

複製程式碼

可以使用 -enable-checker 和 -disable-checker 開啟和禁用具體的 checker 或者 某種類別的 checker。

$ scan-build -enable-checker alpha.security.ArrayBoundV2 ... # 啟用陣列邊界檢查
複製程式碼

當然,使用scan-build啟用的checker只適用於使用scan-build生成的html報告。 scan-build在編譯安裝 llvm/clang 之後可以在/llvm/tools/clang/tools/scan-build目錄下找到

//允許未被預設允許的check並進行程式碼分析並將輸出結果輸出至網頁
./scan-build  -enable-checker alpha.security.ArrayBoundV2 --use-analyzer=/opt/llvm/llvm_build/bin -V xcodebuild  -project /Users/yuhao/TestClang/TestClang.xcodeproj -sdk iphonesimulator10.3
複製程式碼

我們在TestClang.xcodeproj的main.m檔案中插入一段陣列越界的程式碼

int main(){
    @autoreleasepool {
        int a[2];
        int i;
        for (i = 0; i < 3; i++){
            a[i] = 0;
        }
    }
    return 0;
}

複製程式碼

然後執行上面的命令,會匯出這樣的一個介面

image.png

檢視報表

image.png

報表中提示了該程式碼有陣列越界的問題。

####Xcode執行 Xcode本身已經自帶了靜態檢測的功能,可以通過Product-Analyze來執行靜態檢測,這也只是用自帶的clang去執行,如果想用其他的版本,比如自己編譯clang,就需要通過命令來設定。

image.png

在Xcode的Product選項卡下有Analyze的選項,Xcode中預設提供了一些checkers。

Usage: set-xcode-analyzer [options]

Options:
  -h, --help            show this help message and exit
  --use-checker-build=PATH
                        Use the Clang located at the provided absolute path,
                        e.g. /Users/foo/checker-1
  --use-xcode-clang     Use the Clang bundled with Xcode

複製程式碼

可以看到,它有2個選項,

--use-checker-build:用於將xcode的clang版本切換成設定的版本 --use-xcode-clang:用於將xcode的clang版本切換回去

注:在執行上面命令的時候,需要退出xcode執行;且需要用sudo的方式執行。

依然使用上面的project檔案,在Build Settings新增引數,如圖

image.png

-Xanalyzer -analyzer-checker=alpha.security.ArrayBoundV2
複製程式碼

然後cmd+shift+b

image.png

在Xcode中也出現了和報表同樣的提示。 關於checker的開發可以看這裡


###關於ARC(AUTOMATIC REFERENCE COUNTING)

ARC是ios5.0引入的新特性,完全消除手動管理記憶體的繁瑣,編譯器會自動在適合的程式碼裡面插入適當的retain,release,autorelease的語句。我們不要再擔心記憶體管理,因為編譯器幫我們做了這一切。 我們都知道ARC的規則就是隻要物件沒有強指標引用,就會被釋放掉。那麼,該物件是什麼時候被釋放,又是誰操作去釋放該物件的?

自動新增release

int main(int argc, const char * argv[]) {
    id a;
    return 0;
}
複製程式碼

上面的程式碼中有強引用的物件,通過以下命令將程式碼編譯成中間語言:

clang -S -fobjc-arc -emit-llvm main.m -o main.ll
複製程式碼

結果如下:

define i32 @main(i32, i8**) #0 {
  %3 = alloca i32, align 4
  %4 = alloca i32, align 4
  %5 = alloca i8**, align 8
  %6 = alloca i8*, align 8
  store i32 0, i32* %3, align 4
  store i32 %0, i32* %4, align 4
  store i8** %1, i8*** %5, align 8
  store i8* null, i8** %6, align 8
  store i32 0, i32* %3, align 4
  call void @objc_storeStrong(i8** %6, i8* null) #1
  %7 = load i32, i32* %3, align 4
  ret i32 %7
}
複製程式碼

alloca函式申請記憶體地址,而store表示將值存到指定地址。 函式的最後呼叫了函式objc_storeStrong,查詢ARC文件可以知道objc_storeStrong的實現。

void objc_storeStrong(id *object, id value) {
  id oldValue = *object;
  value = [value retain];
  *object = value;
  [oldValue release];
}
複製程式碼

call void @objc_storeStrong(i8** %6, i8* null)null進行了retain,對a進行了release。 綜上,在__strong型別的變數的作用域結束時,自動新增release函式進行釋放。

自動新增retain 查閱ARC文件,發現有objc_retain這樣一個函式,顧名思義,該函式就是將物件進行retain操作。

id objc_retainAutorelease(id value) {
  return objc_autorelease(objc_retain(value));
}
複製程式碼

objc_retainAutorelease(id value)valuenull或指標指向有效物件,如果valuenull,則此呼叫不起作用。否則,它執行保留操作,然後執行自動釋放操作。即對一個變數先進行一次retain,再添進行autorelease

weak的實現 runtime是如何實現在weak修飾的變數的物件在被銷燬時自動置為nil的呢?一個普遍的解釋是:runtime對註冊的類會進行佈局,對於weak修飾的物件會放入一個hash表中。用weak指向的物件記憶體地址作為key,當此物件的引用計數為0的時候會dealloc,假如weak指向的物件記憶體地址是a,那麼就會以a為鍵在這個weak表中搜尋,找到所有以a為鍵的weak物件,從而設定為nil

weak指標的實現藉助Objective-C的執行時特性,runtime通過 objc_storeWeak, objc_destroyWeakobjc_moveWeak等方法,直接修改__weak物件,來實現弱引用。

objc_storeWeak函式,將附有__weak識別符號的變數的地址註冊到weak表中,weak表是一份與引用計數表相似的雜湊表。

而該變數會在釋放的過程中清理weak表中的引用,變數釋放呼叫以下函式:

dealloc
_objec_rootDealloc
object_dispose
objc_destructInstance
objc_clear_deallocating
複製程式碼

在最後的objc_clear_deallocating函式中,從weak表中找到弱引用指標的地址,然後置為nil,並從weak表刪除記錄。

關於ARC更多實現請參閱探究ARC

相關文章