你真的會判斷 _objc_msgForward_stret 嗎

前言

本文須要對消息轉發機制有了解,建議閱讀 Objective-C 消息發送與轉發機制原理html

恰巧在 8 月學習 Method Swizzling ,閱讀了 Aspects 和 JSPatch 作方法替換的處理,注意到了咱們此次介紹的主角 --_objc_msgForward_stret.linux

JSPatch 目前的 Star 數已經破萬,知名度可見一斑。面試時,也會常常被說起。Aspects也是一個在 AOP 方面很是著名的庫。git

在消息轉發時,咱們根據方法返回值的類型,來決定 IMP 使用 _objc_msgForward 或者 _objc_msgForward_stret.github

根據蘋果的文檔描述,使用 _objc_msgForward_stret 的確定是一個結構體:面試

Sends a message with a data-structure return value to an instance of a class.bash

然而,不一樣 CPU 架構下,判斷 _objc_msgForward_stret 的規則也有差別。下面就來看看兩個著名開源庫的作法。架構

JSPatch 的判斷

首先咱們來看 JSPatch , 在 JPEngine.m 文件裏的 overrideMethod 方法,是如何去判斷是否使用 _objc_msgForward_stret:app

IMP msgForwardIMP = _objc_msgForward;
#if !defined(__arm64__)
    if (typeDescription[0] == '{') {
        //In some cases that returns struct, we should use the '_stret' API:
        //http://sealiesoftware.com/blog/archive/2008/10/30/objc_explain_objc_msgSend_stret.html
        //NSMethodSignature knows the detail but has no API to return, we can only get the info from debugDescription.
        NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:typeDescription];
        if ([methodSignature.debugDescription rangeOfString:@"is special struct return? YES"].location != NSNotFound) {
            msgForwardIMP = (IMP)_objc_msgForward_stret;
        }
    }
#endif
複製代碼

上面的代碼,第一判斷在非 arm64 下,第二判斷是否爲 union 或者 struct (詳見Type Encodings )。ide

最後,經過判斷方法簽名的 debugDescription 是否是包含特定字符串-is special struct return? YES,進而決定是否使用 _objc_msgForward_stret .能夠說是一個很是 trick 的作法了.函數

關於 Special Struct ,JSPatch 做者本身也在 JSPatch 實現原理詳解 中提到了緣由.文章說明在非 arm64 下都會存在 Special Struct 這樣的問題。而具體判斷的規則,蘋果並無提供給咱們,因此使用到了這樣的方法進行判斷也是無奈之舉。

好在通過大量項目運行以來,證實這個方法仍是靠譜的。

Aspects 的判斷

Aspects 一樣也是一個很是有名的開源項目,查看 Aspects 中與 _objc_msgForward_stret 相關的 commit,做者對 Special Struct 的判斷頗下功夫,先後修改了不少次。

最後的版本是這樣的:

static IMP aspect_getMsgForwardIMP(NSObject *self, SEL selector) {
IMP msgForwardIMP = _objc_msgForward;
#if !defined(__arm64__)
// As an ugly internal runtime implementation detail in the 32bit runtime, we need to determine of the method we hook returns a struct or anything larger than id.
// https://developer.apple.com/library/mac/documentation/DeveloperTools/Conceptual/LowLevelABI/000-Introduction/introduction.html
// https://github.com/ReactiveCocoa/ReactiveCocoa/issues/783
// http://infocenter.arm.com/help/topic/com.arm.doc.ihi0042e/IHI0042E_aapcs.pdf (Section 5.4)
Method method = class_getInstanceMethod(self.class, selector);
const char *encoding = method_getTypeEncoding(method);
BOOL methodReturnsStructValue = encoding[0] == _C_STRUCT_B;
if (methodReturnsStructValue) {
    @try {
        NSUInteger valueSize = 0;
        NSGetSizeAndAlignment(encoding, &valueSize, NULL);

        if (valueSize == 1 || valueSize == 2 || valueSize == 4 || valueSize == 8) {
            methodReturnsStructValue = NO;
        }
    } @catch (__unused NSException *e) {}
}
if (methodReturnsStructValue) {
    msgForwardIMP = (IMP)_objc_msgForward_stret;
}
#endif
 return msgForwardIMP;
}
複製代碼

與 JSPatch 相同,只對非 arm64 判斷使用 _objc_msgForward_stret.

