iOS之武功秘籍?: LLVM編譯流程

iOS之武功秘籍 文章匯總

寫在前面

本文主要是理解LLVM的編譯流程

本節(jié)可能用到的秘籍Demo

一、什么是編譯器?

① Python案例

  • 創(chuàng)建Python文件夾,新建helloDemo.py文件,內(nèi)容 print("hello\n")
  • 調(diào)用python helloDemo.py執(zhí)行文件,打印出hello

② C 案例

  • vim創(chuàng)建helloDemo.c文件
  • clang helloDemo.c編譯,生成a.out文件.

file a.out查看文件,發(fā)現(xiàn).out文件是:64位Mach-O可執(zhí)行文件,當(dāng)前clang出來的是x86_64架構(gòu),mac電腦可讀. 所以可以./a.out直接執(zhí)行:

③ 相關(guān)疑問

③.1 解釋型語言與編譯型語言

  • 編譯型語言編譯后輸出的是指令0、1組合),cpu可直接執(zhí)行指令
    • C語言編譯型語言,不能直接執(zhí)行,需要編譯器將其轉(zhuǎn)換成機(jī)器識(shí)別語言
  • 解釋性語言:生成的是數(shù)據(jù),不是0、1組合,機(jī)器也能直接識(shí)別
    • python解釋型語言,一邊翻譯一邊執(zhí)行.和js一樣,機(jī)器可直接執(zhí)行.

編譯器的作用,就是將高級(jí)語言轉(zhuǎn)化為機(jī)器能夠識(shí)別的語言(可執(zhí)行文件)

③.2 匯編有指令嗎?

  • 早期科學(xué)家,使用0、1編碼. 比如 00001111 對(duì)應(yīng) call00000111 對(duì)應(yīng)bl.有了對(duì)應(yīng)關(guān)系后. 再手敲0和1就有點(diǎn)難受了.于是寫個(gè)中間解釋器,我們只用輸入call、bl這樣的標(biāo)記指令,經(jīng)過解釋器,變成0和1的組合,再交給機(jī)器去執(zhí)行.這就是匯編的由來.
  • 而基于匯編往上,再映射封裝相關(guān)對(duì)應(yīng)關(guān)系.就跨時(shí)代性c語言,再往上層封裝,就出現(xiàn)了高級(jí)語言oc、swift等語言.所以匯編執(zhí)行快,因?yàn)樗?code>直接轉(zhuǎn)換為機(jī)器語言.
  • 匯編指令集,是針對(duì)同一操作系統(tǒng)而言,它不支持跨平臺(tái).機(jī)器指令cpu的在識(shí)別.早期的計(jì)算機(jī)廠家非常多,雖然都用01的組合,但相同組合背后卻是相應(yīng)不同的指令.所以匯編無法跨平臺(tái),不同操作系統(tǒng)下,匯編指令不同的.

二、LLVM概述

LLVM架構(gòu)編譯器(compiler)的框架系統(tǒng),以C++編寫而成,用于優(yōu)化以任意程序語言編寫的程序的編譯時(shí)間(compile-time)、鏈接時(shí)間(link-time)、運(yùn)行時(shí)間(run-time)以及空閑時(shí)間(idle-time),對(duì)開發(fā)者保持開放,并兼任已有腳本.
LLVM計(jì)劃啟動(dòng)于2000年,最初由美國(guó)UIUC大學(xué)的Chris Lattner博士主持開展.
2006年Chris Lattner加盟Apple Inc.并致力于LLVMApple開發(fā)體系中的應(yīng)用.Apple也是LLVM計(jì)劃的主要資助者.
目前LLVM已經(jīng)被蘋果iOS開發(fā)工具、Xilinx VivadoFacebook、Google等各大公司采用.

三、傳統(tǒng)編譯器的設(shè)計(jì)

