在 iOS逆向07 -- Dyld動(dòng)態(tài)加載器 這篇文章中,詳細(xì)闡述了App啟動(dòng)運(yùn)行過程中Dyld的工作流程膛锭,本篇是在此基礎(chǔ)上來探討dyld與objc之間是如何互動(dòng)關(guān)聯(lián)的粮坞;
_objc_init
源碼分析
-
_objc_init
是objcLib中的函數(shù),其底層實(shí)現(xiàn)如下:
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()
】環(huán)境變量的初始化 - 下面通過修改源碼初狰,去除所有限制條件捞蚂,在控制臺(tái)上打印出所有環(huán)境變量,
源碼修改如下所示:
- 控制臺(tái)上打印的環(huán)境變量如下:
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
- 上面的環(huán)境變量均可通過
target -- Edit Scheme -- Run --Arguments -- Environment Variables
進(jìn)行配置
- 下面介紹幾個(gè)常用的環(huán)境變量:
-
DYLD_PRINT_STATISTICS
:設(shè)置DYLD_PRINT_STATISTICS
為YES時(shí)跷究,控制臺(tái)就會(huì)打印 App 的加載時(shí)長(zhǎng)姓迅,包括整體加載時(shí)長(zhǎng)和動(dòng)態(tài)庫加載時(shí)長(zhǎng),即main函數(shù)之前的啟動(dòng)時(shí)間(查看pre-main耗時(shí))俊马,可以通過設(shè)置了解其耗時(shí)部分丁存,并對(duì)其進(jìn)行啟動(dòng)優(yōu)化; - 設(shè)置如下所示:
- 控制臺(tái)打印結(jié)果如下所示:
OBJC_DISABLE_NONPOINTER_ISA
:杜絕生成相應(yīng)的nonpointer isa(nonpointer isa指針地址 末尾為1 )柴我,生成的都是普通的isaOBJC_PRINT_LOAD_METHODS
:打印出所有Class 及 Category 的load類方法
的調(diào)用信息解寝;NSDoubleLocalizedStrings
:項(xiàng)目做國際化本地化(Localized)的時(shí)候是一個(gè)挺耗時(shí)的工作,想要檢測(cè)國際化翻譯好的語言文字UI會(huì)變成什么樣子艘儒,可以指定這個(gè)啟動(dòng)項(xiàng)聋伦。可以設(shè)置 NSDoubleLocalizedStrings 為YES界睁;NSShowNonLocalizedStrings
:在完成國際化的時(shí)候觉增,偶爾會(huì)有一些字符串沒有做本地化,這時(shí)就可以設(shè)置NSShowNonLocalizedStrings 為YES翻斟,所有沒有被本地化的字符串全都會(huì)變成大寫逾礁。【第二步:
tls_init()
】:線程key的綁定,主要涉及本地線程池的初始化及析構(gòu)访惜;
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()
】:運(yùn)行系統(tǒng)級(jí)別的C++靜態(tài)構(gòu)造函數(shù)
主要是運(yùn)行系統(tǒng)級(jí)別的C++靜態(tài)構(gòu)造函數(shù)嘹履,在dyld調(diào)用我們的靜態(tài)構(gòu)造函數(shù)之前腻扇,libc調(diào)用_objc_init方法,即系統(tǒng)級(jí)別的C++構(gòu)造函數(shù) 先于 自定義的C++構(gòu)造函數(shù) 運(yùn)行砾嫉;
/***********************************************************************
* 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()
】:運(yùn)行時(shí)的初始化 - 運(yùn)行時(shí)的初始化包含兩個(gè)部分:分類與類的初始化幼苛;
void runtime_init(void)
{
objc::unattachedCategories.init(32);
objc::allocatedClasses.init();
}
- 【第五步:
exception_init()
】:初始化libobjc的異常處理系統(tǒng) - 主要是初始化libobjc的異常處理系統(tǒng),注冊(cè)異常處理的回調(diào)焕刮,從而監(jiān)控異常的處理蚓峦,源碼如下:
/***********************************************************************
* exception_init
* Initialize libobjc's exception handling system.
* Called by map_images().
**********************************************************************/
void exception_init(void)
{
old_terminate = std::set_terminate(&_objc_terminate);
}
- 當(dāng)有crash發(fā)生時(shí),會(huì)執(zhí)行
_objc_terminate
函數(shù)济锄,走到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應(yīng)用層會(huì)傳入一個(gè)回調(diào)函數(shù),專門用來處理異常的回調(diào)函數(shù)荐绝,如下所示一汽,其中fn即為傳入的函數(shù),即 uncaught_handler 等于 fn
objc_uncaught_exception_handler
objc_setUncaughtExceptionHandler(objc_uncaught_exception_handler fn)
{
// fn為設(shè)置的異常句柄 傳入的函數(shù)低滩,為外界給的
objc_uncaught_exception_handler result = uncaught_handler;
uncaught_handler = fn; //賦值
return result;
}
crash分類
-
應(yīng)用程序出現(xiàn)crash召夹,主要是因?yàn)槭盏搅宋刺幚淼男盘?hào),信號(hào)主要來源于3個(gè)地方:
-
kernel 內(nèi)核
恕沫; -
其他進(jìn)行
监憎; -
App本身
;
-
-
所以這三種未處理的信號(hào)婶溯,對(duì)應(yīng)了三種類型的crash鲸阔;
-
Mach異常
:是指最底層的內(nèi)核級(jí)異常。用戶態(tài)的開發(fā)者可以直接通過Mach API設(shè)置thread迄委,task褐筛,host的異常端口,來捕獲Mach異常叙身。 -
Unix信號(hào)
:又稱BSD 信號(hào)渔扎,如果開發(fā)者沒有捕獲Mach異常,則會(huì)被host層的方法ux_exception()將Mach異常轉(zhuǎn)換為對(duì)應(yīng)的UNIX信號(hào)信轿,并通過方法threadsignal()將信號(hào)投遞到出錯(cuò)線程晃痴。可以通過方法signal(x, SignalHandler)來捕獲single财忽;Mach異常
與Unix信號(hào)
異常是同一個(gè)異常在不同層級(jí)中處理; -
NSException 應(yīng)用級(jí)異常
:它是未被捕獲的Objective-C異常倘核,導(dǎo)致程序向自身發(fā)送了SIGABRT信號(hào)而崩潰,對(duì)于未捕獲的Objective-C異常定罢,是可以通過try catch來捕獲的笤虫,或者通過NSSetUncaughtExceptionHandler()機(jī)制來捕獲;
-
針對(duì)應(yīng)用級(jí)異常祖凫,可以通過注冊(cè)異常捕獲的函數(shù)琼蚯,即NSSetUncaughtExceptionHandler機(jī)制,實(shí)現(xiàn)線程被菘觯活遭庶,收集上傳崩潰日志;
【第六步:
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()
】:?jiǎn)?dòng)回調(diào)機(jī)制 - 在某些進(jìn)程中渴望加載libobjc-trampolines.dylib稠屠,一些程序(最著名的是嵌入式Chromium的較早版本使用的QtWebEngineProcess)啟用了嚴(yán)格限制的沙盒配置文件峦睡,從而阻止了對(duì)該dylib的訪問,如果有任何調(diào)用imp_implementationWithBlock的操作(如AppKit開始執(zhí)行的操作)权埠,那么我們將在嘗試加載它時(shí)崩潰榨了,將其加載到此處可在啟用沙箱配置文件之前對(duì)其進(jìn)行設(shè)置并阻止它;
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
注冊(cè)回調(diào)函數(shù)_dyld_objc_notify_register()
函數(shù)的聲明與實(shí)現(xiàn)是在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);
從注釋可以看出:
此函數(shù)僅僅提供給objc運(yùn)行時(shí)調(diào)用的攘蔽;
當(dāng)objc的鏡像被mapped龙屉,unmapped,initialized時(shí)满俗,注冊(cè)的回調(diào)函數(shù)會(huì)被調(diào)用转捕;
dyld
將會(huì)通過一個(gè)包含objc-image-info的鏡像文件的數(shù)組
回調(diào)mapped函數(shù)。-
objc調(diào)用_dyld_objc_notify_register()函數(shù)傳入的三個(gè)參數(shù)含義如下:
-
map_images
:dyld將image(鏡像文件)加載進(jìn)內(nèi)存時(shí)唆垃,會(huì)觸發(fā)該函數(shù)五芝; -
load_images
:dyld初始化image會(huì)觸發(fā)該函數(shù); -
unmap_image
:dyld將image移除時(shí)辕万,會(huì)觸發(fā)該函數(shù)枢步;
-
Dyld與Objc之間的關(guān)聯(lián)
- Dyld中
_dyld_objc_notify_register()
函數(shù)實(shí)現(xiàn)如下:
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);
}
-
根據(jù)在objc中調(diào)用傳入的參數(shù),存在下面的對(duì)應(yīng)關(guān)系:
-
mapped
等價(jià)于map_images
-
init
等價(jià)于load_images
-
unmapped
等價(jià)于unmap_image
-
內(nèi)部調(diào)用了
registerObjCNotifiers()
渐尿,實(shí)現(xiàn)如下:
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());
}
}
}
- 所以存在下面的等價(jià)關(guān)系:
- sNotifyObjCMapped == mapped == map_images
- sNotifyObjCInit == init == load_images
- sNotifyObjCUnmapped == unmapped == unmap_image
map_images回調(diào)函數(shù)的調(diào)用時(shí)機(jī)
- 在dyld源碼中全局搜索
sNotifyObjCMapped
价捧,發(fā)現(xiàn)sNotifyObjCMapped
是在notifyBatchPartial
中被調(diào)用的,再全局搜索notifyBatchPartial
發(fā)現(xiàn)是在registerObjCNotifiers
中調(diào)用的涡戳;
load_images回調(diào)函數(shù)的調(diào)用時(shí)機(jī)
-
load_images
的調(diào)用時(shí)機(jī)在 iOS App運(yùn)行加載中已經(jīng)詳細(xì)闡述了结蟋,是在第二次notifySingle()
中調(diào)用的,所以map_images
要先于load_images
調(diào)用渔彰;
unmap_image回調(diào)函數(shù)的調(diào)用時(shí)機(jī)
- 在dyld源碼中全局搜索
sNotifyObjCUnmapped
嵌屎,發(fā)現(xiàn)sNotifyObjCUnmapped
是在removeImage()
中被調(diào)用的,即移除image鏡像時(shí)恍涂,調(diào)用unmap_image
回調(diào)函數(shù)宝惰;
總結(jié)
- Mach-O文件從Dyld的動(dòng)態(tài)鏈接加載,到Objc將所有文件的加載進(jìn)入內(nèi)存再沧,調(diào)用類與分類的load方法尼夺,最后到Application的Main函數(shù)入口,其整體的流程如下圖所示:
- objc調(diào)用
_dyld_objc_notify_register(&map_images, load_images, unmap_image)
函數(shù),然后dyld接收到這三個(gè)方法參數(shù)dyld::registerObjCNotifiers(mapped, init, unmapped)
淤堵,并用全局變量保存寝衫,對(duì)應(yīng)關(guān)系如下:-
map_images
<==>sNotifyObjCMapped
-
load_images
<==>sNotifyObjCInit
-
unmap_image
<==>sNotifyObjCUnmapped
- 最后dyld在合適的時(shí)機(jī),分別調(diào)用這三個(gè)函數(shù)方法拐邪;
-