主要內容:
- 理解
C
、C++
以及OC
的關系 - 編譯型語言與解釋型語言
- 編譯器
LLVM
與CLang
- 理解
iOS
編譯流程 - 預處理
- 編譯
- 匯編
- 鏈接
一、理解C、C++以及OC的關系
1.C語言
-
C
語言是一門面向過程的計算機編程語言,既可用于系統軟件開發,也適用于應用軟件開發; -
C
語言編譯器普遍存在于各種不同的操作系統中,例如Microsoft Windows
,Mac OS X
,Linux
,Unix
等; -
C
語言的設計影響了眾多后來的編程語言,例如C++
、Objective-C
、Java
、C#
等;
2.C++語言
- 兼容了
C
語言面向過程特點,但又進行了擴充和完善; - 作為一種面向對象的語言,具有封裝、多繼承、多態等特性;
3.Objective-C語言
- 擴展了
C
語言的能力,使其具備面向對象設計的能力,相當于C
的超集; -
OC
代碼中也可以有C
和C++
語句,它可以調用C
函數,也可以通過C++
對象訪問方法;
4.OC與C++的比較
-
OC
與C++
都是從C
語言演變而來面向對象設計語言,也都兼容標準的C
語言;但它們屬于不同的面向對象學派; - 兩者最大的不同在于:
OC
提供了運行時的動態綁定機制,而C++
是編譯時靜態綁定,并通過嵌入類和虛函數來模擬實現; -
OC
在編譯階段降低了編譯要求提高了靈活性,而C++
則是提高了編譯要求,在編譯過程中就發現更多的潛在錯誤,在運行前改正,降低了靈活性;
以下面的代碼為例,在編譯期間,C++
認為是錯誤的,而OC
則認為沒有問題:
NSString *test =(id) [[NSArray alloc] init];
OC
與C++
在使用細節上的不同如下:
- 定型:
OC
是動態定型,可以允許根據字符串名字來訪問方法和類,還可以動態鏈接和添加類; - 繼承:
OC
不支持多繼承,C++
支持多繼承; - 函數調用:
OC
通過消息傳遞實現函數調用,而C++
直接進行函數調用; - 接口:
OC
采用Protocol
形式來定義接口,而C++
采用虛函數形式來定義接口; - 重載:
OC
不允許同一個類中兩個方法有相同的名字(即使只是參數類型不同),但C++
可以;
二、編譯型語言與解釋型語言
Objective-C
屬于編譯型語言,這是為了保證iPhone
的執行效率;
1.編譯型語言
- 程序運行前,必須先通過
編譯器
生成機器碼
,機器碼直接通過CPU
執行,運行時不需要重新翻譯; - 程序執行效率高,但依賴編譯器,調試周期長、跨平臺性差些;
- 代表語言:
C
、C++
、OC
等;
2.解釋型語言
- 程序運行前,不需要進行編譯,而是以文本方式存儲程序代碼,運行時需要解釋器解釋后再運行;
- 程序執行效率低下,但是程序具有動態性,運行后也可以隨時增加和更新代碼來改變程序邏輯;
- 代表語言:
Javascript
、Python
等;
三、編譯器LLVM與CLang
1.編譯器
概念:把一種編程語言(原始語言
)轉換為另一種編程語言(目標語言)的程序;
大多數編譯器都分前端
和后端
兩部分:
- 前端:負責
詞法分析
、語法分析
、生成中間代碼
; - 后端:以
中間代碼
作為輸入,進行與架構無關的代碼優化,接著針對不同架構生成不同的機器碼;
補充:
- 前后端以
中間代碼
作為媒介,使得前后端可以獨立的變化,互不影響; - 這樣的好處在于:新增一門語言只需要修改前端,而新增一種
CPU
架構只需要修改后端即可;
2.LLVM與Clang
LLVM
是蘋果當前使用的編譯器:
-
LLVM
是一套編譯器基礎設施項目,為自由軟件,以C++
寫成,包含一系列模塊化的編譯器組件和工具鏈,用來開發編譯器前端
和后端
; - 基于
LLVM
衍生出了一些強大的子項目,比如:Clang
和LLDB
。
CLang
基于LLVM
,是一個高度模塊化開發的輕量級編譯器;
-
CLang
主要來自蘋果電腦的支持,同時支持C
、Objective-C
以及C++
; -
CLang
用于替代Xcode5
版本前使用的GCC
,編譯速度提高了3
倍:
3.理解iOS中的編譯器
- 在
iOS
開發中,通常LLVM
被認為是編譯器的后端,而Clang
是作為編譯器的前端; - 二者以
IR
(中間代碼)作為媒介,這樣前后端分離,使得前后端可以獨立的變化,互不影響; -
C
語言家族的前端是clang
,swift
的前端是swiftc
,但二者的后端都是LLVM
;
四、理解iOS編譯流程
1.編譯流程圖
LLVM的編譯過程相當復雜,iOS
代碼運行需要經過:預處理
、編譯
、匯編
、鏈接
四個關鍵階段,具體的流程如下圖:
2.準備測試文件
以OC
語言為例,詳細分析代碼的編譯流程,準備一個main.m
文件的內容如下:
#import <Foundation/Foundation.h>
/// 增加注釋:宏定義Name
#define Name "梧雨北辰"
int main(int argc, const char * argv[]) {
NSLog(@"Hello, %s", Name);
return 0;
}
五、預處理(Prepressing)
1.主要功能
- 替換宏:替換代碼中各種宏定義,如定義的常量、函數等;
- 導入頭文件:將
#include
包含的文件插入到該指令位置等; - 清理注釋:刪除所有注釋:
//
、/*
*/
等; - 條件編譯:處理
#if
、#ifdef
,#endif
等類似的條件編譯; - 添加行號和文件名標識:以便于編譯時編譯器能夠顯示警告和錯誤的所在行號;
2.查看預處理結果
使用xcrun
命令,在終端執行預處理操作:
xcrun clang -E main.m
終端顯示效果如下:
# 1 "main.m"
# 1 "<built-in>" 1
...
# 1 "/Applications/Xcode13.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Foundation.framework/Headers/FoundationLegacySwiftCompatibility.h" 1 3
# 193 "/Applications/Xcode13.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Foundation.framework/Headers/Foundation.h" 2 3
# 2 "main.m" 2
int main(int argc, const char * argv[]) {
NSLog(@"Hello, %s", "梧雨北辰");
return 0;
}
結果分析:
- 預處理后的文件中,注釋已經被清理,宏定義也已經被替換;
- 預處理后的文件有很多行,因為該過程中導入了頭文件(
Foundation.h
),而且這個過程是遞歸的;
六、編譯(Compilation)
1. 詞法分析(Lexical Analysis)
主要功能:通過掃描器,分割識別源代碼符號(如大小括號、=
、字符串);
使用xcrun
命令,在終端執行詞法分析操作:
xcrun clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
終端顯示效果如下:
annot_module_include '#import <Foundation/Foundation.h>
/' Loc=<main.m:1:1>
int 'int' [StartOfLine] Loc=<main.m:4:1>
identifier 'main' [LeadingSpace]
......
r_brace '}' [StartOfLine] Loc=<main.m:7:1>
eof '' Loc=<main.m:10:1>
結果分析:
- 每個被分割的源代碼符號都被記錄了位置,方便后續定位錯誤;
- 比如
Loc=<main.m:4:1>
就表示:'int'
這個符號是從源文件main.m
的第4
行的第1
個字符開始的;
2.語法分析(Semantic Analysis)
主要功能:對源代碼符號進行分析,驗證語法是否正確,最后生成AST
語法樹;
使用xcrun
命令,查看語法分析結果:
xcrun clang -fsyntax-only -Xclang -ast-dump main.c | open -f
AST
語法樹:
- 是抽象語法樹,結構上比代碼更精簡,遍歷速度更快;
- 能夠更快的進行靜態檢查,同時生成
IR
(中間代碼);
3.靜態分析(Static Analysis)
主要功能:對AST
樹進行遍歷分析,包括類型檢查
、方法實現檢查
,會及時提示錯誤;
4.生成中間代碼(Code Generation)
主要功能:CodeGen
負責將AST
語法樹自頂向下遍歷,逐步翻譯成IR
中間代碼;
IR
中間代碼:
- 這是一種更接近于機器碼的語言,使得編譯器被分為前端和后端,不同的平臺可以利用各自的編譯器將中間代碼,轉化為適合不同平臺的機器碼;
- 對于
iOS
系統來說,IR
中間代碼生成的就是Mach-O
可執行文件; -
IR
是前端的輸出,后端的輸入;
七、匯編(Assembly)
輸出中間代碼
標志著前端工作的完成,接下來將進入后端的處理流程。
1.LLVM優化中間代碼
中間代碼IR
進入后端,LLVM
會對其進行優化:
Optimization Level
bitcode
2.生成匯編代碼
LLVM
對IR
進行優化后,會針對不同架構生成不同匯編代碼;
匯編階段的目的:
- 將代碼匯編化,并將符號進行歸類;
- 將外部導入符號,放到重定位符號表;
- 最后生成一個或多個
.o
目標文件;
使用xcrun
命令,生成匯編文件:
xcrun clang -S main.m -o main.s
打開.s
文件,摘取內容如下:
.section __TEXT,__text,regular,pure_instructions
.build_version macos, 11, 0 sdk_version 11, 3
.globl _main ## -- Begin function main
// ......
callq _NSLog
// ......
.subsections_via_symbols
可以看到,匯編文件中的NSLog
操作已經被轉化為匯編命令形式的調用,即callq _NSLog
;
3.生成目標文件
該階段是匯編器
將匯編代碼
轉換為機器代碼
,并輸出目標文件
,即.o
文件;
使用xcrun
命令,生成目標文件:
xcrun clang -fmodules -c main.m -o main.o
使用file
命令,查看目標文件類型:
% file main.o
main.o: Mach-O 64-bit object x86_64
可以看到,匯編器生成Mach-O
格式的文件,而且是object
類型,即目標文件類型:
-
Mach-O
文件是用于iOS
和OS
平臺上的文件類型; -
Mach-O
作為a.out
格式的替代,提供了更強的擴展性,也提升了符號表中信息的訪問速度;
使用xcrun
命令,查看下main.o
中的符號:
xcrun nm -nm main.o
終端顯示效果如下:
(undefined) external _NSLog
(undefined) external ___CFConstantStringClassReference
0000000000000000 (__TEXT,__text) external _main
可以看到,此時我們使用的NSLog
函數,對應著_NSLog
符號:
-
undefined
:表示在當前文件暫時找不到符號_NSLog
; -
external
:表示這個符號是外部可以訪問的,對應表示文件私有的符號是non-external
;
八、鏈接(Linking)
主要功能:符號解析、重定位、合并目標文件,最終生成可執行文件;
1.使用xcrun命令執行鏈接,得到可執行文件
xcrun clang main.o -o main
2.使用file命令,查看文件類型
% file main
main: Mach-O 64-bit executable x86_64
% ./main
2021-10-01 19:06:41.846 main[5663:660299] Hello, 梧雨北辰
結果分析:雖然還是Mach-O
格式,但此時已經是executable
類型了,即可執行文件。而且運行該文件后也打印出了預期的結果;
3.再次使用xcrun命令,查看可執行文件的符號表
% xcrun nm -nm main
(undefined) external _NSLog (from Foundation)
(undefined) external ___CFConstantStringClassReference (from CoreFoundation)
(undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
0000000100003f40 (__TEXT,__text) external _main
0000000100008008 (__DATA,__data) non-external __dyld_private
結果分析:_NSLog
符號依然是undefined
,不過此時多了一些信息,即from Foundation
,表示這個符號來自于Foundation
,會在運行時動態綁定;
4.鏈接階段的主要任務
1.符號解析
將每個符號引用和對應的符號定義關聯起來;
- 鏈接器鏈接多文件時會創建符號表,用于記錄所有已經定義和未定義的符號;
- 出現相同符號,會報錯:
"ld:dumplicate symbols"
; - 在其他目標文件里沒有找到到符號,會報錯:
"Undefined symbols"
;
- 出現相同符號,會報錯:
- 另外,鏈接器在整理函數的符號調用關系時,可以幫助我們理清那些函數沒有被調用,并自動去除掉;
2.重定位
將變量名、函數名這些符號定義與一個內存位置關聯起來;
- 因為只有通過了綁定,機器才知道需要操作什么內存地址;
- 否則,我們就需要在寫代碼時給每個指令設置好內存地址,不僅操作繁瑣,而且容易引起出錯;
3.合并目標文件
將多個.m文件
編譯產生的.o
目標文件與其他Mach-O
文件(如dylib
、a
、tbd
),合成一個Mach-O
格式的可執行文件;
- 通常項目都會包含多個文件,不同文件之間的
變量
和接口函數
就會產生相互依賴關系; - 程序運行前,需要使用鏈接器將多個文件里的符號和地址綁定起來,才能保證整個程序里的變量、接口的正常調用;
5.理解靜態鏈接與動態鏈接
靜態鏈接:作用于編譯期,鏈接后的文件依然可能會存在一些"undefined"
的符號。但是這些符號都會被記錄下來,在運行時再通過dlopen
和dlsym
動態鏈接綁定;
動態鏈接:作用于運行時,這樣的優勢在于:諸多類似UIKit
這樣的共享庫將不必包含在每一個App
包里。比如:我們使用到的UIKit
系統庫,等到點擊App
真正開始運行之前,才會去鏈接依賴的UIKit
,鏈接完成再運行App
;