这是我参与8月更文挑战的第23天,活动详情查看:8月更文挑战
四、反推objc与dyld的关联
在上面的符号断点过程中可以看到在_dyld_objc_notify_register与doModInitFunctions之间还有非dyld的库。
在_objc_init中打个断点有如下调用栈:
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 4.1
* frame #0: 0x00000001002d2d44 libobjc.A.dylib`_objc_init at objc-os.mm:925:9
frame #1: 0x000000010044b0bc libdispatch.dylib`_os_object_init + 13
frame #2: 0x000000010045bafc libdispatch.dylib`libdispatch_init + 282
frame #3: 0x00007fff69543791 libSystem.B.dylib`libSystem_initializer + 220
frame #4: 0x000000010002f1d3 dyld`ImageLoaderMachO::doModInitFunctions(ImageLoader::LinkContext const&) + 535
frame #5: 0x000000010002f5de dyld`ImageLoaderMachO::doInitialization(ImageLoader::LinkContext const&) + 40
frame #6: 0x0000000100029ffb dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 493
frame #7: 0x0000000100029f66 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 344
frame #8: 0x00000001000280b4 dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 188
frame #9: 0x0000000100028154 dyld`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 82
frame #10: 0x0000000100016662 dyld`dyld::initializeMainExecutable() + 129
frame #11: 0x000000010001bbba dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 6667
frame #12: 0x0000000100015227 dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) + 453
frame #13: 0x0000000100015025 dyld`_dyld_start + 37
对于
doModInitFunctions后面的流程是未知的。从doModInitFunctions->_objc_init流程是未知的。那么最好的方式就是从_objc_init反推调用到它的整个流程。
4.1 _os_object_init
_objc_init是被_os_object_init调用的,这个函数在libdispatch.dylib中。下载libdispatch最新源码1271.120.2直接搜索_os_object_init:
void
_os_object_init(void)
{
//_objc_init调用
_objc_init();
Block_callbacks_RR callbacks = {
sizeof(Block_callbacks_RR),
(void (*)(const void *))&objc_retain,
(void (*)(const void *))&objc_release,
(void (*)(const void *))&_os_objc_destructInstance
};
_Block_use_RR2(&callbacks);
#if DISPATCH_COCOA_COMPAT
const char *v = getenv("OBJC_DEBUG_MISSING_POOLS");
if (v) _os_object_debug_missing_pools = _dispatch_parse_bool(v);
v = getenv("DISPATCH_DEBUG_MISSING_POOLS");
if (v) _os_object_debug_missing_pools = _dispatch_parse_bool(v);
v = getenv("LIBDISPATCH_DEBUG_MISSING_POOLS");
if (v) _os_object_debug_missing_pools = _dispatch_parse_bool(v);
#endif
}
发现确实是在_os_object_init中直接调用了_objc_init()。
接着在libdispatch_init中发现了_os_object_init的调用:
- 其中进行了
TLS键值处理以及线程处理。
4.2 libSystem_initializer
libSystem_initializer是在libSystem库中,下载libSystem最新源码1292.120.1。
同样直接搜索libSystem_initializer:
- 其中直接调用了
libdispatch_init,同样还调用了__malloc_init、_dyld_initializer以及_libtrace_init。libSystem_initializer是ImageLoaderMachO::doModInitFunctions调用的,这样整个流程就回到了dyld中。整个流程就串起来了。 在doModInitFunctions中发现了如下代码:
libSystem库必须第一个被初始化。这也能被理解,因为要初始化dispatch以及objc。其它image都依赖它。func是对c++构造函数的调用。 那么libSystem_initializer是在哪里调用的呢?在doModInitFunctions中并没有看到libSystem_initializer的调用。但是断点读取确实读取到了:
前面已经分析过了
doModInitFunctions中是对c++构造函数的调用。libSystem_initializer正好是c++构造函数:
这样整个流程就通了。只不过
libSystem_initializer这个c++构造函数被先调用。
libSystem的c++构造函数在dyld、libobjc、Foundation的c++构造函数之后,主程序之前执行。
五、 dyld注册objc回调简单分析
通过上面的分析在_objc_init中调用了_dyld_objc_notify_register进行回调注册,有如下赋值:
//第一个参数 map_images
sNotifyObjCMapped = mapped;
//第二个参数 load_images
sNotifyObjCInit = init;
//第三个参数 unmap_image
sNotifyObjCUnmapped = unmapped;
接下来将详细分析这3个回调的逻辑。
5.1 sNotifyObjCMapped(map_images)
sNotifyObjCMapped在dyld中的调用只在notifyBatchPartial中:
而
notifyBatchPartial的调用是在registerObjCNotifiers、registerImageStateBatchChangeHandler、以及notifyBatch中。那么根据之前的分析这里的调用就是registerObjCNotifiers注册回调后就在里面调用了。
在objc源码map_images中打个断点:
可以验证在注册回调后立马调用了
map_images。
map_images中直接加锁调用了map_images_nolock,其中进行了类的加载相关的操作。这块逻辑将单独写篇文章进行分析。
5.2 sNotifyObjCInit(load_images)
sNotifyObjCInit在dyld中的调用分为以下情况:
- 1.
notifySingleFromCache中。 - 2.
notifySingle中。 - 3.
registerObjCNotifiers。notifySingleFromCache与notifySingle逻辑基本相同,无非就是有没有缓存的区别。registerObjCNotifiers是在注册回调函数的时候直接进行的回调。直接在load_images中打个断点可以跟踪到如下信息:
可以看到系统的基础库在注册回调后就马上进行了
load_images的调用。
而对于其他库是通过notifySingle走的回调逻辑:
5.2.1 load_images(objc-runtime-new.mm)
sNotifyObjCInit其实就是load_images,它的实现如下:
void
load_images(const char *path __unused, const struct mach_header *mh)
{
if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
didInitialAttachCategories = true;
//加载所有分类
loadAllCategories();
}
// Return without taking locks if there are no +load methods here.
if (!hasLoadMethods((const headerType *)mh)) return;
recursive_mutex_locker_t lock(loadMethodLock);
// Discover load methods
{
mutex_locker_t lock2(runtimeLock);
//准备所有load方法
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
//调用 + load方法
call_load_methods();
}
- 加载所有分类。
- 准备所有
load方法。 - 最终调用了
call_load_methods。 prepare_load_methods
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
runtimeLock.assertLocked();
classref_t const *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
//添加主类的load方法
schedule_class_load(remapClass(classlist[i]));
}
category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
//分类准备好
for (i = 0; i < count; i++) {
category_t *cat = categorylist[i];
Class cls = remapClass(cat->cls);
if (!cls) continue; // category for ignored weak-linked class
if (cls->isSwiftStable()) {
_objc_fatal("Swift class extensions and categories on Swift "
"classes are not allowed to have +load methods");
}
//实现类
realizeClassWithoutSwift(cls, nil);
ASSERT(cls->ISA()->isRealized());
//添加分类的load方法。
add_category_to_loadable_list(cat);
}
}
- 添加主类的
load方法。 - 添加分类的
load方法。 schedule_class_load
static void schedule_class_load(Class cls)
{
if (!cls) return;
ASSERT(cls->isRealized()); // _read_images should realize
if (cls->data()->flags & RW_LOADED) return;
// Ensure superclass-first ordering
//调度类的load方法,递归到nil
schedule_class_load(cls->getSuperclass());
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
- 递归调度类的
load方法,直到父类为nil。
add_class_to_loadable_list & add_category_to_loadable_list
void add_class_to_loadable_list(Class cls)
{
IMP method;
loadMethodLock.assertLocked();
//load方法
method = cls->getLoadMethod();
if (!method) return; // Don't bother if cls has no +load method
if (PrintLoading) {
_objc_inform("LOAD: class '%s' scheduled for +load",
cls->nameForLogging());
}
//空间不足开辟空间
if (loadable_classes_used == loadable_classes_allocated) {
loadable_classes_allocated = loadable_classes_allocated*2 + 16;
loadable_classes = (struct loadable_class *)
realloc(loadable_classes,
loadable_classes_allocated *
sizeof(struct loadable_class));
}
//将load方法添加到loadable_classes中。相当于一个下标中存储的是cls-method
loadable_classes[loadable_classes_used].cls = cls;
loadable_classes[loadable_classes_used].method = method;
loadable_classes_used++;
}
void add_category_to_loadable_list(Category cat)
{
IMP method;
loadMethodLock.assertLocked();
//获取load方法
method = _category_getLoadMethod(cat);
// Don't bother if cat has no +load method
if (!method) return;
if (PrintLoading) {
_objc_inform("LOAD: category '%s(%s)' scheduled for +load",
_category_getClassName(cat), _category_getName(cat));
}
if (loadable_categories_used == loadable_categories_allocated) {
loadable_categories_allocated = loadable_categories_allocated*2 + 16;
loadable_categories = (struct loadable_category *)
realloc(loadable_categories,
loadable_categories_allocated *
sizeof(struct loadable_category));
}
//分类添加到loadable_categories中
loadable_categories[loadable_categories_used].cat = cat;
loadable_categories[loadable_categories_used].method = method;
loadable_categories_used++;
}
- 通过字符出那比较获取
load方法。 - 空间不足的情况下开辟空间吗,每次开辟的空间大小为(
2倍+16)* 16 字节。
struct loadable_class {
Class cls; // may be nil
IMP method;
};
- 将对应的数据添加进
loadable_classes与loadable_categories中。 ⚠️加载过程中类和分类是有区分的。为什么区分将在后续的文章中详细分析。 getLoadMethod
IMP
objc_class::getLoadMethod()
{
runtimeLock.assertLocked();
const method_list_t *mlist;
//递归所有的baseMethods,查找load方法。
mlist = ISA()->data()->ro()->baseMethods();
if (mlist) {
for (const auto& meth : *mlist) {
const char *name = sel_cname(meth.name());
//匹配load
if (0 == strcmp(name, "load")) {
return meth.imp(false);
}
}
}
return nil;
}
IMP
_category_getLoadMethod(Category cat)
{
runtimeLock.assertLocked();
const method_list_t *mlist;
mlist = cat->classMethods;
if (mlist) {
for (const auto& meth : *mlist) {
const char *name = sel_cname(meth.name());
if (0 == strcmp(name, "load")) {
return meth.imp(false);
}
}
}
return nil;
}
load方法获取是通过字符出那比较获取的。
5.2.2 call_load_methods (objc-loadmethod.mm)
void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;
loadMethodLock.assertLocked();
// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;
void *pool = objc_autoreleasePoolPush();
//循环调用 call_class_loads,类的load方法在这一刻被调用
do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
//调用每个类的load
call_class_loads();
}
// 2. Call category +loads ONCE
//调用分类load,这里也就说明分类的 load 在所有类的load方法调用后才调用。(针对image而言)
more_categories = call_category_loads();
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
- 调用
call_class_loads加载类的+ load。 - 接着调用
call_category_loads加载分类的+ load。这里也就说明分类的load在所有类的load方法调用后才调用。(针对image而言)。 在这里也就调用到了+ load方法,这也就是+ load在main之前被调用的原因。 call_class_loads
static void call_class_loads(void)
{
int i;
// Detach current loadable list.
struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
//清空值
loadable_classes_used = 0;
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
//从classes中获取method
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;
if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
//调用load
(*load_method)(cls, @selector(load));
}
// Destroy the detached list.
if (classes) free(classes);
}
- 内部也是从
loadable_classes中循环取到load方法进行调用。 call_category_loads
static bool call_category_loads(void)
{
int i, shift;
bool new_categories_added = NO;
// Detach current loadable list.
struct loadable_category *cats = loadable_categories;
int used = loadable_categories_used;
int allocated = loadable_categories_allocated;
loadable_categories = nil;
loadable_categories_allocated = 0;
loadable_categories_used = 0;
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Category cat = cats[i].cat;
//从cats中取出load
load_method_t load_method = (load_method_t)cats[i].method;
Class cls;
if (!cat) continue;
cls = _category_getClass(cat);
if (cls && cls->isLoadable()) {
if (PrintLoading) {
_objc_inform("LOAD: +[%s(%s) load]\n",
cls->nameForLogging(),
_category_getName(cat));
}
(*load_method)(cls, @selector(load));
cats[i].cat = nil;
}
}
……
}
- 分类
load的调用也是从loadable_categories循环取load方法进行调用。分类中内部处理逻辑更多一些。 所以在调用完+ load以及c++构造函数才返回LC_MAIN进行main函数的调用。可以通过汇编断点验证:
这样就和开头的时候对应上了。那么如果修改
main函数的名称,编译的时候就报错了。主程序的入口main是写死的,可以通过Hook去操作main隐藏自己的逻辑。
根据以上分析可以看到dyld是按image list顺序从第1个image调用runInitializers(可以看做是以image分组)。再调用下一个image的runInitializers最后再调用主程序(下标为0)的runInitializers。在runInitializers内部先调用所有类的+load,再调用所有分类的+ load,最后调用c++的构造函数。
objc中调用load,dyld中调用doModInitFunctions。
⚠️如果在+ load中做了防护,那么可以通过在+ load执行前断住外部符号做处理。这样就可以绕过防护了。
防护最重要的就是不让别人找到防护的逻辑,只要能找到那么破解就很容易了。
5.3 sNotifyObjCUnmapped(unmap_image)
sNotifyObjCUnmapped在dyld中只有removeImage进行了调用:
removeImage被checkandAddImage、garbageCollectImages、_dyld_link_module调用。
garbageCollectImages:在link等其它异常以及回收的时候调用。checkandAddImage:检测加载的image不在镜像列表中的时候直接删除。_dyld_link_module:暂时不确定是哪里调用的。
5.3.1 unmap_image
unmap_image中调用了unmap_image_nolock,核心代码如下:
void
unmap_image_nolock(const struct mach_header *mh)
{
……
header_info *hi;
……
//释放类,分类相关资源。
_unload_image(hi);
// Remove header_info from header list
//移除remove Header
removeHeader(hi);
free(hi);
}
- 移除释放类,分类相关资源。
- 移除
Header信息。
六 、dyld3闭包模式分析
关于闭包模式在开启闭包模式的情况下就直接return了,所以核心逻辑就在launchWithClosure中了:
static bool launchWithClosure(const dyld3::closure::LaunchClosure* mainClosure,
const DyldSharedCache* dyldCache,
const dyld3::MachOLoaded* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[], Diagnostics& diag,
uintptr_t* entry, uintptr_t* startGlue, bool* closureOutOfDate, bool* recoverable)
{
……
libDyldEntry->runInitialzersBottomUp((mach_header*)mainExecutableMH);
……
}
在launchWithClosure中发现了runInitialzersBottomUp的调用:
void AllImages::runInitialzersBottomUp(const closure::Image* topImage)
{
// walk closure specified initializer list, already ordered bottom up
topImage->forEachImageToInitBefore(^(closure::ImageNum imageToInit, bool& stop) {
// get copy of LoadedImage about imageToInit, but don't keep reference into _loadedImages, because it may move if initialzers call dlopen()
uint32_t indexHint = 0;
LoadedImage loadedImageCopy = findImageNum(imageToInit, indexHint);
// skip if the image is already inited, or in process of being inited (dependency cycle)
if ( (loadedImageCopy.state() == LoadedImage::State::fixedUp) && swapImageState(imageToInit, indexHint, LoadedImage::State::fixedUp, LoadedImage::State::beingInited) ) {
// tell objc to run any +load methods in image
if ( (_objcNotifyInit != nullptr) && loadedImageCopy.image()->mayHavePlusLoads() ) {
dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)loadedImageCopy.loadedAddress(), 0, 0);
const char* path = imagePath(loadedImageCopy.image());
log_notifications("dyld: objc-init-notifier called with mh=%p, path=%s\n", loadedImageCopy.loadedAddress(), path);
//+load
(*_objcNotifyInit)(path, loadedImageCopy.loadedAddress());
}
// run all initializers in image
//c++构造函数
runAllInitializersInImage(loadedImageCopy.image(), loadedImageCopy.loadedAddress());
// advance state to inited
swapImageState(imageToInit, indexHint, LoadedImage::State::beingInited, LoadedImage::State::inited);
}
});
}
_objcNotifyInit最终调用到了+ load方法。runAllInitializersInImage调用c++构造函数,其中包括注册回调。
void AllImages::runAllInitializersInImage(const closure::Image* image, const MachOLoaded* ml)
{
image->forEachInitializer(ml, ^(const void* func) {
Initializer initFunc = (Initializer)func;
#if __has_feature(ptrauth_calls)
initFunc = (Initializer)__builtin_ptrauth_sign_unauthenticated((void*)initFunc, 0, 0);
#endif
{
ScopedTimer(DBG_DYLD_TIMING_STATIC_INITIALIZER, (uint64_t)ml, (uint64_t)func, 0);
//c++构造函数
initFunc(NXArgc, NXArgv, environ, appleParams, _programVars);
}
log_initializers("dyld: called initialzer %p in %s\n", initFunc, image->path());
});
}
在真机/模拟器调试中对_dyld_objc_notify_register下符号断点发现_dyld_objc_notify_register()的注册回调是dyld3::_dyld_objc_notify_register调用的:
但是最终的回调以及调用方确是
dyld2的逻辑。看下源码:
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped)
{
if ( gUseDyld3 )
return dyld3::_dyld_objc_notify_register(mapped, init, unmapped);
DYLD_LOCK_THIS_BLOCK;
typedef bool (*funcType)(_dyld_objc_notify_mapped, _dyld_objc_notify_init, _dyld_objc_notify_unmapped);
static funcType __ptrauth_dyld_function_ptr p = NULL;
if(p == NULL)
dyld_func_lookup_and_resign("__dyld_objc_notify_register", &p);
p(mapped, init, unmapped);
}
那么就说明gUseDyld3为NULL,走了dyld2的逻辑。但是如果走dyld3可以得到以下信息,注册的三个回调函数指针与dyld2名称不同:
_objcNotifyMapped = map;
_objcNotifyInit = init;
_objcNotifyUnmapped = unmap;
_objcNotifyInit已经清楚了是在runInitialzersBottomUp中调用的。_objcNotifyUnmapped是在garbageCollectImages ->removeImages中调用的。_objcNotifyMapped是在runImageCallbacks中调用的,它有两个调用方applyInitialImages以及loadImage。applyInitialImages是被_dyld_initializer调用的。_dyld_initializer在第四部分已经明确了是在libSystem_initializer中调用的。而由于_dyld_initializer是在libdispatch_init之前调用的,所以这个时候应该还没有注册回调。loadImage是在dlopen中调用的。
由于真机和模拟器以及mac都没有办法进入闭包模式调试验证。并且闭包模式代码逻辑可读性比较差,所以这里只是根据源码得出的结论,不一定成立。