在 iOS逆向07 -- Dyld动态加载器 这篇文章中,详细阐述了App启动运行过程中Dyld的工作流程,本篇是在此基础上来探讨dyld与objc之间是如何互动关联的;
_objc_init
源码分析
-
_objc_init
是objcLib中的函数,其底层实现如下:
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
runtime_init();
exception_init();
cache_init();
_imp_implementationWithBlock_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}
- 【第一步:
environ_init()
】环境变量的初始化 - 下面通过修改源码,去除所有限制条件,在控制台上打印出所有环境变量,
源码修改如下所示:
![](https://img.haomeiwen.com/i25440976/6adbd6a2351fcf9b.png)
- 控制台上打印的环境变量如下:
objc[38442]: OBJC_PRINT_IMAGES: log image and library names as they are loaded
objc[38442]: OBJC_PRINT_IMAGE_TIMES: measure duration of image loading steps
objc[38442]: OBJC_PRINT_LOAD_METHODS: log calls to class and category +load methods
objc[38442]: OBJC_PRINT_INITIALIZE_METHODS: log calls to class +initialize methods
objc[38442]: OBJC_PRINT_RESOLVED_METHODS: log methods created by +resolveClassMethod: and +resolveInstanceMethod:
objc[38442]: OBJC_PRINT_CLASS_SETUP: log progress of class and category setup
objc[38442]: OBJC_PRINT_PROTOCOL_SETUP: log progress of protocol setup
objc[38442]: OBJC_PRINT_IVAR_SETUP: log processing of non-fragile ivars
objc[38442]: OBJC_PRINT_VTABLE_SETUP: log processing of class vtables
objc[38442]: OBJC_PRINT_VTABLE_IMAGES: print vtable images showing overridden methods
objc[38442]: OBJC_PRINT_CACHE_SETUP: log processing of method caches
objc[38442]: OBJC_PRINT_FUTURE_CLASSES: log use of future classes for toll-free bridging
objc[38442]: OBJC_PRINT_PREOPTIMIZATION: log preoptimization courtesy of dyld shared cache
objc[38442]: OBJC_PRINT_CXX_CTORS: log calls to C++ ctors and dtors for instance variables
objc[38442]: OBJC_PRINT_EXCEPTIONS: log exception handling
objc[38442]: OBJC_PRINT_EXCEPTION_THROW: log backtrace of every objc_exception_throw()
objc[38442]: OBJC_PRINT_ALT_HANDLERS: log processing of exception alt handlers
objc[38442]: OBJC_PRINT_REPLACED_METHODS: log methods replaced by category implementations
objc[38442]: OBJC_PRINT_DEPRECATION_WARNINGS: warn about calls to deprecated runtime functions
objc[38442]: OBJC_PRINT_POOL_HIGHWATER: log high-water marks for autorelease pools
objc[38442]: OBJC_PRINT_CUSTOM_CORE: log classes with custom core methods
objc[38442]: OBJC_PRINT_CUSTOM_RR: log classes with custom retain/release methods
objc[38442]: OBJC_PRINT_CUSTOM_AWZ: log classes with custom allocWithZone methods
objc[38442]: OBJC_PRINT_RAW_ISA: log classes that require raw pointer isa fields
objc[38442]: OBJC_DEBUG_UNLOAD: warn about poorly-behaving bundles when unloaded
objc[38442]: OBJC_DEBUG_FRAGILE_SUPERCLASSES: warn about subclasses that may have been broken by subsequent changes to superclasses
objc[38442]: OBJC_DEBUG_NIL_SYNC: warn about @synchronized(nil), which does no synchronization
objc[38442]: OBJC_DEBUG_NONFRAGILE_IVARS: capriciously rearrange non-fragile ivars
objc[38442]: OBJC_DEBUG_ALT_HANDLERS: record more info about bad alt handler use
objc[38442]: OBJC_DEBUG_MISSING_POOLS: warn about autorelease with no pool in place, which may be a leak
objc[38442]: OBJC_DEBUG_POOL_ALLOCATION: halt when autorelease pools are popped out of order, and allow heap debuggers to track autorelease pools
objc[38442]: OBJC_DEBUG_DUPLICATE_CLASSES: halt when multiple classes with the same name are present
objc[38442]: OBJC_DEBUG_DONT_CRASH: halt the process by exiting instead of crashing
objc[38442]: OBJC_DISABLE_VTABLES: disable vtable dispatch
objc[38442]: OBJC_DISABLE_PREOPTIMIZATION: disable preoptimization courtesy of dyld shared cache
objc[38442]: OBJC_DISABLE_TAGGED_POINTERS: disable tagged pointer optimization of NSNumber et al.
objc[38442]: OBJC_DISABLE_TAG_OBFUSCATION: disable obfuscation of tagged pointers
objc[38442]: OBJC_DISABLE_NONPOINTER_ISA: disable non-pointer isa fields
objc[38442]: OBJC_DISABLE_INITIALIZE_FORK_SAFETY: disable safety checks for +initialize after fork
- 上面的环境变量均可通过
target -- Edit Scheme -- Run --Arguments -- Environment Variables
进行配置
![](https://img.haomeiwen.com/i25440976/99cd917a207d977d.png)
- 下面介绍几个常用的环境变量:
-
DYLD_PRINT_STATISTICS
:设置DYLD_PRINT_STATISTICS
为YES时,控制台就会打印 App 的加载时长,包括整体加载时长和动态库加载时长,即main函数之前的启动时间(查看pre-main耗时),可以通过设置了解其耗时部分,并对其进行启动优化; - 设置如下所示:
![](https://img.haomeiwen.com/i25440976/5c208bafc0d786de.png)
- 控制台打印结果如下所示:
![](https://img.haomeiwen.com/i25440976/1245e784a1118d45.png)
-
OBJC_DISABLE_NONPOINTER_ISA
:杜绝生成相应的nonpointer isa(nonpointer isa指针地址 末尾为1 ),生成的都是普通的isa -
OBJC_PRINT_LOAD_METHODS
:打印出所有Class 及 Category 的load类方法
的调用信息; -
NSDoubleLocalizedStrings
:项目做国际化本地化(Localized)的时候是一个挺耗时的工作,想要检测国际化翻译好的语言文字UI会变成什么样子,可以指定这个启动项。可以设置 NSDoubleLocalizedStrings 为YES; -
NSShowNonLocalizedStrings
:在完成国际化的时候,偶尔会有一些字符串没有做本地化,这时就可以设置NSShowNonLocalizedStrings 为YES,所有没有被本地化的字符串全都会变成大写。 -
【第二步:
tls_init()
】:线程key的绑定,主要涉及本地线程池的初始化及析构;
void tls_init(void)
{
#if SUPPORT_DIRECT_THREAD_KEYS
pthread_key_init_np(TLS_DIRECT_KEY, &_objc_pthread_destroyspecific);
#else
_objc_pthread_key = tls_create(&_objc_pthread_destroyspecific);
#endif
}
- 【第三步:
static_init()
】:运行系统级别的C++静态构造函数
主要是运行系统级别的C++静态构造函数,在dyld调用我们的静态构造函数之前,libc调用_objc_init方法,即系统级别的C++构造函数 先于 自定义的C++构造函数 运行;
/***********************************************************************
* static_init
* Run C++ static constructor functions.
* libc calls _objc_init() before dyld would call our static constructors,
* so we have to do it ourselves.
**********************************************************************/
static void static_init()
{
size_t count;
auto inits = getLibobjcInitializers(&_mh_dylib_header, &count);
for (size_t i = 0; i < count; i++) {
inits[i]();
}
}
- 【第四步:
runtime_init()
】:运行时的初始化 - 运行时的初始化包含两个部分:分类与类的初始化;
void runtime_init(void)
{
objc::unattachedCategories.init(32);
objc::allocatedClasses.init();
}
- 【第五步:
exception_init()
】:初始化libobjc的异常处理系统 - 主要是初始化libobjc的异常处理系统,注册异常处理的回调,从而监控异常的处理,源码如下:
/***********************************************************************
* exception_init
* Initialize libobjc's exception handling system.
* Called by map_images().
**********************************************************************/
void exception_init(void)
{
old_terminate = std::set_terminate(&_objc_terminate);
}
- 当有crash发生时,会执行
_objc_terminate
函数,走到uncaught_handler扔出异常;
static void _objc_terminate(void)
{
if (PrintExceptions) {
_objc_inform("EXCEPTIONS: terminating");
}
if (! __cxa_current_exception_type()) {
// No current exception.
(*old_terminate)();
}else {
// There is a current exception. Check if it's an objc exception.
@try {
__cxa_rethrow();
} @catch (id e) {
// It's an objc object. Call Foundation's handler, if any.
(*uncaught_handler)((id)e);
(*old_terminate)();
} @catch (...) {
// It's not an objc object. Continue to C++ terminate.
(*old_terminate)();
}
}
}
- 搜索
uncaught_handler
App应用层会传入一个回调函数,专门用来处理异常的回调函数,如下所示,其中fn即为传入的函数,即 uncaught_handler 等于 fn
objc_uncaught_exception_handler
objc_setUncaughtExceptionHandler(objc_uncaught_exception_handler fn)
{
// fn为设置的异常句柄 传入的函数,为外界给的
objc_uncaught_exception_handler result = uncaught_handler;
uncaught_handler = fn; //赋值
return result;
}
crash分类
-
应用程序出现crash,主要是因为收到了未处理的信号,信号主要来源于3个地方:
-
kernel 内核
; -
其他进行
; -
App本身
;
-
-
所以这三种未处理的信号,对应了三种类型的crash;
-
Mach异常
:是指最底层的内核级异常。用户态的开发者可以直接通过Mach API设置thread,task,host的异常端口,来捕获Mach异常。 -
Unix信号
:又称BSD 信号,如果开发者没有捕获Mach异常,则会被host层的方法ux_exception()将Mach异常转换为对应的UNIX信号,并通过方法threadsignal()将信号投递到出错线程。可以通过方法signal(x, SignalHandler)来捕获single;Mach异常
与Unix信号
异常是同一个异常在不同层级中处理; -
NSException 应用级异常
:它是未被捕获的Objective-C异常,导致程序向自身发送了SIGABRT信号而崩溃,对于未捕获的Objective-C异常,是可以通过try catch来捕获的,或者通过NSSetUncaughtExceptionHandler()机制来捕获;
-
-
针对应用级异常,可以通过注册异常捕获的函数,即NSSetUncaughtExceptionHandler机制,实现线程保活,收集上传崩溃日志;
-
【第六步:
cache_init()
】:缓存初始化
void cache_init()
{
#if HAVE_TASK_RESTARTABLE_RANGES
mach_msg_type_number_t count = 0;
kern_return_t kr;
while (objc_restartableRanges[count].location) {
count++;
}
kr = task_restartable_ranges_register(mach_task_self(),
objc_restartableRanges, count);
if (kr == KERN_SUCCESS) return;
_objc_fatal("task_restartable_ranges_register failed (result 0x%x: %s)",
kr, mach_error_string(kr));
#endif // HAVE_TASK_RESTARTABLE_RANGES
}
- 【第七步:
_imp_implementationWithBlock_init()
】:启动回调机制 - 在某些进程中渴望加载libobjc-trampolines.dylib,一些程序(最著名的是嵌入式Chromium的较早版本使用的QtWebEngineProcess)启用了严格限制的沙盒配置文件,从而阻止了对该dylib的访问,如果有任何调用imp_implementationWithBlock的操作(如AppKit开始执行的操作),那么我们将在尝试加载它时崩溃,将其加载到此处可在启用沙箱配置文件之前对其进行设置并阻止它;
void
_imp_implementationWithBlock_init(void)
{
#if TARGET_OS_OSX
// Eagerly load libobjc-trampolines.dylib in certain processes. Some
// programs (most notably QtWebEngineProcess used by older versions of
// embedded Chromium) enable a highly restrictive sandbox profile which
// blocks access to that dylib. If anything calls
// imp_implementationWithBlock (as AppKit has started doing) then we'll
// crash trying to load it. Loading it here sets it up before the sandbox
// profile is enabled and blocks it.
//
// This fixes EA Origin (rdar://problem/50813789)
// and Steam (rdar://problem/55286131)
if (__progname &&
(strcmp(__progname, "QtWebEngineProcess") == 0 ||
strcmp(__progname, "Steam Helper") == 0)) {
Trampolines.Initialize();
}
#endif
}
- 【第八步:
_dyld_objc_notify_register(&map_images, load_images, unmap_image)
】 -
objc
向Dyld
注册回调函数_dyld_objc_notify_register()
函数的声明与实现是在dyld中:
// Note: only for use by objc runtime
// Register handlers to be called when objc images are mapped, unmapped, and initialized.
// Dyld will call back the "mapped" function with an array of images that contain an objc-image-info section.
// Those images that are dylibs will have the ref-counts automatically bumped, so objc will no longer need to
// call dlopen() on them to keep them from being unloaded. During the call to _dyld_objc_notify_register(),
// dyld will call the "mapped" function with already loaded objc images. During any later dlopen() call,
// dyld will also call the "mapped" function. Dyld will call the "init" function when dyld would be called
// initializers in that image. This is when objc calls any +load methods in that image.
//
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped);
从注释可以看出:
-
此函数仅仅提供给objc运行时调用的;
-
当objc的镜像被mapped,unmapped,initialized时,注册的回调函数会被调用;
-
dyld
将会通过一个包含objc-image-info的镜像文件的数组
回调mapped函数。 -
objc调用_dyld_objc_notify_register()函数传入的三个参数含义如下:
-
map_images
:dyld将image(镜像文件)加载进内存时,会触发该函数; -
load_images
:dyld初始化image会触发该函数; -
unmap_image
:dyld将image移除时,会触发该函数;
-
Dyld与Objc之间的关联
- Dyld中
_dyld_objc_notify_register()
函数实现如下:
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped)
{
dyld::registerObjCNotifiers(mapped, init, unmapped);
}
-
根据在objc中调用传入的参数,存在下面的对应关系:
-
mapped
等价于map_images
-
init
等价于load_images
-
unmapped
等价于unmap_image
-
-
内部调用了
registerObjCNotifiers()
,实现如下:
void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
// record functions to call
sNotifyObjCMapped = mapped;
sNotifyObjCInit = init;
sNotifyObjCUnmapped = unmapped;
// call 'mapped' function with all images mapped so far
try {
notifyBatchPartial(dyld_image_state_bound, true, NULL, false, true);
}
catch (const char* msg) {
// ignore request to abort during registration
}
// <rdar://problem/32209809> call 'init' function on all images already init'ed (below libSystem)
for (std::vector<ImageLoader*>::iterator it=sAllImages.begin(); it != sAllImages.end(); it++) {
ImageLoader* image = *it;
if ( (image->getState() == dyld_image_state_initialized) && image->notifyObjC() ) {
dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
}
}
}
- 所以存在下面的等价关系:
- sNotifyObjCMapped == mapped == map_images
- sNotifyObjCInit == init == load_images
- sNotifyObjCUnmapped == unmapped == unmap_image
map_images回调函数的调用时机
- 在dyld源码中全局搜索
sNotifyObjCMapped
,发现sNotifyObjCMapped
是在notifyBatchPartial
中被调用的,再全局搜索notifyBatchPartial
发现是在registerObjCNotifiers
中调用的;
![](https://img.haomeiwen.com/i25440976/611aa40d55e74a12.png)
load_images回调函数的调用时机
-
load_images
的调用时机在 iOS App运行加载中已经详细阐述了,是在第二次notifySingle()
中调用的,所以map_images
要先于load_images
调用;
unmap_image回调函数的调用时机
- 在dyld源码中全局搜索
sNotifyObjCUnmapped
,发现sNotifyObjCUnmapped
是在removeImage()
中被调用的,即移除image镜像时,调用unmap_image
回调函数;
总结
- Mach-O文件从Dyld的动态链接加载,到Objc将所有文件的加载进入内存,调用类与分类的load方法,最后到Application的Main函数入口,其整体的流程如下图所示:
![](https://img.haomeiwen.com/i25440976/b12f813dc30f40fc.png)
- objc调用
_dyld_objc_notify_register(&map_images, load_images, unmap_image)
函数,然后dyld接收到这三个方法参数dyld::registerObjCNotifiers(mapped, init, unmapped)
,并用全局变量保存,对应关系如下:-
map_images
<==>sNotifyObjCMapped
-
load_images
<==>sNotifyObjCInit
-
unmap_image
<==>sNotifyObjCUnmapped
- 最后dyld在合适的时机,分别调用这三个函数方法;
-
网友评论