在 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")
, 如果 true
, return 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 源码地址。