最大的不一樣,在於 Aspects 是判斷方法返回值的內存大小,來決定是否使用_objc_msgForward_stret

根據代碼上的註釋,做者參考了 蘋果的 OS X ABI Function Call Guide ,以及 ARM 遵循的標準 Procedure Call Standard for the ARM® Architecture.

官方文檔的解釋

抱着搞明白的心態,我也去看了上述文檔裏面關於 Return Values 的說明:

通常來講,函數的返回值,和函數存儲在同一個寄存器當中。可是有些 Special Struct 太大了,超出了寄存器能存儲的範圍,就只能放置一個指針在存儲器上,指向內存中返回值所在的地址。

專門查閱了 System V Application Binary Interface 中的 Intel386 Architecture Processor Supplement ,這裏對於返回值爲結構體類型的存儲,有一個比較清楚的界定:

Structures. The called function returns structures according to their aligned size.

  • Structures 1 or 2 bytes in size are placed in EAX.
  • Structures 4 or 8 bytes in size are placed in: EAX and EDX.
  • Structures of other sizes are placed at the address supplied by the caller. For example, the C++ language occasionally forces the compiler to return a value in memory when it would normally be returned in registers. See Passing Arguments for more information.

IA-32 說明 1,2,4,8 字節大小的結構體,被存儲在寄存器中。其它大小的結構體,被放置的在寄存器中的,則是結構體的指針。

Aspects 中的判斷,應該就是基於這個的。我心想,靠譜了。

彷彿學習到姿式的我,興沖沖的去對 JSPatch 提了一個 PR .

事實證實,我仍是太年輕 ,測試結果 是這樣的:

Xcode7.3 iPhone4s(8.1) 成功

Xcode7.3 iPhone6s(9.3) 失敗

發現是在 64 位底下,一些結構體判斷失敗了。由於在 IA-32 下,寄存器是 32 位的。而新的機型,好比這裏測試的 6s 模擬器,則屬於 x86-64 ,寄存器是 64 位的。

因此須要增長對 16 字節的判斷。

因而,本地對 6s 進行測試經過後,又增長了一次提交:

if (valueSize == 1 || valueSize == 2 || valueSize == 4 || valueSize == 8 || valueSize == 16) {
                  methodReturnsStructValue = NO;
 }
複製代碼

然而....測試結果 仍是不行:

Xcode7.3 iPhone4s(8.1) 失敗

Xcode7.3 iPhone6s(9.3) 成功

上面說了,16 字節的判斷是在 64 位機型狀況下作的,因此在的 32 位的機型上, 也對 16 字節進行處理,繼續使用 _objc_msgForward 是會 Crash 的。

再增長一次提交:

#if defined(__LP64__) && __LP64__
          if (valueSize == 16) {
             methodReturnsStructValue = NO;
          }
#endif
複製代碼

終於經過了測試,完美 : )

這裏說明一下,爲何寄存器所能存儲的結構體,是自己處理器位數的 2 倍的問題:

好比,在 x86-64 中,RAX 一般用於存儲函數調用的返回結果,但同時也在乘法和除法指令中。在 imul 指令中,2 個 64 位的乘法最多會產生 128 位的結果,就須要 RAXRDX 共同存儲乘法結果. IA-32 也是一樣的道理.

在蘋果描述 32bit-PowerPC 函數規則的文檔裏,關於 Returning Results 的也有 2 個寄存器共同存儲 1 個返回值狀況的描述:

Values of type long long are returned in the high word of GPR3 and the low word of GPR4.

按照上述結果,Aspects 是有問題的,果真通過測試,在 64 位模擬器上返回結構體 Crash 了,這裏是我提供的 復現過程

ARM 中的處理

說完了模擬器中的處理,再來看看真機的規則。

查看蘋果的 iOS ABI Function Call Guide , ARMv6 ,ARMv7 等遵循的規則一致。找到 Procedure Call Standard for the ARM Architecture (AAPCS)。有一份在線的 PDF,裏面對 Result Return 有比較完整的說明:

The manner in which a result is returned from a function is determined by the type of that result. For the base standard:

  • A Half-precision Floating Point Type is returned in the least significant 16 bits of r0.
  • A Fundamental Data Type that is smaller than 4 bytes is zero- or sign-extended to a word and returned in r0.
  • A word-sized Fundamental Data Type (e.g., int, float) is returned in r0.
  • A double-word sized Fundamental Data Type (e.g., long long, double and 64-bit containerized vectors) is returned in r0 and r1.
  • A 128-bit containerized vector is returned in r0-r3.
  • A Composite Type not larger than 4 bytes is returned in r0. The format is as if the result had been stored in memory at a word-aligned address and then loaded into r0 with an LDR instruction. Any bits in r0 that lie outside the bounds of the result have unspecified values.
  • A Composite Type larger than 4 bytes, or whose size cannot be determined statically by both caller and callee, is stored in memory at an address passed as an extra argument when the function was called (§5.5, rule A.4). The memory to be used for the result may be modified at any point during the function call.

上面總結咱們要的關鍵信息,大於 4 字節的複合類型返回值,會被存儲在內存中的地址上,做爲一個額外的參數傳遞。

ARM64 爲何沒有大小限制?

前面一直判斷的,都屬於非 ARM64 的,我也很好奇,爲何 ARM64 就沒有問題?

查看關於 ARM64 調用規則文檔裏的 Result Return 說明:

The manner in which a result is returned from a function is determined by the type of that result:

  • If the type, T, of the result of a function is such that:
void func(T arg)
複製代碼

would require that arg be passed as a value in a register (or set of registers) according to the rules in §5.4 Parameter Passing, then the result is returned in the same registers as would be used for such an argument.

  • Otherwise, the caller shall reserve a block of memory of sufficient size and alignment to hold the result. The address of the memory block shall be passed as an additional argument to the function in x8. The callee may modify the result memory block at any point during the execution of the subroutine (there is no requirement for the callee to preserve the value stored in x8).

上面說到 x8 寄存器。查看關於返回值的寄存器功能的說明,以下:

寄存器 功能
r0…r7 Parameter/result registers
r8 Indirect result location register

第一條說的,就是返回值會和參數存在同樣的寄存器,也就是 x0-x7 中。

第二條說的,除了第一條的狀況以外,調用者就會爲這個函數預留一各足夠大小和對齊的內存塊,存在 x8 寄存器中。

因爲第二條規則,咱們能夠知道,只要返回的不是 void. arm64 上存儲的返回值都是經過指向內存的指針來作的。我也拿了一個很是大的結構體進行驗證:

typedef struct {
    CGRect rect;
    CGSize size;
    CGPoint orign;
}TestStruct;

typedef struct {
    TestStruct struct1;
    TestStruct struct2;
    TestStruct struct3;
    TestStruct struct4;
    TestStruct struct5;
    TestStruct struct6;
    TestStruct struct7;
    TestStruct struct8;
    TestStruct struct9;
    TestStruct struct10;
}TestBigStruct;
複製代碼

測試 TestBigStruct ,打印它的方法簽名的 debugDescription,包含的內容是 is special struct return? NO. valueSize 倒是 640,寄存器確定是存放不下,使用的指針指向內存。

規則彙總

斷定返回值Special Struct的條件:

機器 條件
i386 大小非 1,2,4,8 字節
x86-64 大小非 1,2,4,8,16 字節
arm-32 大於4字節
arm-64 不存在的 :)

固然,還有判斷方法簽名的 debugDescription 是否含有 is special struct return? YES 的方法。

總結

由於包含有 PowerPC-32/PowerPC-64/IA-32/X86-64/ARMv6/ARMv7/ARM64 這麼多個體系的英文說明,不少仍是關於寄存器和彙編的,能夠說看的我很是痛苦了。同時收穫也是很是大的。

提及來對 JSPatch 的原理解讀文章,應該沒有誰寫的比做者本人還好了,裏面介紹了項目實際遇到的各類難點。讀完下來,其中關於 super關鍵字 的解讀,給了我另外一個問題的靈感。

建議你們學習新知識時,不妨結合知名的項目進行借鑑,能學到許多知識 : )

此次的探究過程,徹底屬於本菜鳥本身瞎摸索的,若是有不對的地方,但願你們多拍磚,讓我進一步學習。

參考

Introduction to OS X ABI Function Call Guide

PowerPC 體系結構開發者指南

Intel386 Architecture Processor Supplement

iOS ABI Function Call Guide

Procedure Call Standard for the ARM 64-bit Architecture

Procedure Call Standard for the ARM® Architecture

JSPatch 實現原理詳解

objc_msgSend_stret

重識 Objective-C Runtime - 看透 Type 與 Value

什麼是-x8六、i38六、ia32等等

相關文章
相關標籤/搜索