为什么objc_msgSend必须用汇编实现
譯者前言
?
總是看到有人說用匯編實現objc_msgSend是為了速度快,當然這個不可否認。但是難道沒有別的原因?于是就看到了這篇文章,遂翻譯之!=。=
?
我自己的理解就是,用匯編實現,是為了應對不同的“Calling convention”,把函數調用前的棧和寄存器的參數、狀態設置,交給編譯器去處理。
?
先看看原文吧。
?
原作者: Ari Grant
原文鏈接: Why objc_msgSend Must be Written in Assembly
http://arigrant.com/blog/2014/2/12/why-objcmsgsend-must-be-written-in-assembly
?
開始
?
對于Objective-C來說,調用一個對象實例的方法,也叫作向這個對象實例“發送消息”,而每條“消息”,在編譯階段都會轉變為一次對objc_msgSend函數的調用,調用的參數不僅有原本消息的所有參數,還有消息的接收者receiver和對應的方法selector。舉個例子,下面的語句:
?
[receiver message:foo beforeDate:bar];
?
將會被編譯成:
?
objc_msgSend(receiver, @selector(message:beforeDate:), foo, bar);
?
對于objc_msgSend函數的實現原理,前人已經做了大量的探索。所以,本文將會把重點放在objc_msgSend的一個之前沒有太受到關注的點上,那就是:
?
objc_msgSend是不可能用Objective-C、C或者C++實現的。
?
THE RETURN TYPE – 返回類型
?
先看看如下兩行代碼:
?
NSUInteger n = [array count];
id obj = [array objectAtIndex:6];
?
直觀上看,將會被編譯成
?
NSUInteger n = objc_msgSend(array,??@selector(count));
id obj = objc_msgSend(array, @selector(objectAtIndex:), 6);
?
但是實際上這是不可能的,因為沒有函數可以同時滿足這兩個調用。而且它的返回值也不能同時是NSUInteger和id。
?
而且,上面的代碼也是無法編譯通過的。那么,加上類型轉換怎么樣?
?
NSUInteger n = (NSUInteger (*)(id, SEL))objc_msgSend(array,??@selector(count));
id obj = (id (*)(id, SEL, NSUInteger))objc_msgSend(array, @selector(objectAtIndex:), 6);
?
這下可以編譯通過了,雖然看起來不直觀。。。
objc_msgSend是一個Public的函數,在里聲明,如果你想直接調用它,就必須按照上面的格式加上強制類型轉換,要不然是無法編譯通過的。但是objc_msgSend到底是如何實現,來支持各種返回類型的?本文后面會講到。
?
THE IMP – 方法對應的函數指針
?
objc_msgSend函數的本質很簡單,傳入一個接受者對象實例receiver和方法名selector,它就會按照以下步驟執行:(譯者注:只是最粗略的步驟=。=)
?
-
獲取receiver得類Class
-
在Class的方法列表method table里面查找對應selector的方法實現
-
找到的話就調用,返回
-
找不到就在其父類中找,重復前面的步驟(直到沒有父類為止)
?
整個流程很簡單,沿著繼承鏈,向上找到方法selector對應的函數指針即可,也就是IMP。同時,在每層Class中都有緩存,加快后續的方法查找。但是,這也只是objc_msgSend的實現細節,所以,接著往下看。
?
THE ARG TYPES AND COUNT – 參數類型和數量
?
簡單來說,當objc_msgSend找到對應的函數指針后,只要用傳入的參數調用這個函數即可。剩下來的就是找到一種方法,可以調用任意參數類型、數量的任意函數。
?
參數的數量很容易計算。然后我們可以把所有的參數都放入varargs,然后調用函數時傳入即可。但是這樣的話,每個Objective-C的方法都必須在其prologue(譯者注:函數執行具體的“任務”前,所做的準備環節)里面把所有的參數從varargs里面提取出來。
?
這種把參數打包到varargs里面然后又取出來的辦法顯然是非常糟糕的,同時也是不必要的。
?
在C語言中,調用一個函數會被編譯成對應的匯編語言指令,首先是設置參數(把參數放到寄存器、棧上),然后用如jump或者call的指令,跳到具體的函數代碼地址處。如果我們想支持任意類型的函數類型,我們就必須寫一個switch語句,把所有的參數組合情況都包含起來,這樣才能正確的為任何形式的函數設置參數(譯者注:即按照某種“規范”、“約定”,把參數依次存放到“約定”的寄存器、棧上),這顯然是沒有擴展性的,更是不可能的。
?
UNWINDING THE CALL – 拆解調用
?
objc_msgSend的解決辦法,主要依據的是:當objc_msgSend被調用時,所有的參數已經被設置好了。
?
換一種方式來說,就是:在objc_msgSend開始執行時,棧幀(stack frame)的狀態、數據,和各個寄存器的組合形式、數據,跟調用具體的函數指針(IMP)時所需的狀態、數據,是完全一致的!
?
如下這行代碼:
?
id obj = objc_msgSend(array, @selector(objectAtIndex:), 6);
?
在調用objc_msgSend時,需要設置三個參數,分別是被調用方receiver、方法名selector和最后一個整型參數6。這和具體的方法函數IMP的參數順序、類型是完全一致的,也就是說,調用objc_msgSend前,設置的棧、寄存器的狀態、數據正是調用具體的方法函數時需要的狀態!
?
所以,當objc_msgSend找到要調用的函數實現IMP后,只需要把所有的對棧、寄存器的操作“倒”回到objc_msgSend執行開始的狀態(類似于函數執行完成return返回前,做的“收尾處理”工作一樣,即epilogue),直接jump/call到IMP函數指針對應的地址,執行指令即可,因為所有的參數已經被設置好了。
?
同時,當selector對應的IMP執行完成后,返回值也被正確的設置好了(在x86平臺上,返回值被設置到了指定的寄存器eax/rax里,在arm上,則是r0寄存器),所以,我們也不必擔心前文提到的不同類型的返回值問題了。
?
WRAP UP – 總結
?
把上面提到的所有解釋綜合起來,就是:在C語言里面調用函數,必須在編譯時就知道調用的“狀態”;而這些“狀態”在運行時是無法得出或正確處理的,所以必須往底層走,用匯編處理。(譯者注:這里不知道咋翻譯好=。=,原文是:calling a function in C requires the signature to be known for each call-site at compile-time;doing so at run-time is not possible and so one must drop down into assembly and party there instead.)
?
UPDATE – 后續
?
有人指出objc_msgSend有可能是用GCC的擴展方法__builtin_apply_args,__builtin_apply,和__builtin_return實現的。這也正指出了一個事實,就是這些builtins方法是非常有必要的,因為單靠語言本身無法實現這些功能。實現objc_msgSend所需要的技巧,也正是實現這些builtins方法所需要的技巧。本文的目的并不是非要將什么是真正的C、什么不是真正的C分個清楚,只是為了指出objc_msgSend特殊罷了。
?
譯者總結
?
開頭也說了,我的理解是:用匯編實現,是為了應對不同的“Calling convention”,把函數調用前的棧和寄存器的參數、狀態設置,交給編譯器去處理。
?
嗯,以后不要再說用匯編實現只是為了快了=。=
轉載于:https://www.cnblogs.com/fengmin/p/5619115.html
總結
以上是生活随笔為你收集整理的为什么objc_msgSend必须用汇编实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux板级初始化
- 下一篇: linux嵌入式贪吃蛇