你真的會判斷 _objc_msgForward_stret 嗎

ZenonHuang發表於2019-03-03

前言

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

恰巧在 8 月學習 Method Swizzling ,閱讀了 Aspects 和 JSPatch 做方法替換的處理,注意到了我們這次介紹的主角 —_objc_msgForward_stret.

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

在訊息轉發時,我們根據方法返回值的型別,來決定 IMP 使用 _objc_msgForward 或者 _objc_msgForward_stret.

根據蘋果的文件描述,使用 _objc_msgForward_stret 的肯定是一個結構體:

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

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

JSPatch 的判斷

首先我們來看 JSPatch , 在 JPEngine.m 檔案裡的 overrideMethod 方法,是如何去判斷是否使用 _objc_msgForward_stret:

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 )。

最後,通過判斷方法簽名的 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

什麼是-x86、i386、ia32等等

相關文章