移动开发

关于alloc初探中alloc进入objc_alloc的原因

2020-12-21 10:11:11 阅读数 2810 收藏 0

alloc初探中 调试 objc 源码的时候发现了一个问题,明明点击 [[NSObject alloc] init]alloc 方法的时候进入的是 _objc_rootAlloc ,那怎么会调试的时候发现进入的是 objc_alloc ?这么诡异的问题到底出在哪里?

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSObject *object = [[NSObject alloc] init];
        [object testClassInstanceMethod];
        NSLog(@"Hello, World! %@",object);
    }
    return 0;
}

+ (id)alloc {
    return _objc_rootAlloc(self);
}

// Calls [cls alloc].
id
objc_alloc(Class cls)
{
    return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}

1、从调试和源码以及 MachO 中寻找答案

创建一个 NSObject 的子类 TestClass ,再次重现调试过程。

从上方的截图就能看到,先调用的是 objc_alloc ,然后调用 callAlloc 函数的 [cls alloc] ,然后才来到我们点击 alloc 直接跳转的方法里面来,接下来就是 alloc初探 的流程了。

打开 objc-debug.MachO 文件,在如下 symbol Table 段就能看到 objc_alloc 的符号绑定。

其实macho 在编译绑定符号的时候将 sel_alloc 绑定到了 objc_alloc 上。事实上 objc_alloc 并没有真正开源,也不能确定是怎么绑定到 objc_alloc 上的,但是如下代码给了思路:

static void 
fixupMessageRef(message_ref_t *msg)
{    
   msg->sel = sel_registerName((const char *)msg->sel);

   if (msg->imp == &objc_msgSend_fixup) { 
       if (msg->sel == SEL_alloc) {
           msg->imp = (IMP)&objc_alloc;
       } else if (msg->sel == SEL_allocWithZone) {
           msg->imp = (IMP)&objc_allocWithZone;
       } else if (msg->sel == SEL_retain) {
           msg->imp = (IMP)&objc_retain;
       } else if (msg->sel == SEL_release) {
           msg->imp = (IMP)&objc_release;
       } else if (msg->sel == SEL_autorelease) {
           msg->imp = (IMP)&objc_autorelease;
       } else {
           msg->imp = &objc_msgSend_fixedup;
       }
   } 
   
   //...
}

如果符号绑定失败了就会触发一个这样的修复操作,明显的能看到 if (msg->sel == SEL_alloc) , msg->imp = (IMP)&objc_alloc; 这和平常使用的 Method Swizzling 很相似,不过这里只是临时交换了一下,而 Method Swizzling 是永久交换。

2、从 LLVM 中寻找答案

上述代码运行进入汇编,能看到 alloc 的时候先调起了 symbol stub for: objc_alloc 然后才调用 objc_msgSend 发送 testClassInstanceMethod 的消息。

这里就和平时调用方法不一样了,在 OC 中调用方法是发消息,在这里却是调用了符号 symbol objc_alloc ,并没有调用 objc_msgSend ,明白了这里并不是我们调用 alloc 发送消息的,而是系统接收到我们调用了 alloc ,然后系统帮我们调用了底层的符号 objc_alloc

从上方的 MachO 中能看到在编译成功后就有了 objc_alloc ,得知产生 objc_alloc 时间比较超前,在编译期就有了,所以我们可以看看 LLVM

1、搜索测试类里关于 objc_alloc

convert-messages-to-runtime-calls.m 下有一个这样的方法, test 是测试方法,test_alloc_class_ptr(对象创建时会调) 方法下注释里写了,会调用指针 *objc_alloc 然后偏移后返回再调用 alloc

// Make sure we get a bitcast on the return type as the
// call will return i8* which we have to cast to A*
// CHECK-LABEL: define {{.*}}void @test_alloc_class_ptr
A* test_alloc_class_ptr() {
  // CALLS: {{call.*@objc_alloc}}
  // CALLS-NEXT: bitcast i8*
  // CALLS-NEXT: ret
  return [B alloc];
}

2、搜索 objc_alloc

既然我们知道了会调用 objc_alloc ,继续查找在 CGObjC.cpp ,看到了如下代码。

/// Allocate the given objc object.
///   call i8* \@objc_alloc(i8* %value)
llvm::Value *CodeGenFunction::EmitObjCAlloc(llvm::Value *value,
                                            llvm::Type *resultType) {
  return emitObjCValueOperation(*this, value, resultType,
                                CGM.getObjCEntrypoints().objc_alloc,
                                "objc_alloc");
}

3、EmitObjCAlloc 对于 alloc 调用的判断

发现在调用函数 EmitObjCAlloc 的时候,有了有关 objc_alloc 的信息,搜索 EmitObjCAlloc 在哪里调用的。

CGObjC.cpp 中发现了下方调用:

在进入OMF_alloc 分支的时候,判断 if (Sel.isUnarySelector() && Sel.getNameForSlot(0) == "alloc") , 如果 truereturn CGF.EmitObjCAlloc(Receiver, CGF.ConvertType(ResultType));

// CGObjC.cpp