源碼 Source Code + 前端 Frontend + 優(yōu)化器 Optimizer + 后端 Backend(代碼生成器 CodeGenerator)+ 機(jī)器碼 Machine Code,如下圖所示

編譯器前端(Frontend)
編譯器前端的任務(wù)是解析源代碼(編譯階段),它會(huì)進(jìn)行 詞法分析、語法分析、語義分析、檢查源代碼是否存在錯(cuò)誤,然后構(gòu)建抽象語法樹(Abstract Syntax Tree AST),LLVM的前端還會(huì)生成中間代碼(intermediate representation,簡(jiǎn)稱IR),可以理解為LLVM編譯器 + 優(yōu)化器, 接收的是IR中間代碼,輸出的還是IR,給后端,經(jīng)過后端翻譯成目標(biāo)指令集

優(yōu)化器(Optimizer)
優(yōu)化器負(fù)責(zé)進(jìn)行各種優(yōu)化,改善代碼的運(yùn)行時(shí)間,例如消除冗余計(jì)算等

后端(Backend)/(代碼生成器 Code Generator)
將代碼映射到目標(biāo)指令集,生成機(jī)器代語言,并且進(jìn)行機(jī)器代碼相關(guān)的代碼優(yōu)化

iOS的編譯器架構(gòu)

Objective C/C/C++ 使用的編譯器前端是Clang,Swift是swift,后端都是LLVM.

LLVM的設(shè)計(jì)

當(dāng)編譯器決定支持多種源語言或多種硬件架構(gòu)時(shí),LLVM最重要的地方就來了.其他的編譯器如GCC,它方法非常成功,但由于它是作為整體應(yīng)用程序設(shè)計(jì)的,因此它們的用途受到了很大的限制.
LLVM設(shè)計(jì)的最重要方面是,使用通用的代碼表示形式(IR),它是用來在編譯器中表示代碼的形式,所以LLVM可以為任何編程語言獨(dú)立編寫前端,并且可以為任意硬件架構(gòu)獨(dú)立編寫后端,如下所示

通俗的一句話理解就是:LLVM的設(shè)計(jì)是前后端分離的,無論前端還是后端發(fā)生變化,都不會(huì)影響另一個(gè)

Clang簡(jiǎn)介

ClangLLVM項(xiàng)目中的一個(gè)子項(xiàng)目,它是基于LLVM架構(gòu)圖的輕量級(jí)編譯器,誕生之初是為了替代GCC,提供更快的編譯速度,它是負(fù)責(zé)C、C++、OC語言的編譯器,屬于整個(gè)LLVM架構(gòu)中的 編譯器前端,對(duì)于開發(fā)者來說,研究Clang可以給我們帶來很多好處

四、LLVM編譯流程

  • 新建一個(gè)Mac OS命令行工程:
  • 沒有改動(dòng)代碼

① 打印源碼的編譯階段

  • cdmain.m的文件夾.使用clang -ccc-print-phases main.m命令查看main.m的編譯步驟:

編譯流程分為以下7步

  • 0: input, "main.m", objective-c
    • 輸入文件:找到源文件
  • 1: preprocessor, {0}, objective-c-cpp-output
    • 預(yù)處理:宏的展開,頭文件的導(dǎo)入
  • 2: compiler, {1}, ir
    • 編譯:詞法、語法、語義分析,最終生成IR
  • 3: backend, {2}, assembler ()
    • 匯編: LLVM通過一個(gè)個(gè)的Pass去優(yōu)化,每個(gè)Pass做一些事,最后生成匯編代碼
  • 4: assembler, {3}, object
    • 目標(biāo)文件
  • 5: linker, {4}, image
    • 鏈接: 鏈接需要的動(dòng)態(tài)庫和靜態(tài)庫,生成可執(zhí)行文件
  • 6: bind-arch, "x86_64", {5}, image
    • 架構(gòu)可執(zhí)行文件:通過不同架構(gòu),生成對(duì)應(yīng)的可執(zhí)行文件

