一门语言的开发入门,总是抬手就能整出一个「Hello World Demo」。比如下面这样:
显然,熟悉 iOS 开发的同学都知道,上面这个来自 Objective-C。
今天,我们就从这熟悉的代码入手,来一起研究研究「Hello World」出世的整个过程。
main
函数
众说周知,main
函数是我们程序的入口,我们不妨从此入手,开始我们的表演。
入手的姿势已经确定,甩手一个断点,拿到下图:
显然,在main
函数执行之前,先是调用了start
方法,那这个所谓的start
方法又是什么呢?哪里来的呢?又是怎么调起来的呢?
从上图我们并不看的很真切,因+ (void)load {}
方法的调用是在加载阶段(后面验证),而加载完之后才触发main
函数的调用,所以我们在load
方法时候再加上一个断点(这里可以随便弄个类,重写load
方法),看看究竟。
+load
方法
再次运行代码之后很容易先确认前面提到的「load 方法调用在 main 调用前」
,并拿到下面的调用堆栈(bt
命令可打印更详细的堆栈信息):
从上图我们可以清楚的看到一切的开始源于一堆dyld
的东西,那么,dyld
是个啥?
dyld
(全名 the dynamic link editor)是苹果的动态链接器,用来链接所有的库和可执行文件,是苹果操作系统一个重要组成部分,在系统内核做好程序准备工作之后,交由dyld
负责余下的工作。它的代码也是开源的,正常可以从这里下载
!!注意:网上有很多关于
dyld
执行流程的介绍,但都是基于稍老的一些版本,可以看到上面的堆栈信息与老版本的也有些许差异,但是总体流程上基本一致,这里介绍基于最新的dyld4
版本,源码可从这里下载
我们接着说,在dyld
做完加载库、可执行文件等一系列准备工作之后,通过dyld4::RuntimeState::notifyObjCInit
触发libobjc.A.dylib
中的load_images
函数,再到我们自定义的[Person load]
方法的调用,最终到在之后的main
函数。
此话怎么讲呢?iOS开发者对notify
这样的字眼是不是有些熟悉?对的,通知!还是通知的触发,上面的过程很显然就是dyld
触发通知到注册通知的接收方,哪里呢?libobjc.A.dylib
中的load_images
函数,这个库其实就是我们常念叨的Runtime
。Runtime
的代码也是在官方的开源库中,所以我们接下来可以直接验证一下我们的猜想。Runtime
源码这里下载
这里额外说明一下,
Runtime
源码是可以运行起来的,当然需要一堆配置,这里提供一个可以直接跑起来的源码,在这里,下载下来之后,Target 选择KCObjcBuild
就可以,源码中直接Debug
,不要太爽。
load_images
函数
哦了,接着说,我们怎么验证呢?直接在Runtime
源码中直接搜索load_images
,很容易定位到下面这里:
显然是load_images
定义的地方,直接甩一断点验证看看,(源码直接编译优势凸显):
这里调用的堆栈信息,很显然符合我们的猜想。
继续,是通知就该有注册的地方,不然怎么就能调用到上面的load_images
函数呢?前面搜索load_images
的时候其实就有这么个地儿:
上图中我们看到了什么呢?是的,_dyld_objc_notify_register
,盲猜一下,应该就是通知的注册地了吧。
既然如此,我们知道要想实现上面的调用过程,那么,通知的注册应该要在调用触发之前,不然可说不过去,来来来,接着验证,甩手一个断点(就问源码直接debug爽还是爽?):
果然如此吧,甚至上面的堆栈信息直接能看出来_objc_init
的调用过程,算是意外之喜了。
_objc_init
函数
_objc_init
的调用过程,从上面的堆栈信息可以看到:
-> dyld`start
-> dyld`dyld4::prepare()
-> dyld`dyld4::APIs::runAllInitializersForMain()
-> dyld`dyld4::Loader::findAndRunAllInitializers()
-> dyld`dyld3::MachOAnalyzer::forEachInitializer()
-> dyld`dyld3::MachOFile::forEachSection() const
-> dyld`dyld3::MachOFile::forEachLoadCommand()
-> dyld3::MachOFile::forEachSection()
-> dyld3::MachOAnalyzer::forEachInitializer()
-> dyld4::Loader::findAndRunAllInitializers()
-> libSystem.B.dylib`libSystem_initializer
-> libdispatch.dylib`libdispatch_init
-> libdispatch.dylib`_os_object_init
-> libobjc.A.dylib`_objc_init
dyld
相关的函数在dyld
开源库中,前面已经提到了,这里。
libSystem.B.dylib
的相关代码在官方的开源库中能直接拿到,这里。
libdispatch.dylib
相关的代码也在官方的开源库中能查询到,这里。
而libobjc.A.dylib
就是上面提到的Runtime
代码了,这里
运行轨迹
截止目前,我们大致了解了main
函数调用的基本逻辑:
dyld
的start
开始 -> _objc_init
函数加载(注册了load_images
) -> 触发load_images
函数 -> 触发+load
方法 -> 在最后才调用main
函数 -> 最终输出Hello World
可见,在main
函数调用之前,确实还是有一大堆我们并没有意识到的操作,大部分是dyld
在处理,我们姑且称之为–加载过程。
但是main
函数究竟是怎么被唤起的呢?目前看着仍旧不是很明朗,还是要继续撸源码,那块儿的源码呢?从前面的堆栈来看,还是要从dyld
的start
跟prepare
入手。
dyld
唤起main
函数
我们可以在dyld
源码中全局搜索prepare(
,最终找到能这个地方:
而这里正好是start
函数的内部调用,符合我们前面的堆栈信息,地方应该可以确认了,没跑了。
其实从这里我们就可以在此大胆假设,然后去小心求证了:
// load all dependents of program and bind them together
MainFunc appMain = prepare(state, dyldMA);
// now make all dyld Allocated data structures read-only
state.decWritable();
// call main() and if it returns, call exit() with the result
// Note: this is organized so that a backtrace in a program's main thread shows just "start" below "main"
int result = appMain(state.config.process.argc, state.config.process.argv, state.config.process.envp, state.config.process.apple);
从上面的命名及注释,很容易看到:prepare()
主要就是处理main
函数调用前的准备工作,比如加载所有的dependents
,最终返回的就是main
函数的入口(MainFunc
类型),而后的appMain()
实际上就是对main
函数的调用了,我们可以看到其参数就跟我们main
的参数神似了。
上面的这些,我们如果深入prepare()
就会进一步得到验证,截取部分代码如下:
...
// run all initializers
state.runAllInitializersForMain();
// notify we are about to call main
notifyMonitoringDyldMain();
if ( dyld3::kdebug_trace_dyld_enabled(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE) ) {
dyld3::kdebug_trace_dyld_duration_end(launchTraceID, DBG_DYLD_TIMING_LAUNCH_EXECUTABLE, 0, 0, 4);
}
ARIADNEDBG_CODE(220, 1);
MainFunc result;
if ( state.config.security.skipMain ) {
return &fake_main;
}
else if ( state.config.process.platform == dyld3::Platform::driverKit ) {
result = state.mainFunc();
if ( result == 0 )
halt("DriverKit main entry point not set");
#if __has_feature(ptrauth_calls)
// HACK: DriverKit signs the pointer with a diversity different than dyld expects when calling the pointer.
result = (MainFunc)__builtin_ptrauth_strip((void*)result, ptrauth_key_function_pointer);
result = (MainFunc)__builtin_ptrauth_sign_unauthenticated((void*)result, 0, 0);
#endif
}
else {
// find entry point for main executable
uint64_t entryOffset;
bool usesCRT;
if ( !state.config.process.mainExecutable->getEntry(entryOffset, usesCRT) )
halt("main executable has no entry point");
result = (MainFunc)((uintptr_t)state.config.process.mainExecutable + entryOffset);
if ( usesCRT ) {
// main executable uses LC_UNIXTHREAD, dyld needs to cut back kernel arg stack and jump to "start"
#if SUPPPORT_PRE_LC_MAIN
// backsolve for KernelArgs (original stack entry point in _dyld_start)
const KernelArgs* kernArgs = (KernelArgs*)(&state.config.process.argv[-2]);
gotoAppStart((uintptr_t)result, kernArgs);
#else
halt("main executable is missing LC_MAIN");
#endif
}
#if __has_feature(ptrauth_calls)
result = (MainFunc)__builtin_ptrauth_sign_unauthenticated((void*)result, 0, 0);
#endif
}
runAllInitializersForMain()
也能在前面的堆栈中得以体现,函数名也很直观,为main
函数的调用,做所有的初始化工作。
而下面的result
赋值的过程,实际上就是main
函数入口查找的过程。
总结
综上所述,main
函数调用前的过程我们更清晰了,一切的开始是从dyld
的start
函数开始,其通过prepare()
函数做好了main
函数调用前的所有准备工作,如初始化、链接所有的动态库及可执行文件等,查找main
函数的入口并返回到start
函数后,实现main
函数的调用触发。
当然,这中间实际上还有一些模糊的地方,比如_objc_init
里面具体干了啥,load_images
有什么用,以及上面的appMain
就是我们最开始截图里面的main
方法吗?(显然不是,参数数量对不上,哈哈,中间还有一些过程)。
有兴趣的小伙伴可以继续研究研究,后续我们再继续讨论。
Python 的迅速崛起对整个行业来说都是极其有利的 ,但“人红是非多
”,导致它平添了许许多多的批评,不过依旧挡不住它火爆的发展势头。
如果你对Python感兴趣,想要学习python,这里给大家分享一份Python全套学习资料,都是我自己学习时整理的,希望可以帮到你,一起加油!
😝有需要的小伙伴,可以点击下方链接免费领取或者V扫描下方二维码免费领取🆓
Python全套学习资料
1️⃣零基础入门
① 学习路线
对于从来没有接触过Python的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,你可以按照上面的知识点去找对应的学习资源,保证自己学得较为全面。
② 路线对应学习视频
还有很多适合0基础入门的学习视频,有了这些视频,轻轻松松上手Python~
③练习题
每节视频课后,都有对应的练习题哦,可以检验学习成果哈哈!
2️⃣国内外Python书籍、文档
① 文档和书籍资料
3️⃣Python工具包+项目源码合集
①Python工具包
学习Python常用的开发软件都在这里了!每个都有详细的安装教程,保证你可以安装成功哦!
②Python实战案例
光学理论是没用的,要学会跟着一起敲代码,动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。100+实战案例源码等你来拿!
③Python小游戏源码
如果觉得上面的实战案例有点枯燥,可以试试自己用Python编写小游戏,让你的学习过程中增添一点趣味!
4️⃣Python面试题
我们学会了Python之后,有了技能就可以出去找工作啦!下面这些面试题是都来自阿里、腾讯、字节等一线互联网大厂,并且有阿里大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。
5️⃣Python兼职渠道
而且学会Python以后,还可以在各大兼职平台接单赚钱,各种兼职渠道+兼职注意事项+如何和客户沟通,我都整理成文档了。
上述所有资料 ⚡️ ,朋友们如果有需要的,可以扫描下方👇👇👇二维码免费领取🆓