前言
上篇文章多環境配置
、Mach-O與鏈接器
,但是Symbol
還沒又說道,這篇文章我們繼續上篇文章內容講下去
.xconnfig補充
上面文章在介紹多環境配置的時候講到了.xconnfig
,說到了.xconnfig
可以統一管理環境配置
,這里可以根據不同的條件
配置不同的設置
,我們那Other Linker Flags
來說明
上圖配置意思就是在
Debug環境下,設備為模擬器,切架構為x86時
添加framework "Man"
。
我們看到此時在
arm64
下編譯時成功
的,因為Other Linker Flags
并沒有導入Man
這次我們看到在
x86_64環境
下,編譯時發現報錯,告訴我們找不到Man,這是因為這種環境
下我們的Man被放入了項目環境
中,所以才會提示找不到
作為一個開發者,有一個學習的氛圍跟一個交流圈子特別重要,這是一個我的iOS開發交流群:130 595 548,不管你是小白還是大牛都歡迎入駐 ,讓我們一起進步,共同發展!(群內會免費提供一些群主收藏的免費學習書籍資料以及整理好的幾百道面試題和答案文檔!)
Mach-O再講
上篇文章只是粗略的講了Mach-O,這里再補充一下
Mach-O結構
Mach-O的結構圖如下:解釋如下:
- 1.
Mach Header
告訴執行者自己包含
哪些信息
(也就是這個Mach-O的身份信息) - 2.
Load Command
就是配置文件
,最后三
個才是我們的代碼編譯后的文件位置
- 3.
配置文件
都記錄
一些必要的文件信息
,以Load Command _TEXT為例,它里面記錄下面內容:text代碼段的大小
text代碼段的起始位置
- 記錄其它
必要信息
,比如:UUID標識符
,Version版本
,Dylinker連接器位置
,Linkdit動態庫信息
,Dylib引入哪些庫
,指定入口為Main函數
(你可以不制定Mian函數作為入口)
【注意】:每次讀都能保證讀完一個Load Command
,這是因為這些信息的排列
是按照結構體對齊
的方式進行存儲排列
的,所以按著約定好的字節數,就能正好讀完
Mach Header
Mach Header主要結構如下:解釋幾個主要的:
cputype:架構
filetype:是可執行文件還是目標文件
sizeofcmds:大小
__TEXT
我們通過命令來看下main.m在x86下編譯成text情況-
最左邊
的是地址
,我們看到main的起始地址為100003f20,結束地址為100003f5e。 -
中間
的是機器碼
,給機器讀
的 -
右邊
是匯編
,給開發者讀
的
這個有點像查字典,提前約定好匯編
和機器碼
的對應關系
,當讀機器碼55
時,就對應
著匯編
:pushq %rbp
,以此類推
Mach-O特性
上篇文章講了Mach-O
是可讀
,可寫
的。可讀我們已經說了,可寫是什么意思?Mach-O
之所以能被執行
是因為有簽名
,當我們修改
了Mach-O文件
,需要重新簽名
才能被蘋果系統所接受
。這也是為什么破解軟解都需要重新簽名的原因。
鏈接器
生成目標文件過程
- 1.
鏈接器(llvm-ld)并沒有被執行
- 2.
目標文件不會包含Unix程序在被裝載和執行時所必須的包含信息
上面不是很好理解,我們直接通過代碼來解釋
代碼講解
我們在main.m文件中寫如下代碼:我們看到.m中有定義的屬性了,我們再看看此時編譯為__TEXT
是什么樣
我們和上面的相比較發現多了很多東西例如:NSLog
此時變成了一個指令callq地址0x100003f60
也就是說在編譯的時候:
- 1.把
能變成匯編
的先變成匯編
,機器碼
。 - 2.把
屬性
轉成符號
進行歸類
->放入重定位符號表
(重定位符號表就是放.m/.o用到的API) - 3.
.o
->鏈接器
->一張表
->可執行文件exec
之所以要放入重定位符號表,是因為已經放入符號表
中,在生成.o文件時,其地址
還未虛擬化
,鏈接器
在進行連接
的時候,會對重定位符號表
進行合并
。
通過上面我們可知鏈接
就是處理目標文件符號的過程
指令查看重定位符號表
命令:objdump --macho --reloc +.o文件
,運行后,重定位符號表打印如下:
未用到
的將不會放到.o文件
,通過這個特性,我們可以通過查看.o文件
來查看文件
對某種API的使用情況
符號(Symbol)
我們通過指令查看下main.m的符號表l:local布局的意思
g:global全局的意思
d:Debug的意思
o:Data的意思
F:Function的意思
我們發現上面符號表有很多Debug模式
下的輸出,下面我們用命令將這部分去掉。我們可以通過strip命令
也可以通過鏈接器參數-S
就是
鏈接
的時
候不把調試符號放到
最終生成
的可執行文件
中。
-
調試符號
:當我們的文件通過匯編器
會生成
一個DWARF格式
的調試文件
,它會被放在Mach-O
的__DWARF段
中,在連接
的時候會把__DWARF段干掉
,同時
把__DWARF段變成符號
,放到符號表
中。
通過上面兩個圖可以知道
全局變量
都是g(全局符號)
,而本地變量
都是l(局部符號)
,而將全局符號變為本地符號
:1.加static
2.使用__attribute__
關鍵字(第16行)
導入導出符號
我們知道
NSLog是Foundation框架
下的,寫在19行,相當于是導入
了NSLog符號
,又因為Foundation
又導出了NSLog符號
,讓其它地方使用(導出符號又是全局符號
)
下面我們看下main.m中有哪些導出符號,通過在.xcconfig中寫入命令
,編譯
下面我們創建一個OC類,再查看符號我們看到有
4個導出符號
,它正好對應上面打印符號表
中的4個全局符號
,這也就意味著當我們聲明全局符號
時,會默認為導出符號
,其它地方也可以使用
OC類都會默認為導出符號
間接符號表
我們知道動態庫
是在運行
的過程中加載
,也就意味著它在編譯鏈接階段
只需要提供符號
就可以了,上篇文章我們在說符號表
時提到:間接符號表
保存這項目使用的其它動態庫
的符號
,下面我們通過在.xcconfig
中寫入命令,編譯來查看間接符號表
這里面我們就只認識最后的
NSLog
,這個是Foundation給我提供的導出符號
總結
- 1.
全局符號可以變成導出符號給外界使用
- 2.
間接符號表不能刪除
,意味著動態庫
中的全局符號不能刪除
,也就說明在strip動態庫
時,不能strip全局符號
- 3.
OC類
在編譯時
都會默認
為導出符號
,那么我們在用OC寫動態庫
時,如果想盡可能讓動態庫包小些
,我們可以在.xcconfig定義參數不導出符號
-
進行編譯
-
和上面的相比發現少了一個
_OBJC_CLASS_$_LjOneObject
,相同的方法可以讓_OBJC_METACLASS_$_LjOneObject
也消失
-
補充
上面總結說了可以通過不導出符號來使動態庫體積減小
,但是如果我們要寫的類太多了怎么辦,其實給了有方法:
- 1.
可以執行文件
- 2.1中的文件的獲得可以
通過查看當前文件使用類庫的信息
紅框內是告訴開發者,生成了
幾個目標文件
,項目使用了哪些庫文件
。通過map可以導出符號信息,鏈接信息
Weak Symbol
Weak Symbol具體分一下兩種
1.
Weak Reference Symbol
:表示此未定義符號
是弱引用
。如果動態鏈接器找不到該符號
的定義
,則將其設置為0
。鏈接器
會將此符號設置弱鏈接標志
。2.
Weak defintion Symbol
:表示此符號為弱定義符號
。如果靜態鏈接器
或動態鏈接器
為此符號找到另一個(非弱)定義
,則弱定義將被忽略
。只能將合并部分
中的符號標記為弱定義
。
Weak Reference Symbol
Weak Reference Symbol(弱引用)寫法:我們通過代碼來說明一些問題,在main函數寫如下代碼:上面的解釋:也就是如果若引入符號未被定義(不想要實現),系統不會報錯
但是這么寫會報錯,原因:在說編譯鏈接原理
的時候說過,符號怎么來查找的呢?我們在WeakImportSymbol.h寫了聲明
,在main函數中使用
,就是用的API
,但是在連接的時候
,我們需要知道符號具體的地址
在什么地方,否則提示找不到
我們可以告訴編譯器
,我這個符號時動態鏈接
的,不要管
它的具體位置
即使它是弱引用的,到時候dyld運行
起來,自己會查找
的
-U參數就是告訴編譯器這個沒有定義,需要動態查找
再次運行就會成功了,那么這個有什么用處呢?比如:我們可以判斷
其它庫里
,有沒有這個符號
,有
這個符號我就調用
,沒有
這個符號我就不調用
。還有個用途就是在動態庫
上,我們可以將整個動態庫文件聲明成一個弱引用
,這個有什么好處呢?也就意味著如果你這個庫沒有導入
的話,也不會報動態庫找不到的錯誤
。
Weak defintion Symbol
Weak defintion Symbol(弱定義)寫法:上面講到弱定義符號:如果靜態鏈接器或動態鏈接器為此符號找到另一個(非弱)定義,則弱定義將被忽略
,怎么理解這句話呢?我們通過代碼來理解
- 1.在.h中我們
弱定義了weak_function方法
- 在.m中我們實現這個
弱定義方法
方法實現
和聲明都是全局
的,上面講了應該轉為導出符號
,下面我們變一下,看下打印
當我們在.m聲明相同的方法當聲明為
弱定義方法
,并不影響
作為導出符號導出
下面我們在main函數中調用這個方法如果
正常情況
下,由于方法名相同
,運行應該會報錯
,但是由于這個方法被弱定義
,此時編譯是不會報錯
的。
如果我們把弱定義的符號聲明成一個隱藏符號,此時它應該是一個弱定義的本地符號我們看到執行了
main函數的weak_function方法
,并沒有執行WeakSymbol的weak_function
,這也就是上面說的:如果靜態鏈接器或動態鏈接器為此符號找到另一個(非弱)定義,則弱定義將被忽略
重新導出符號
當我們NSLog
在main函數
中使用
,當我想讓其它項目使用
這個main.m
時,也能夠使用NSLog
,這就需要我們對NSLog進行重新導出
(舉的事NSLog,其實在Foundation
中已經對NSLog
做了重新導出
,否則外界是無法使用的
)
當我們重新導出NSLog
,需要對間接符號的符號起別名
它會自動的將這個
NSLog
變成導出符號Lj_NSLog
,編譯
我們發現存在了
Lj_NSLog
,但是這種形式不夠友好,所以我們需要換種打印方式,寫入命令
我們看到這個
Lj_NSLog
變成了NSLog的別名
,我們再看下導出符號表符號
可以看到
Lj_NSLog是被導出
了,而且是重新導出的一個符號
作用:在我們的動態庫中鏈接
另一個動態庫
的時候,其中一個動態庫
對你鏈接
的程序
是不可見
的,我們就可以用這種重新導出
方式讓這個動態庫可見
,可以讓一個符號可見
,也可以讓一個動態庫可見
總結
通過上面的符號可以知道一下幾點:
- 1.間接符號表中的符號不能刪除,意味著
動態庫
中的全局符號不能刪除
,也就說明在strip動態庫
時,不能strip全局符號
- 2.
靜態庫
是.o文件合計
以及重定位符號表
,由于重定位符號不能刪除
,所以只能strip.0文件中的調試符號
【問題】App加入動態庫體積和加入靜態庫體積誰的更大(只考慮符號)
答案:動態庫的體積更大
原因:
- 【靜態庫】
App在鏈接靜態庫
時,會將.o文件
以及重定位符號表
放到App的符號表
中,也就意味著它變成
了可能
是本地、全局、導出符號
,根據我們上面說的脫離符號表規則
,除了間接符號表中的符號
,其它都可以脫 - 【動態庫】
App在鏈接動態庫
時,符號都
放到間接符號表
中,導致在脫離符號表
時無法脫離間接符號表
拓展Strip Style(符號脫離)
- 1.
Debugging Symbols
(.o 靜態庫 / 可執行文件 動態庫) - 2.
All Symbols
- 3.
Non-Global Symbols
Strip Style過程
靜態庫
動態庫
All Symbols
Non-Global Symbols
寫到最后
文章寫的有些東西沒有細說,后面會介紹的!希望大家能夠多多交流,共同進步,最后貼出來上面說的指令: