ios .mm文件调用c语言函数报错,深入浅出 iOS 编译
前言
兩年前曾經(jīng)寫(xiě)過(guò)一篇關(guān)于編譯的文章《iOS編譯過(guò)程的原理和應(yīng)用》,這篇文章介紹了iOS編譯相關(guān)基礎(chǔ)知識(shí)和簡(jiǎn)單應(yīng)用,但也很有多問(wèn)題都沒(méi)有解釋清楚:
Clang和LLVM究竟是什么
源文件到機(jī)器碼的細(xì)節(jié)
Linker做了哪些工作
編譯順序如何確定
頭文件是什么?XCode是如何找到頭文件的?
Clang Module
簽名是什么?為什么要簽名
為了搞清楚這些問(wèn)題,我們來(lái)挖掘下XCode編譯iOS應(yīng)用的細(xì)節(jié)。
編譯器
把一種編程語(yǔ)言(原始語(yǔ)言)轉(zhuǎn)換為另一種編程語(yǔ)言(目標(biāo)語(yǔ)言)的程序叫做編譯器。
大多數(shù)編譯器由兩部分組成:前端和后端。
前端負(fù)責(zé)詞法分析,語(yǔ)法分析,生成中間代碼;
后端以中間代碼作為輸入,進(jìn)行行架構(gòu)無(wú)關(guān)的代碼優(yōu)化,接著針對(duì)不同架構(gòu)生成不同的機(jī)器碼。
前后端依賴(lài)統(tǒng)一格式的中間代碼(IR),使得前后端可以獨(dú)立的變化。新增一門(mén)語(yǔ)言只需要修改前端,而新增一個(gè)CPU架構(gòu)只需要修改后端即可。
Objective C/C/C++使用的編譯器前端是clang,swift是swift,后端都是LLVM。
LLVM
LLVM(Low Level Virtual Machine)是一個(gè)強(qiáng)大的編譯器開(kāi)發(fā)工具套件,聽(tīng)起來(lái)像是虛擬機(jī),但實(shí)際上LLVM和傳統(tǒng)意義的虛擬機(jī)關(guān)系不大,只不過(guò)項(xiàng)目最初的名字是LLVM罷了。
LLVM的核心庫(kù)提供了現(xiàn)代化的source-target-independent優(yōu)化器和支持諸多流行CPU架構(gòu)的代碼生成器,這些核心代碼是圍繞著LLVM IR(中間代碼)建立的。
基于LLVM,又衍生出了一些強(qiáng)大的子項(xiàng)目,其中iOS開(kāi)發(fā)者耳熟能詳?shù)氖?#xff1a;Clang和LLDB。
clang
clang是C語(yǔ)言家族的編譯器前端,誕生之初是為了替代GCC,提供更快的編譯速度。一張圖了解clang編譯的大致流程:
接下來(lái),從代碼層面看一下具體的轉(zhuǎn)化過(guò)程,新建一個(gè)main.c:
#include
// 一點(diǎn)注釋
#define DEBUG 1
int main() {
#ifdef DEBUG
printf("hello debug\n");
#else
printf("hello world\n");
#endif
return 0;
}
預(yù)處理(preprocessor)
預(yù)處理會(huì)替進(jìn)行頭文件引入,宏替換,注釋處理,條件編譯(#ifdef)等操作。
#include "stdio.h"就是告訴預(yù)處理器將這一行替換成頭文件stdio.h中的內(nèi)容,這個(gè)過(guò)程是遞歸的:因?yàn)閟tdio.h也有可能包含其頭文件。
用clang查看預(yù)處理的結(jié)果:
xcrun clang -E main.c
預(yù)處理后的文件有400多行,在文件的末尾,可以找到main函數(shù)
int main() {
printf("hello debug\n");
return 0;
}
可以看到,在預(yù)處理的時(shí)候,注釋被刪除,條件編譯被處理。
詞法分析(lexical anaysis)
詞法分析器讀入源文件的字符流,將他們組織稱(chēng)有意義的詞素(lexeme)序列,對(duì)于每個(gè)詞素,此法分析器產(chǎn)生詞法單元(token)作為輸出。
$ xcrun clang -fmodules -fsyntax-only -Xclang -dump-tokens main.c
輸出:
annot_module_include '#include
int 'int' [StartOfLine]Loc=<4:1>4:1>
identifier 'main' [LeadingSpace]Loc=<4:5>4:5>
....
Loc=<1:1>標(biāo)示這個(gè)token位于源文件main.c的第1行,從第1個(gè)字符開(kāi)始。保存token在源文件中的位置是方便后續(xù)clang分析的時(shí)候能夠找到出錯(cuò)的原始位置。1:1>
語(yǔ)法分析(semantic analysis)
詞法分析的Token流會(huì)被解析成一顆抽象語(yǔ)法樹(shù)(abstract syntax tree - AST)。
$ xcrun clang -fsyntax-only -Xclang -ast-dump main.c | open -f
main函數(shù)AST的結(jié)構(gòu)如下:
�[0;34m`-�[0m�[0;1;32mFunctionDecl�[0m�[0;33m 0x7fcc188dc700�[0m �[0;33mline:4:5�[0m�[0;1;36m main�[0m �[0;32m'int ()'�[0m
�[0;34m `-�[0m�[0;1;35mCompoundStmt�[0m�[0;33m 0x7fcc188dc918�[0m
�[0;34m |-�[0m�[0;1;35mCallExpr�[0m�[0;33m 0x7fcc188dc880�[0m �[0;32m'int'�[0m�[0;36m�[0m�[0;36m�[0m
�[0;34m | |-�[0m�[0;1;35mImplicitCastExpr�[0m�[0;33m 0x7fcc188dc868�[0m �[0;32m'int (*)(const char *, ...)'�[0m�[0;36m�[0m�[0;36m�[0m
�[0;34m | | `-�[0m�[0;1;35mDeclRefExpr�[0m�[0;33m 0x7fcc188dc7a0�[0m �[0;32m'int (const char *, ...)'�[0m�[0;36m�[0m�[0;36m�[0m �[0;1;32mFunction�[0m�[0;33m 0x7fcc188c5160�[0m�[0;1;36m 'printf'�[0m �[0;32m'int (const char *, ...)'�[0m
�[0;34m | `-�[0m�[0;1;35mImplicitCastExpr�[0m�[0;33m 0x7fcc188dc8c8�[0m �[0;32m'const char *'�[0m�[0;36m�[0m�[0;36m�[0m
�[0;34m | `-�[0m�[0;1;35mImplicitCastExpr�[0m�[0;33m 0x7fcc188dc8b0�[0m �[0;32m'char *'�[0m�[0;36m�[0m�[0;36m�[0m
�[0;34m | `-�[0m�[0;1;35mStringLiteral�[0m�[0;33m 0x7fcc188dc808�[0m �[0;32m'char [13]'�[0m�[0;36m lvalue�[0m�[0;36m�[0m�[0;1;36m "hello debug\n"�[0m
�[0;34m `-�[0m�[0;1;35mReturnStmt�[0m�[0;33m 0x7fcc188dc900�[0m
�[0;34m `-�[0m�[0;1;35mIntegerLiteral�[0m�[0;33m 0x7fcc188dc8e0�[0m �[0;32m'int'�[0m�[0;36m�[0m�[0;36m�[0m�[0;1;36m 0�[0m
有了抽象語(yǔ)法樹(shù),clang就可以對(duì)這個(gè)樹(shù)進(jìn)行分析,找出代碼中的錯(cuò)誤。比如類(lèi)型不匹配,亦或Objective C中向target發(fā)送了一個(gè)未實(shí)現(xiàn)的消息。
AST是開(kāi)發(fā)者編寫(xiě)clang插件主要交互的數(shù)據(jù)結(jié)構(gòu),clang也提供很多API去讀取AST。更多細(xì)節(jié):Introduction to the Clang AST。
CodeGen
CodeGen遍歷語(yǔ)法樹(shù),生成LLVM IR代碼。LLVM IR是前端的輸出,后端的輸入。
xcrun clang -S -emit-llvm main.c -o main.ll
main.ll文件內(nèi)容:
...
@.str = private unnamed_addr constant [13 x i8] c"hello debug
...
@.str = private unnamed_addr constant [13 x i8] c"hello debug\0A\00", align 1
; Function Attrs: noinline nounwind optnone ssp uwtable
define i32 @main() #0 {
%1 = alloca i32, align 4
store i32 0, i32* %1, align 4
%2 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([13 x i8], [13 x i8]* @.str, i32 0, i32 0))
ret i32 0
}
...
A
...
@.str = private unnamed_addr constant [13 x i8] c"hello debug\0A\00", align 1
; Function Attrs: noinline nounwind optnone ssp uwtable
define i32 @main() #0 {
%1 = alloca i32, align 4
store i32 0, i32* %1, align 4
%2 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([13 x i8], [13 x i8]* @.str, i32 0, i32 0))
ret i32 0
}
...
0", align 1
; Function Attrs: noinline nounwind optnone ssp uwtable
define i32 @main() #0 {
%1 = alloca i32, align 4
store i32 0, i32* %1, align 4
%2 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([13 x i8], [13 x i8]* @.str, i32 0, i32 0))
ret i32 0
}
...
Objective C代碼在這一步會(huì)進(jìn)行runtime的橋接:property合成,ARC處理等。
LLVM會(huì)對(duì)生成的IR進(jìn)行優(yōu)化,優(yōu)化會(huì)調(diào)用相應(yīng)的Pass進(jìn)行處理。Pass由多個(gè)節(jié)點(diǎn)組成,都是Pass類(lèi)的子類(lèi),每個(gè)節(jié)點(diǎn)負(fù)責(zé)做特定的優(yōu)化,更多細(xì)節(jié):Writing an LLVM Pass。
生成匯編代碼
LLVM對(duì)IR進(jìn)行優(yōu)化后,會(huì)針對(duì)不同架構(gòu)生成不同的目標(biāo)代碼,最后以匯編代碼的格式輸出:
生成arm 64匯編:
$ xcrun clang -S main.c -o main.s
查看生成的main.s文件,篇幅有限,對(duì)匯編感興趣的同學(xué)可以看看我的這篇文章:iOS匯編快速入門(mén)。
_main: ## @main
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
...
匯編器
匯編器以匯編代碼作為輸入,將匯編代碼轉(zhuǎn)換為機(jī)器代碼,最后輸出目標(biāo)文件(object file)。
$ xcrun clang -fmodules -c main.c -o main.o
還記得我們代碼中調(diào)用了一個(gè)函數(shù)printf么?通過(guò)nm命令,查看下main.o中的符號(hào)
$ xcrun nm -nm main.o
(undefined) external _printf
0000000000000000 (__TEXT,__text) external _main
_printf是一個(gè)是undefined external的。undefined表示在當(dāng)前文件暫時(shí)找不到符號(hào)_printf,而external表示這個(gè)符號(hào)是外部可以訪問(wèn)的,對(duì)應(yīng)表示文件私有的符號(hào)是non-external。
Tips:什么是符號(hào)(Symbols)? 符號(hào)就是指向一段代碼或者數(shù)據(jù)的名稱(chēng)。還有一種叫做WeakSymols,也就是并不一定會(huì)存在的符號(hào),需要在運(yùn)行時(shí)決定。比如iOS 12特有的API,在iOS11上就沒(méi)有。
鏈接
連接器把編譯產(chǎn)生的.o文件和(dylib,a,tbd)文件,生成一個(gè)mach-o文件。
$ xcrun clang main.o -o main
我們就得到了一個(gè)mach o格式的可執(zhí)行文件
$ file main
main: Mach-O 64-bit executable x86_64
$ ./main
hello debug
在用nm命令,查看可執(zhí)行文件的符號(hào)表:
$ nm -nm main
(undefined) external _printf (from libSystem)
(undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
0000000100000f60 (__TEXT,__text) external _main
_printf仍然是undefined,但是后面多了一些信息:from libSystem,表示這個(gè)符號(hào)來(lái)自于libSystem,會(huì)在運(yùn)行時(shí)動(dòng)態(tài)綁定。
XCode編譯
通過(guò)上文我們大概了解了Clang編譯一個(gè)C語(yǔ)言文件的過(guò)程,但是XCode開(kāi)發(fā)的項(xiàng)目不僅僅包含了代碼文件,還包括了圖片,plist等。以的iOS App為例,我們來(lái)看看在XCode中編譯一次都要經(jīng)過(guò)哪些過(guò)程?
新建一個(gè)單頁(yè)面的Demo工程:CocoaPods依賴(lài)AFNetworking和SDWebImage,同時(shí)依賴(lài)于一個(gè)內(nèi)部Framework。按下Command+B,在XCode的Report Navigator模塊中,可以找到編譯的詳細(xì)日志:
詳細(xì)的步驟如下:
創(chuàng)建Product.app的文件夾
把Entitlements.plist寫(xiě)入到DerivedData里,處理打包的時(shí)候需要的信息(比如application-identifier)。
創(chuàng)建一些輔助文件,比如各種.hmap,這是headermap文件,具體作用下文會(huì)講解。
執(zhí)行CocoaPods的編譯前腳本:檢查Manifest.lock文件。
編譯.m文件,生成.o文件。
鏈接動(dòng)態(tài)庫(kù),o文件,生成一個(gè)mach o格式的可執(zhí)行文件。
編譯assets,編譯storyboard,鏈接storyboard
拷貝動(dòng)態(tài)庫(kù)Logger.framework,并且對(duì)其簽名
執(zhí)行CocoaPods編譯后腳本:拷貝CocoaPods Target生成的Framework
對(duì)Demo.App簽名,并驗(yàn)證(validate)
生成Product.app
編譯順序
編譯的時(shí)候有很多的Task(任務(wù))要去執(zhí)行,XCode如何決定Task的執(zhí)行順序呢?
答案是:依賴(lài)關(guān)系。
還是以剛剛的Demo項(xiàng)目為例,整個(gè)依賴(lài)關(guān)系如下:
可以從XCode的Report Navigator看到Target的編譯順序:
XCode編譯的時(shí)候會(huì)盡可能的利用多核性能,多Target并發(fā)編譯。
那么,XCode又從哪里得到了這些依賴(lài)關(guān)系呢?
Target Dependencies - 顯式聲明的依賴(lài)關(guān)系
Linked Frameworks and Libraries - 隱式聲明的依賴(lài)關(guān)系
Build Phase - 定義了編譯一個(gè)Target的每一步
增量編譯
日常開(kāi)發(fā)中,一次完整的編譯可能要幾分鐘,甚至幾十分鐘,而增量編譯只需要不到1分鐘,為什么增量編譯會(huì)這么快呢?
因?yàn)閄Code會(huì)對(duì)每一個(gè)Task生成一個(gè)哈希值,只有哈希值改變的時(shí)候才會(huì)重新編譯。
比如,修改了ViewControler.m,只有圖中灰色的三個(gè)Task會(huì)重新執(zhí)行(這里不考慮build phase腳本)。
頭文件
C語(yǔ)言家族中,頭文件(.h)文件用來(lái)引入函數(shù)/類(lèi)/宏定義等聲明,讓開(kāi)發(fā)者更靈活的組織代碼,而不必把所有的代碼寫(xiě)到一個(gè)文件里。
頭文件對(duì)于編譯器來(lái)說(shuō)就是一個(gè)promise。頭文件里的聲明,編譯會(huì)認(rèn)為有對(duì)應(yīng)實(shí)現(xiàn),在鏈接的時(shí)候再解決具體實(shí)現(xiàn)的位置。
當(dāng)只有聲明,沒(méi)有實(shí)現(xiàn)的時(shí)候,鏈接器就會(huì)報(bào)錯(cuò)。
Undefined symbols for architecture arm64:
"_umimplementMethod", referenced from:
-[ClassA method] in ClassA.o
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
Objective C的方法要到運(yùn)行時(shí)才會(huì)報(bào)錯(cuò),因?yàn)镺bjective C是一門(mén)動(dòng)態(tài)語(yǔ)言,編譯器無(wú)法確定對(duì)應(yīng)的方法名(SEL)在運(yùn)行時(shí)到底有沒(méi)有實(shí)現(xiàn)(IMP)。
日常開(kāi)發(fā)中,兩種常見(jiàn)的頭文件引入方式:
#include "CustomClass.h" //自定義
#include //系統(tǒng)或者內(nèi)部framework
引入的時(shí)候并沒(méi)有指明文件的具體路徑,編譯器是如何找到這些頭文件的呢?
回到XCode的Report Navigator,找到上一個(gè)編譯記錄,可以看到編譯ViewController.m的具體日志:
把這個(gè)日志整體拷貝到命令行中,然后最后加上-v,表示我們希望得到更多的日志信息,執(zhí)行這段代碼,在日志最后可以看到clang是如何找到頭文件的:
#include "..." search starts here:
/Users/.../Build/Intermediates.noindex/Demo.build/Debug-iphoneos/Demo.build/Demo-generated-files.hmap (headermap)
/Users/.../Build/Intermediates.noindex/Demo.build/Debug-iphoneos/Demo.build/Demo-project-headers.hmap (headermap)
/Users/.../Build/Products/Debug-iphoneos/AFNetworking/AFNetworking.framework/Headers
/Users/.../Build/Products/Debug-iphoneos/SDWebImage/SDWebImage.framework/Headers
#include <...> search starts here:
/Users/.../Build/Intermediates.noindex/Demo.build/Debug-iphoneos/Demo.build/Demo-own-target-headers.hmap (headermap)
/Users/.../Build/Intermediates.noindex/Demo.build/Debug-iphoneos/Demo.build/Demo-all-non-framework-target-headers.hmap (headermap)
/Users/.../Build/Intermediates.noindex/Demo.build/Debug-iphoneos/Demo.build/DerivedSources
/Users/.../Build/Products/Debug-iphoneos (framework directory)
/Users/.../Build/Products/Debug-iphoneos/AFNetworking (framework directory)
/Users/.../Build/Products/Debug-iphoneos/SDWebImage (framework directory)
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/10.0.0/include
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include
$SDKROOT/usr/include
$SDKROOT/System/Library/Frameworks (framework directory)
End of search list.
這里有個(gè)文件類(lèi)型叫做heademap,headermap是幫助編譯器找到頭文件的輔助文件:存儲(chǔ)這頭文件到其物理路徑的映射關(guān)系。
可以通過(guò)一個(gè)輔助的小工具h(yuǎn)map查看hmap中的內(nèi)容:
192:Desktop Leo$ ./hmap print Demo-project-headers.hmap
AppDelegate.h -> /Users/huangwenchen/Desktop/Demo/Demo/AppDelegate.h
Demo-Bridging-Header.h -> /Users/huangwenchen/Desktop/Demo/Demo/Demo-Bridging-Header.h
Dummy.h -> /Users/huangwenchen/Desktop/Demo/Framework/Dummy.h
Framework.h -> Framework/Framework.h
TestView.h -> /Users/huangwenchen/Desktop/Demo/Demo/View/TestView.h
ViewController.h -> /Users/huangwenchen/Desktop/Demo/Demo/ViewController.h
Tips: 這就是為什么備份/恢復(fù)Mac后,需要clean build folder,因?yàn)閮膳_(tái)mac對(duì)應(yīng)文件的物理位置可能不一樣。
clang發(fā)現(xiàn)#import "TestView.h"的時(shí)候,先在headermap(Demo-generated-files.hmap,Demo-project-headers.hmap)里查找,如果headermap文件找不到,接著在own target的framework里找:
/Users/.../Build/Products/Debug-iphoneos/AFNetworking/AFNetworking.framework/Headers/TestView.h
/Users/.../Build/Products/Debug-iphoneos/SDWebImage/SDWebImage.framework/Headers/TestView.h
系統(tǒng)的頭文件查找的時(shí)候也是優(yōu)先headermap,headermap查找不到會(huì)查找own target framework,最后查找SDK目錄。
以#import 為例,在SDK目錄查找時(shí):
首先查找framework是否存在
$SDKROOT/System/Library/Frameworks/Foundation.framework
如果framework存在,再在headers目錄里查找頭文件是否存在
$SDKROOT/System/Library/Frameworks/Foundation.framework/headers/Foundation.h
Clang Module
傳統(tǒng)的#include/#import都是文本語(yǔ)義:預(yù)處理器在處理的時(shí)候會(huì)把這一行替換成對(duì)應(yīng)頭文件的文本,這種簡(jiǎn)單粗暴替換是有很多問(wèn)題的:
大量的預(yù)處理消耗。假如有N個(gè)頭文件,每個(gè)頭文件又#include了M個(gè)頭文件,那么整個(gè)預(yù)處理的消耗是N*M。
文件導(dǎo)入后,宏定義容易出現(xiàn)問(wèn)題。因?yàn)槭俏谋緦?dǎo)入,并且按照include依次替換,當(dāng)?shù)谝粋€(gè)頭文件定義了#define std hello_world,而第二個(gè)頭文件剛好又是C++標(biāo)準(zhǔn)庫(kù),那么所有的std都會(huì)被替換。
邊界不明顯。拿到一組.a和.h文件,很難確定.h是屬于哪個(gè).a的,需要以什么樣的順序?qū)氩拍苷_編譯。
clang module不再使用文本模型,而是采用更高效的語(yǔ)義模型。clang module提供了一種新的導(dǎo)入方式:@import,module會(huì)被作為一個(gè)獨(dú)立的模塊編譯,并且產(chǎn)生獨(dú)立的緩存,從而大幅度提高預(yù)處理效率,這樣時(shí)間消耗從M*N變成了M+N。
XCode創(chuàng)建的Target是Framework的時(shí)候,默認(rèn)define module會(huì)設(shè)置為YES,從而支持module,當(dāng)然像Foundation等系統(tǒng)的framwork同樣支持module。
#import 的時(shí)候,編譯器會(huì)檢查NSString.h是否在一個(gè)module里,如果是的話,這一行會(huì)被替換成@import Foundation。
那么,如何定義一個(gè)module呢?答案是:modulemap文件,這個(gè)文件描述了一組頭文件如何轉(zhuǎn)換為一個(gè)module,舉個(gè)例子:
framework module Foundation [extern_c] [system] {
umbrella header "Foundation.h" // 所有要暴露的頭文件
export *
module * {
export *
}
explicit module NSDebug { //submodule
header "NSDebug.h"
export *
}
}
swift是可以直接import一個(gè)clang module的,比如你有一些C庫(kù),需要在Swift中使用,就可以用modulemap的方式。
Swift編譯
現(xiàn)代化的語(yǔ)言幾乎都拋棄了頭文件,swift也不例外。問(wèn)題來(lái)了,swift沒(méi)有頭文件又是怎么找到聲明的呢?
編譯器干了這些臟活累活。編譯一個(gè)Swift頭文件,需要解析Target中所有的Swift文件,找到對(duì)應(yīng)的聲明。
當(dāng)開(kāi)發(fā)中難免要有Objective C和Swfit相互調(diào)用的場(chǎng)景,兩種語(yǔ)言在編譯的時(shí)候查找符號(hào)的方式不同,如何一起工作的呢?
Swift引用Objective C:
Swift的編譯器內(nèi)部使用了clang,所以swift可以直接使用clang module,從而支持直接import Objective C編寫(xiě)的framework。
swift編譯器會(huì)從objective c頭文件里查找符號(hào),頭文件的來(lái)源分為兩大類(lèi):
Bridging-Header.h中暴露給swfit的頭文件
framework中公開(kāi)的頭文件,根據(jù)編寫(xiě)的語(yǔ)言不通,可能從modulemap或者umbrella header查找
XCode提供了宏定義NS_SWIFT_NAME來(lái)讓開(kāi)發(fā)者定義Objective C => Swift的符號(hào)映射,可以通過(guò)Related Items -> Generate Interface來(lái)查看轉(zhuǎn)換后的結(jié)果:
Objective引用swift
xcode會(huì)以module為單位,為swift自動(dòng)生成頭文件,供Objective C引用,通常這個(gè)文件命名為ProductName-Swift.h。
swift提供了關(guān)鍵詞@objc來(lái)把類(lèi)型暴露給Objective C和Objective C Runtime。
@objc public class MyClass
深入理解Linker
鏈接器會(huì)把編譯器編譯生成的多個(gè)文件,鏈接成一個(gè)可執(zhí)行文件。鏈接并不會(huì)產(chǎn)生新的代碼,只是在現(xiàn)有代碼的基礎(chǔ)上做移動(dòng)和補(bǔ)丁。
鏈接器的輸入可能是以下幾種文件:
object file(.o),單個(gè)源文件的編輯結(jié)果,包含了由符號(hào)表示的代碼和數(shù)據(jù)。
動(dòng)態(tài)庫(kù)(.dylib),mach o類(lèi)型的可執(zhí)行文件,鏈接的時(shí)候只會(huì)綁定符號(hào),動(dòng)態(tài)庫(kù)會(huì)被拷貝到app里,運(yùn)行時(shí)加載
靜態(tài)庫(kù)(.a),由ar命令打包的一組.o文件,鏈接的時(shí)候會(huì)把具體的代碼拷貝到最后的mach-o
tbd,只包含符號(hào)的庫(kù)文件
這里我們提到了一個(gè)概念:符號(hào)(Symbols),那么符號(hào)是什么呢?
符號(hào)是一段代碼或者數(shù)據(jù)的名稱(chēng),一個(gè)符號(hào)內(nèi)部也有可能引用另一個(gè)符號(hào)。
以一段代碼為例,看看鏈接時(shí)究竟發(fā)生了什么?
源代碼:
- (void)log{
printf("hello world\n");
}
.o文件:
#代碼
adrp x0, l_.str@PAGE
add x0, x0, l_.str@PAGEOFF
bl _printf
#字符串符號(hào)
l_.str: ; @.str
.asciz "hello world\n"
在.o文件中,字符串"hello world\n"作為一個(gè)符號(hào)來(lái)引用,匯編代碼讀取的時(shí)候按照l(shuí)_.str所在的頁(yè)加上偏移量的方式讀取,然后調(diào)用printf符號(hào)。到這一步,CPU還不知道怎么執(zhí)行,因?yàn)檫€有兩個(gè)問(wèn)題沒(méi)解決:
l_.str在可執(zhí)行文件的哪個(gè)位置?
printf函數(shù)來(lái)自哪里?
再來(lái)看看鏈接之后的mach o文件:
鏈接器如何解決這兩個(gè)問(wèn)題呢?
鏈接后,不再是以頁(yè)+偏移量的方式讀取字符串,而是直接讀虛擬內(nèi)存中的抵制,解決了l_.str的位置問(wèn)題。
鏈接后,不再是調(diào)用符號(hào)_printf,而是在DATA段上創(chuàng)建了一個(gè)函數(shù)指針_printf$ptr,值為0x0,代碼直接調(diào)用這個(gè)函數(shù)指針。啟動(dòng)的時(shí)候,dyld會(huì)把DATA段上的指針,進(jìn)行動(dòng)態(tài)綁定,綁定到具體虛擬內(nèi)存中的_printf地址。更多細(xì)節(jié),可以參考我之前的這篇文章:深入理解iOS App的啟動(dòng)過(guò)程。
Tips: Mach-O有一個(gè)區(qū)域叫做LINKEDIT,這個(gè)區(qū)域用來(lái)存儲(chǔ)啟動(dòng)的時(shí)候,dyld需要?jiǎng)討B(tài)修復(fù)的一些數(shù)據(jù):比如剛剛提到的printf在內(nèi)存中的地址。
理解簽名
基礎(chǔ)回顧
非對(duì)稱(chēng)加密。在密碼學(xué)中,非對(duì)稱(chēng)加密需要兩個(gè)密鑰:公鑰和私鑰。私鑰加密的只能用公鑰解密,公鑰加密的只能用私鑰解密。
數(shù)字簽名。數(shù)字簽名表示我對(duì)數(shù)據(jù)做了個(gè)標(biāo)記,表示這是我的數(shù)據(jù),沒(méi)有經(jīng)過(guò)篡改。
數(shù)據(jù)發(fā)送方Leo產(chǎn)生一對(duì)公私鑰,私鑰自己保存,公鑰發(fā)給接收方Lina。Leo用摘要算法,對(duì)發(fā)送的數(shù)據(jù)生成一段摘要,摘要算法保證了只要數(shù)據(jù)修改,那么摘要一定改變。然后,用私鑰對(duì)這個(gè)摘要進(jìn)行加密,和數(shù)據(jù)一起發(fā)送給Lina。
Lina收到數(shù)據(jù)后,用公鑰解密簽名,得到Leo發(fā)過(guò)來(lái)的摘要;然后自己按照同樣的摘要算法計(jì)算摘要,如果計(jì)算的結(jié)果和Leo的一樣,說(shuō)明數(shù)據(jù)沒(méi)有被篡改過(guò)。
但是,現(xiàn)在還有個(gè)問(wèn)題:Lina有一個(gè)公鑰,假如攻擊者把Lina的公鑰替換成自己的公鑰,那么攻擊者就可以偽裝成Leo進(jìn)行通信,所以Lina需要確保這個(gè)公鑰來(lái)自于Leo,可以通過(guò)數(shù)字證書(shū)來(lái)解決這個(gè)問(wèn)題。
數(shù)字證書(shū)由CA(Certificate Authority)頒發(fā),以Leo的證書(shū)為例,里面包含了以下數(shù)據(jù):簽發(fā)者;Leo的公鑰;Leo使用的Hash算法;證書(shū)的數(shù)字簽名;到期時(shí)間等,這些數(shù)據(jù)用CA的公鑰進(jìn)行加密。
有了數(shù)字證書(shū)后,Leo再發(fā)送數(shù)據(jù)的時(shí)候,把自己從CA申請(qǐng)的證書(shū)一起發(fā)送給Lina。Lina收到數(shù)據(jù)后,先驗(yàn)證證書(shū)的數(shù)字簽名是否正確,如果正確說(shuō)明證書(shū)沒(méi)有被篡改過(guò),然后用CA的公鑰解密證書(shū)數(shù)據(jù),根據(jù)證書(shū)的信息就可以確認(rèn)這是Leo的證書(shū),對(duì)應(yīng)的公鑰也是Leo的。
iOS App簽名
為什么要對(duì)App進(jìn)行簽名呢?簽名能夠讓iOS識(shí)別出是誰(shuí)對(duì)簽名了App,并且簽名后,App沒(méi)有被修改過(guò),
另外Apple要嚴(yán)格控制App的分發(fā):
App來(lái)自Apple信任的開(kāi)發(fā)者
安裝的設(shè)備是Apple允許的設(shè)備
App在簽名后安裝包沒(méi)有被篡改過(guò)
證書(shū)
通過(guò)上文的講解,我們知道數(shù)字證書(shū)里包含著申請(qǐng)證書(shū)設(shè)備的公鑰,所以在Apple開(kāi)發(fā)者后臺(tái)創(chuàng)建證書(shū)的時(shí)候,需要上傳CSR文件(Certificate Signing Request),用keychain生成這個(gè)文件的時(shí)候,就生成了一對(duì)公/私鑰:公鑰在CSR里,私鑰在本地的Mac上。Apple本身也有一對(duì)公鑰和私鑰:私鑰保存在Apple后臺(tái),公鑰在每一臺(tái)iOS設(shè)備上。
Provisioning Profile
iOS App安裝到設(shè)備的途徑(非越獄)有以下幾種:
開(kāi)發(fā)包(插線,或者archive導(dǎo)出develop包)
Ad Hoc
App Store
企業(yè)證書(shū)
開(kāi)發(fā)包和Ad Hoc都會(huì)嚴(yán)格限制安裝設(shè)備,為了把設(shè)備uuid等信息一起打包進(jìn)App,開(kāi)發(fā)者需要配置Provisioning Profile。
可以通過(guò)以下命令來(lái)查看Provisioning Profile中的內(nèi)容:
security cms -D -i embedded.mobileprovision > result.plist
open result.plist
本質(zhì)上就是一個(gè)編碼過(guò)后的plist
iOS簽名
生成安裝包的最后一步,XCode會(huì)調(diào)用codesign對(duì)Product.app進(jìn)行簽名。
創(chuàng)建一個(gè)額外的目錄_CodeSignature以plist的方式存放安裝包內(nèi)每一個(gè)文件簽名
Base.lproj/LaunchScreen.storyboardc/01J-lp-oVM-view-Ze5-6b-2t3.nib
T2g5jlq7EVFHNzL/ip3fSoXKoOI=
Info.plist
5aVg/3m4y30m+GSB8LkZNNU3mug=
PkgInfo
n57qDP4tZfLD1rCS43W0B4LQjzE=
embedded.mobileprovision
tm/I1g+0u2Cx9qrPJeC0zgyuVUE=
...
代碼簽名會(huì)直接寫(xiě)入到mach-o的可執(zhí)行文件里,值得注意的是簽名是以頁(yè)(Page)為單位的,而不是整個(gè)文件簽名:
驗(yàn)證
在安裝App的時(shí)候,
從embedded.mobileprovision取出證書(shū),驗(yàn)證證書(shū)是否來(lái)自Apple信任的開(kāi)發(fā)者
證書(shū)驗(yàn)證通過(guò)后,從證書(shū)中取出Leo的公鑰
讀取_CodeSignature中的簽名結(jié)果,用Leo的公鑰驗(yàn)證每個(gè)文件的簽名是否正確
文件embedded.mobileprovision驗(yàn)證通過(guò)后,讀取里面的設(shè)備id列表,判斷當(dāng)前設(shè)備是否可安裝(App Store和企業(yè)證書(shū)不做這步驗(yàn)證)
驗(yàn)證通過(guò)后,安裝App
啟動(dòng)App的時(shí)候:
驗(yàn)證bundle id,entitlements和embedded.mobileprovision中的AppId,entitlements是否一致
判斷device id包含在embedded.mobileprovision里
App Store和企業(yè)證書(shū)不做驗(yàn)證
如果是企業(yè)證書(shū),驗(yàn)證用戶(hù)是否信任企業(yè)證書(shū)
App啟動(dòng)后,當(dāng)缺頁(yè)中斷(page fault)發(fā)生的時(shí)候,系統(tǒng)會(huì)把對(duì)應(yīng)的mach-o頁(yè)讀取物理內(nèi)存,然后驗(yàn)證這個(gè)page的簽名是否正確。
以上都驗(yàn)證通過(guò),App才能正常啟動(dòng)
小結(jié)
如有內(nèi)容錯(cuò)誤,歡迎issue指正。
總結(jié)
以上是生活随笔為你收集整理的ios .mm文件调用c语言函数报错,深入浅出 iOS 编译的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: centos :不在 sudoers 文
- 下一篇: 你需要了解的opn模块