preface

This article requires understanding of message forwarding mechanism. It is recommended to read the principles of Objective-C message sending and forwarding mechanism

I happened to learn Method Swizzling in August, read Aspects and JSPatch to do Method replacement processing, and noticed the main character of our introduction this time –_objc_msgForward_stret.

JSPatch’s current Star count has broken ten thousand, which shows its popularity. It’s also often mentioned in interviews. Aspects is also a well-known AOP library.

On message forwarding, we determine whether IMP uses _objc_msgForward or _objc_msgForward_stret, depending on the type of value returned by the method.

According to Apple’s documentation, using _objc_msgForward_stret must be a structure:

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

However, the rules for determining _objc_msgForward_stret vary by CPU architecture. Here’s a look at what two well-known open source libraries do.

JSPatch judgment

First, let’s see how JSPatch, the overrideMethod in jpengine. m, determines whether to use _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; } } #endifCopy the code

In the above code, the first judge is under non-ARM64 and the second judge is union or struct (see Type Encodings).

-is special struct return? -is special struct return? YES, which in turn determines whether to use _objc_msgForward_stret. Can be said to be a very trick practice.

As for the Special Struct, the author of JSPatch also mentioned the reason in the detailed explanation of the implementation principle of JSPatch. The article explains that Special structs exist in all non-ARM64 structures. Apple has not provided us with the specific rules of judgment, so it is helpless to use such a method to make judgment.

Fortunately, this approach has been proven to work in a number of projects.

Aspects of judgment

Aspects is also a well-known open source project. If you look at the COMMIT related to _objc_msgForward_stret in Aspects, the author has made a lot of changes to the Special Struct.

The final version looks like this:

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/introductio n.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; }Copy the code

As with JSPatch, _objc_msgForward_stret is used only for non-ARM64 judgments.

The biggest difference is that Aspects determine the memory size of the return value of the method to determine whether to use _objc_msgForward_stret.

According to the comments on the code, the author referred to Apple’s OS X ABI Function Call Guide, as well as the Procedure Call Standard for the ARM® Architecture that ARM follows.

Explanation of official documents

With a clear mind, I also read the description of Return Values in the above document:

Generally, the return value of a function is stored in the same register as the function. But some Special structs are too large to be stored in registers, so they can only place a pointer in memory to the address of the return value in memory.

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 indicates 1,2,4, 8-byte structures that are stored in registers. Structures of other sizes, placed in registers, are Pointers to structures.

The judgment in Aspects should be based on this. Good for you, I thought.

As if I had learned the gesture, I excitedly gave JSPatch a PR.

As it turned out, I was still too young, and here’s what happened:

Xcode7.3 4 s (8.1)

Xcode7.3 iPhone6s (9.3)

It was found to be below 64 bits, and some structure judgments failed. Because under IA-32, the registers are 32 bits. Newer models, such as the 6S emulator tested here, are x86-64, with 64-bit registers.

Therefore, the 16-byte judgment needs to be added.

Therefore, after the local test of 6S passed, another submission was added:

 if (valueSize == 1 || valueSize == 2 || valueSize == 4 || valueSize == 8 || valueSize == 16) {
                  methodReturnsStructValue = NO;
 }
Copy the code

However… The test results still don’t work:

Xcode7.3 4 s (8.1) failure

Xcode7.3 iPhone6s (9.3)

As stated above, the 16-byte determination is made on the 64-bit model, so on the 32-bit model, the 16-byte determination is also processed, and using _objc_msgForward will Crash.

Add one more submission:

#if defined(__LP64__) && __LP64__
          if (valueSize == 16) {
             methodReturnsStructValue = NO;
          }
#endif
Copy the code

Finally passed the test, perfect:)

Here’s why registers can store twice as many bits of structure as the processor itself:

For example, in x86-64, RAX is usually used to store the return result of a function call, but it is also used in multiplication and division instructions. In the IMul instruction, a multiplication of two 64-bit bits yields a maximum of 128 bits, requiring RAX to store the multiplication together with RDX. The same goes for the IA-32.

In the Apple document describing the rules for 32-bit PowerPC functions, there is also a description of the situation in which 2 Returning registers are used to store a return value:

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

According to the above results, the Aspects were problematic. Sure enough, the test returned the Crash of the structure on the 64-bit simulator. Here is the replay process I provided.

Processing in ARM

Having said that, let’s look at the rules of the real machine.

Check out Apple’s iOS ABI Function Call Guide, ARMv6,ARMv7, etc to follow consistent rules. Find Procedure Call Standard for the ARM Architecture (AAPCS). There is an online PDF with a complete description of 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.

To summarize the key information we need above, the return value of a compound type greater than 4 bytes is stored at the address in memory and passed as an additional parameter.

Why does the ARM64 have no size limit?

All of the ones I’ve been judging are not ARM64, and I’m curious, why is ARM64 ok?

ARM64 call 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)
Copy the code

Would require that an 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).

The x8 register was mentioned above. For a description of the return value register function, see below:

register function
R0… r7 Parameter/result registers
r8 Indirect result location register

The first one is that the return value will reside in the same register as the parameter, namely in x0-x7.

In addition to the first case, the caller will reserve a memory block of sufficient size and alignment for the function, stored in the X8 register.

Because of the second rule, we know that any return value stored on void. Arm64 that is not returned is done by a pointer to memory. I also took a very large structure to verify:

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;
Copy the code

Test TestBigStruct and print debugDescription of its method signature, including is special struct return? NO. ValueSize is 640, the register must not be stored, the pointer to the memory.

Rules of the summary

Conditions for determining the return Special Struct:

The machine conditions
i386 Size not 1,2,4,8 bytes
x86-64 Size not 1,2,4,8,16 bytes
arm-32 Greater than 4 bytes
arm-64 Non-existent 🙂

Does the debugDescription of the method signature contain an is special struct return? YES method.

conclusion

Because contains powerPC-32 / PowerPC-64 / IA-32 / x86-64 / ARMv6 / ARMv7 / ARM64 so many systems Of English description, many or registers and assembly, can be said to see me very painful. At the same time the harvest is also very big.

Speaking of the principle of JSPatch, no one should have written a better article than the author himself, which introduces various difficulties encountered in the actual project. After reading it, the interpretation of the super keyword gave me another inspiration.

It is suggested that when learning new knowledge, you may wish to combine well-known projects for reference, can learn a lot of knowledge:)

This inquiry process, completely belongs to this novice oneself fumble, if there is wrong place, hope everybody pats brick more, let me further study.

reference

Introduction to OS X ABI Function Call Guide

A Developer’s Guide to PowerPC Architecture

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 implementation principle detailed explanation

objc_msgSend_stret

Re-identify Objective-C Runtime – See through Type and Value

What is -x86, i386, IA32, etc.