optimizer優(yōu)化并沒有作為一個(gè)獨(dú)立階段,在編譯階段內(nèi)部完成的

② 預(yù)處理階段

這個(gè)階段主要是處理包括宏的替換,頭文件的導(dǎo)入,可以執(zhí)行如下命令,執(zhí)行完畢可以看到頭文件的導(dǎo)入和宏的替換

  • main.m文件中準(zhǔn)備測(cè)試代碼:
  • clang預(yù)編譯輸出main2.m文件:通過指令clang -E main.m >> main2.m
  • 打開main2.m文件其中大部分是stdio庫的代碼:
  • 我們發(fā)現(xiàn)測(cè)試代碼中的宏C,在預(yù)編譯階段完成了替換,變成了30

  • 修改測(cè)試代碼,給int類型取個(gè)別名CJ_INT_64,再次預(yù)編譯處理

  • 發(fā)現(xiàn)typedef不會(huì)被替換

小結(jié):

  • typedef在給數(shù)據(jù)類型取別名時(shí),在預(yù)處理階段不會(huì)被替換掉
  • define則在預(yù)處理階段會(huì)被替換,所以經(jīng)常被用來進(jìn)行代碼混淆,目的是為了app安全,實(shí)現(xiàn)邏輯是:將app中核心類、核心方法等用系統(tǒng)相似的名稱進(jìn)行取別名,然后在預(yù)處理階段就被替換了,來達(dá)到代碼混淆的目的

③ 編譯階段

編譯階段主要是進(jìn)行詞法、語法等的分析和檢查,然后生成中間代碼IR

③.1 詞法分析

預(yù)處理完成后就會(huì)進(jìn)行詞法分析,這里會(huì)把代碼切成一個(gè)個(gè)Token,比如大小括號(hào)、等于號(hào)還有字符串等,而且還標(biāo)注了位置第幾行的第幾個(gè)字符開始的.

  • 可以通過clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m命令查看

③.2 語法分析

詞法分析完成后就是語法分析,它的任務(wù)是驗(yàn)證語法是否正確,在詞法分析的基礎(chǔ)上將單詞序列組合成各類此法短語,如程序、語句、表達(dá)式 等等,然后將所有節(jié)點(diǎn)組成抽象語法樹(Abstract Syntax Tree, AST),語法分析程序判斷源程序結(jié)構(gòu)上是否正確.

  • 可以通過clang -fmodules -fsyntax-only -Xclang -ast-dump main.m命令查看語法分析的結(jié)果

其中,主要說明幾個(gè)關(guān)鍵字的含義

  • -FunctionDecl 函數(shù)
  • -ParmVarDecl 參數(shù)
  • -CallExpr 調(diào)用一個(gè)函數(shù)
  • -BinaryOperator 運(yùn)算符

④ 生成中間代碼IR

完成以上步驟后,就開始生成中間代碼IR了,代碼生成器(Code Generation)會(huì)將語法樹自頂向下遍歷逐步翻譯成LLVM IR.

便于理解,我們簡(jiǎn)化代碼:
  • 通過clang -S -fobjc-arc -emit-llvm main.m命令可以生成.ll的文本文件,查看IR代碼.OC代碼在這一步會(huì)進(jìn)行runtime橋接:property合成、ARC處理等

IR基本語法

  • @ 全局標(biāo)識(shí)
  • % 局部標(biāo)識(shí)
  • alloca 開辟空間
  • align 內(nèi)存對(duì)齊
  • i32 32bit,4個(gè)字節(jié)
  • store 寫入內(nèi)存
  • load 讀取數(shù)據(jù)
  • call 調(diào)用函數(shù)
  • ret 返回

下面是生成的中間代碼.ll文件

其中,test函數(shù)的參數(shù)解釋為