static Optional<llvm::Value *>
tryGenerateSpecializedMessageSend(CodeGenFunction &CGF, QualType ResultType,
                                  llvm::Value *Receiver,
                                  const CallArgList& Args, Selector Sel,
                                  const ObjCMethodDecl *method,
                                  bool isClassMessage) {
  auto &CGM = CGF.CGM;
  if (!CGM.getCodeGenOpts().ObjCConvertMessagesToRuntimeCalls)
    return None;

  auto &Runtime = CGM.getLangOpts().ObjCRuntime;
  switch (Sel.getMethodFamily()) {
  case OMF_alloc:
    if (isClassMessage && Runtime.shouldUseRuntimeFunctionsForAlloc() &&  ResultType->isObjCObjectPointerType()) {
        
        // 重点
        if (Sel.isUnarySelector() && Sel.getNameForSlot(0) == "alloc")
          
            return CGF.EmitObjCAlloc(Receiver, CGF.ConvertType(ResultType));
    
       // ...
       // 其他代码不理会
    }
    break;

  case OMF_autorelease:
     // ...
     // 其他分支不理会
    break;

  case OMF_retain:
    // ...
    // 其他分支不理会
    break;

  case OMF_release:
    // ...
    // 其他分支不理会
    break;

  default:
    break;
  }
  return None;
}

结合上方两段代码有点思路了,原来系统在编译的时候拦截了 alloc ,如果有 alloc 的方法调用,把 objc_alloc 传入下层进行处理。

好,接着追... 。

4、emitObjCValueOperation 对于 objc_alloc 的绑定

上方代码看到调用了一个函数 emitObjCValueOperation ,我们查找一下这个函数的实现,然后去除无用的判断代码,变成下方代码了。

/// Perform an operation having the signature
///   i8* (i8*)
/// where a null input causes a no-op and returns null.
static llvm::Value *emitObjCValueOperation(CodeGenFunction &CGF,
                                           llvm::Value *value,
                                           llvm::Type *returnType,
                                           llvm::FunctionCallee &fn,
                                           StringRef fnName) {
  if (isa<llvm::ConstantPointerNull>(value))
    return value;

  if (!fn) {
      //... 删除了这里的不存在的 `fn` 处理
  }

  //... 删除一些无用代码
    
  // Call the function.
  llvm::CallBase *Inst = CGF.EmitCallOrInvoke(fn, value);

  // Cast the result back to the original type.
  return CGF.Builder.CreateBitCast(Inst, origType);
}

下方代码方便参数对比:

                    emitObjCValueOperation(*this,
                                            value, 
                                            resultType, 
                                            CGM.getObjCEntrypoints().objc_alloc, 
                                            "objc_alloc");

static llvm::Value *emitObjCValueOperation(CodeGenFunction &CGF,
                                           llvm::Value *value,
                                           llvm::Type *returnType,
                                           llvm::FunctionCallee &fn,
                                           StringRef fnName)

我们对比一下传入的参数发现, CGM.getObjCEntrypoints().objc_alloc 给了 FunctionCallee &fn"objc_alloc" 给了 StringRef fnName,还看到了 llvm::CallBase *Inst = CGF.EmitCallOrInvoke(fn, value); 调用 EmitCallOrInvoke 返回了 Inst 。接着查找 EmitCallOrInvoke 函数的实现:

/// Emits a call or invoke instruction to the given function, depending 
/// on the current state of the EH stack.
//上方翻译: 根据EH堆栈的当前状态,向给定的函数发出调用或调用指令。
llvm::CallBase *CodeGenFunction::EmitCallOrInvoke(llvm::FunctionCallee Callee,
                                                  ArrayRef<llvm::Value *> Args,
                                                  const Twine &Name) {
  llvm::BasicBlock *InvokeDest = getInvokeDest();
  SmallVector<llvm::OperandBundleDef, 1> BundleList =
      getBundlesForFunclet(Callee.getCallee());

  llvm::CallBase *Inst;
  if (!InvokeDest)
  
    //  CreateInvoke 创建调用
    Inst = Builder.CreateCall(Callee, Args, BundleList, Name);
  else {
    llvm::BasicBlock *ContBB = createBasicBlock("invoke.cont");
    
    //  CreateInvoke 创建调用
    Inst = Builder.CreateInvoke(Callee, ContBB, InvokeDest, Args, BundleList,
                                Name);
    EmitBlock(ContBB);
  }
    
  // 删除无用代码     
  // ...

  return Inst;
}

从上方的注释明白了根据EH堆栈的当前状态,向给定的函数发出调用或调用指令。这段代码的核心其实就是 Builder.CreateCall(Callee, Args, ...) ,而 Callee 是之前传入的 fn ,也就是 CGM.getObjCEntrypoints().objc_alloc ,Args 是参数,从这里能看到调用了 alloc 来到了 objc_alloc

以上就是对 alloc 时候没有直接进入 objc_rootAlloc 而是进入 objc_alloc 的一点点补充 。

PS:可以运行的并且不断进行注释的objc_756.2 源码地址