Swift是蘋果新的編程語言,很多人都認爲它可以“替換”Objective-C,但事實卻並非如此。我花了一些時間對Swift運行時和二進制文件進行了逆向工程,不過(不過錶轉折,可去掉)發現了很多有趣的事。總結起來就是:Swift是沒有消息機制的Objective-C。
面向對象
不管你信不信,Swift對象實際上是Objective-C對象。在Mach-O二進制文件中可以發現__objc_classlist部分包含了每個類的二進制數據,結構類似:
- struct objc_class {
- uint64_t isa;
- uint64_t superclass;
- uint64_t cache;
- uint64_t vtable;
- uint64_t data;
- };
(注:所有的結構都是在64位機器上編譯)
注意數據入口,它指向一個列出了類的方法、實例變量以及協議等等的結構。通常情況下,數據是8個字節對齊的。然而,在Swift的類中,最後一個數據將只佔有1個字節。
類
Swift類的實際結構有點奇怪。Swift類沒有類似於Objective-C的方法,這點會在後面說明。Swift類中變量被存儲爲一個實例變量。在Swift中利用getter和setter方法修改這個實例變量的值(譯者注:此處的getter和setter並不實際存在,只是抽象爲實例變量的訪問和修改入口,是一種規範,在下文中不對getter和setter進行特別翻譯)。奇怪的是,Swift類的實例變量其實是沒有類型編碼。通常應該指向類型編碼的指針爲NULL,這想必是因爲Objective-C的運行時不會親自去處理Swift變量。
繼承
正如你所期望的那樣,Swift同樣擁有類的繼承與派生。比如,在Swift中,Shape(形狀)的子類Square(方形)在Objective-C類中同樣是Shape的子類。然而,如果Swift中的類沒有父類會怎樣?
例如:
- class Shape { }
在這種情況下,Shape類是一個SwiftObject的子類。而類swiftobject是一個類似於Objective-C中NSObject這樣的一個基類。它沒有父類,這意味着isa變量指向其本身。其目的是利用Swift運行時方法處理像分配和釋放內存這樣的事,而不是標準的Objective-C運行時。比如說,改爲- (void)retain不是調用objc_retain,而是調用swift_retain
類方法
就像我前面提到的,Swift對象的類沒有方法。相反,他們使用像C++一樣的函數、重載以及其他替代。這可能是爲什麼Swift要比Objective-C快得多;這裏不怎麼需要用objc_msgsend來尋找和調用方法的實現。
在Objective-C中,方法實現類似這樣:
type method(id self, SEL _cmd, id arg1, id arg2, ...)
Swift方法是非常相似的,但稍有不同的是參數佈局,self只能作爲最後一個參數傳遞,這裏沒有選擇器。
type method(id arg1, id arg2, ..., id self)
虛函數表
就像在C++一樣,Swift的類有一個虛函數表,在虛表裏列出了類中的方法。它位於二進制的數據後面,看起來像這樣:
- struct swift_vtable_header {
- uint32_t vtable_size;
- uint32_t unknown_000;
- uint32_t unknown_001;
- uint32_t unknown_002;
- void* nominalTypeDescriptor;
- // vtable pointers
- }
這裏需要說明的是,Swift類的虛函數表只能當它在編譯時可見的情況下使用。否則將會報錯。
Name Mangling(命名重整)
Swift在各自的標誌中保存元數據的函數(以及更多),這就是所謂的name mangling。該元數據包括函數名,屬性、模塊名稱,參數類型,返回類型等等。
- class Shape{
- func numberOfSides() -> Int {
- return 5
- }
- }
以此爲例:Simpledescriptio方法改編(重載)的名字是_tfc9swifttest5shape17simpledescriptionfs0_ft_si。這裏分解下:
t–所有Swift符號的前綴。所有事情皆始於次。
F–函數。
C一類的函數。(方法)
9swifttest–帶有一個前綴長度的模塊名稱。
5shape–函數所屬類的名稱,帶有一個前綴長度。
17simpledescription–函數名。
f–函數屬性。在本例中,它是“f”,這是一個常見的函數。
s0_ft–我不確定這意味着什麼,但它似乎是標記參數的開始,並返回類型。
“_”該下劃線把參數類型和返回類型分別開來。由於函數沒有參數,所以它直接跟在S0_FT後邊。
S--這是返回類型的開始。“S”代表Swift;返回類型是一個Swift內建類型。下一個字符決定類型。
i–這是Swift的內建類型。一個小寫的“i”,可代替int (小寫的“I”代表Int)。
函數屬性
Swift的內置命令
命名重整並不僅僅只用於函數,但我只是給出一個簡短的概述。
函數連接
這句子有足夠的語義,讓我們來到一個有趣的部分!如果我們有這樣的一個類:
- class Shape {
- var numberOfSides: Int;
- init(){
- numberOfSides = 5;
- }
- }
我們說我們想把numberofsides 改爲4。有多種方式來做這個。我們可以使用MobileSubstrate連接到getter方法,並改變返回值,像這樣:
- int (*numberOfSides)(id self);
- MSHook(int, numberOfSides, id self){
- return 4;
- }
- %ctor{
- numberOfSides = (int (*)(id self)) dlsym(RTLD_DEFAULT, "_TFC9swifttest5Shapeg13numberOfSidesSi");
- MSHookFunction(numberOfSides, MSHake(numberOfSides));
- }
如果我們創建一個Shape實例,並打印出變量numberofsides的值,我們看到結果爲4!這並不是很壞,是吧?現在,我知道你在想什麼;“難道不是應該返回一個對象,而不是字符“4”嗎?”
的確,在Swift中,大量的內建類型都是文本。例如Int,與C中的Int一樣。注意,字符串類型有點奇怪,這是一個little-endian UTF-16字符串,所以沒有可以使用的C字面量。
讓我們做同樣的事情,但是這一次,我們會連接setter代替getter。
- void (*setNumberOfSides)(int newNumber, id self);
- MSHook(void, setNumberOfSides, int newNumber, id self){
- _setNumberOfSides(4, self);
- }
- %ctor {
- setNumberOfSides = (void (*)(int newNumber, id self)) dlsym(RTLD_DEFAULT, "_TFC9swifttest5Shapes13numberOfSidesSi");
- MSHookFunction(setNumberOfSides, MSHake(setNumberOfSides));
- }
再試試跑一次程序吧…結果仍然是5。你一定會問發生什麼事了?這是因爲,在Swift的某些地方函數是內聯的。該類的構造函數是一個這樣的函數。它直接設置numberofsides ivar。所以,只有數值在頂部的代碼中被再次設置時纔會調用setter方法。執行調用後,我們會的到4這個值。
最後,讓我們通過直接設置變量實例改變numberofsides的值。
- void (*setNumberOfSides)(int newNumber, id self);
- MSHook(void, setNumberOfSides, int newNumber, id self){
- MSHookIvar<int>(self, "numberOfSides") = 4;
- }
- %ctor {
- setNumberOfSides = (void (*)(int newNumber, id self)) dlsym(RTLD_DEFAULT, "_TFC9swifttest5Shapes13numberOfSidesSi");
- MSHookFunction(setNumberOfSides, MSHake(setNumberOfSides));
- }
這樣同樣可以達到效果。雖然並不推薦這樣做,但它仍是可行的解決方案。
這就是我要寫的所有東西。雖然有很多其他的東西,但我不知道是否有足夠精力去寫。在這篇文章中許多東西是會變化的。它們只是我使用Swift從運行時和二進制文件逆向工程得到的。但我覺得這還不錯。這意味着,MobileSubstrate不會隨着Objective-C消亡,仍有調整的空間。