圖中為何多創(chuàng)建那么多局部變量?(如test函數(shù)內(nèi)的a5、a6)
因?yàn)樵谏弦浑A段(編譯階段),我們將代碼編譯成了語法樹結(jié)構(gòu).而此時(shí),我們只是沿著語法樹進(jìn)行讀取.語法樹每一個(gè)層級(jí),都需要一個(gè)臨時(shí)變量來承接.再返回上一層級(jí)處理.所以會(huì)產(chǎn)生那么多局部變量

當(dāng)然,IR文件在OC中是可以進(jìn)行優(yōu)化的,一般Xcode中設(shè)置是在target - Build Setting - Optimization Level(優(yōu)化器等級(jí))中設(shè)置.(Debug模式默認(rèn)None [O0]無優(yōu)化,Release模式默認(rèn)Fastest,Smallest [Os]最快最小)

LLVM的優(yōu)化級(jí)別分別是-O0 -O1 -O2 -O3 -Os(第一個(gè)是大寫英文字母O),下面是帶優(yōu)化的生成中間代碼IR的命令

clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll

這是優(yōu)化后的中間代碼

優(yōu)化后的代碼,舒服多了.之前那些冗余的臨時(shí)局部變量,也都被優(yōu)化,代碼量減少很多.

  • xcode7以后開啟bitcode,蘋果會(huì)做進(jìn)一步優(yōu)化,生成.bc的中間代碼,我們通過優(yōu)化后的IR代碼生成.bc代碼.
    • 優(yōu)化指令clang -emit-llvm -c main.ll -o main.bc

⑤ 生成匯編代碼

LLVM在后端主要是會(huì)通過一個(gè)個(gè)的Pass去優(yōu)化,每個(gè)Pass做一些事情,最終生成匯編代碼

  • 完成中間代碼的生成后,可以將代碼轉(zhuǎn)變?yōu)?code>匯編代碼了
  • 此刻我們有4種不同程度的代碼(源代碼->無優(yōu)化IR代碼->Os優(yōu)化IR代碼 -> bitcode優(yōu)化代碼
  • 分別對(duì)4種程度的代碼輸出匯編文件:
clang -S -fobjc-arc main.m -o main.s
clang -S -fobjc-arc main.ll -o mainO0.s
clang -S -fobjc-arc mainOs.ll -o mainOs.s
clang -S -fobjc-arc main.bc -o mainbc.s

可以看到在生成匯編代碼時(shí),只有選擇了優(yōu)化等級(jí),才能減少匯編代碼量.

  • 生成匯編代碼也可以進(jìn)行優(yōu)化---即在生成中間代碼的前后,都可以進(jìn)行優(yōu)化
    • ① 將main.m直接選擇Os級(jí)別優(yōu)化生成.s匯編文件--clang -Os -S -fobjc-arc main.m -o mainOs.s
    • ② 將main.m生成無優(yōu)化的mainO0.ll,再mainO0.ll選擇Os級(jí)別優(yōu)化生成.s匯編文件 -- clang -S -fobjc-arc -emit-llvm main.m -o mainO0.ll,clang -Os -S -fobjc-arc mainO0.ll -o mainO0Os.s
    • ③ 將main.m選擇Os級(jí)別優(yōu)化生成mainOs.ll,再mainOs.ll選擇無優(yōu)化級(jí)別生成.s匯編文件 -- clang -Os -S -fobjc-arc -emit-llvm main.m -o mainOs.ll,clang -S -fobjc-arc mainOs.ll -o mainOsO0.s
    • ④ 將main.m選擇Os級(jí)別優(yōu)化生成mainOs.ll,再mainOs.ll選擇Os級(jí)別優(yōu)化生成.s匯編文件 -- clang -Os -S -fobjc-arc -emit-llvm main.m -o mainOs.ll,clang -Os -S -fobjc-arc mainOs.ll -o mainOsOs.s

⑥ 生成目標(biāo)文件(機(jī)器代碼)

目標(biāo)文件的生成,是匯編器匯編代碼作為插入,將匯編代碼轉(zhuǎn)換為機(jī)器代碼,最后輸出目標(biāo)文件(object file)-- clang -fmodules -c main.s -o main.o

  • 此時(shí)我們file對(duì)比一下main.s匯編代碼和main.o機(jī)器代碼.
  • 可以通過nm命令,查看下main.o中的符號(hào) -- xcrun nm -nm main.o

    • _printf函數(shù)是一個(gè)是undefined 、external
    • undefined表示在當(dāng)前文件暫時(shí)找不到符號(hào)_printf
    • external表示這個(gè)符號(hào)外部可以訪問的

所以當(dāng)前雖轉(zhuǎn)換成了機(jī)器代碼.但是只是目標(biāo)文件,并不能直接執(zhí)行,需要將所有資源鏈接起來,才可以執(zhí)行.

⑦ 生成可執(zhí)行文件(鏈接)

鏈接主要是鏈接需要的動(dòng)態(tài)庫靜態(tài)庫,生成可執(zhí)行文件,其中

  • 靜態(tài)庫會(huì)和可執(zhí)行文件合并
  • 動(dòng)態(tài)庫是獨(dú)立的

連接器把編譯生成的.o文件和 .dyld.a文件鏈接,生成一個(gè)mach-o文件,接著輸入以下指令

clang main.o -o main // 將目標(biāo)文件轉(zhuǎn)成可執(zhí)行文件
file main            // 查看文件
xcrun nm -nm main    // 查看main的符號(hào)

結(jié)果如下所示,其中的undefined表示會(huì)在運(yùn)行時(shí)進(jìn)行動(dòng)態(tài)綁定

對(duì)比main.o目標(biāo)文件,此時(shí)生成的main文件:

  • object文件變成了executable可執(zhí)行文件
  • 雖然都有undefined,但是可執(zhí)行文件中指定了該符號(hào)的來源庫.機(jī)器在運(yùn)行時(shí),會(huì)從相應(yīng)的庫中取讀取該符號(hào)(printf)

⑧ 綁定

綁定主要是通過不同的架構(gòu),生成對(duì)應(yīng)的mach-o格式可執(zhí)行文件

至此,我們已完整分析了:從源代碼可執(zhí)行文件整個(gè)流程.

寫在后面

和諧學(xué)習(xí),不急不躁.我還是我,顏色不一樣的煙火.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • LLVM 簡(jiǎn)介 LLVM 是架構(gòu)編譯器(compiler)的框架系統(tǒng),以 C++ 編寫而成,用于優(yōu)化以任意程序語言...
    遠(yuǎn)方竹葉閱讀 1,123評(píng)論 0 2
  • 目錄 傳統(tǒng)編譯器設(shè)計(jì) 輸入源代碼(Obj-C, Swift, ...) → 編譯器處理 → 輸出機(jī)器碼(01010...
    小瞎_MarkDash閱讀 1,349評(píng)論 0 2
  • 前言 作為一個(gè)合格的iOS開發(fā)者,我們必須清楚,我們平時(shí)寫的代碼,是如何一步步轉(zhuǎn)變生成App包的,這個(gè)過程就是我們...
    深圳_你要的昵稱閱讀 1,959評(píng)論 0 6
  • 前言 語言類型 我們有很多維度可以將計(jì)算機(jī)語言進(jìn)行分類,其中以編譯/執(zhí)行方式為維度,可以將計(jì)算機(jī)語言分為: 編譯型...
    AiLearn閱讀 2,623評(píng)論 1 6
  • LLVM的編譯流程 在介紹編譯流程之前,首先回顧一下LLVM:LLVM是一個(gè)模塊化的、可重用的編譯器和工具鏈技術(shù)的...
    愛笑的云里看夢(mèng)閱讀 6,689評(píng)論 1 8

友情鏈接更多精彩內(nèi)容