跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Objective-C

Hello World 背后的启动逻辑

综述由AI生成深入解析了 iOS 应用中 Hello World 程序的启动流程。从 main 函数入手,揭示了 dyld 动态链接器在程序启动中的核心作用,包括 start 函数、load_images 回调、+load 方法调用顺序以及 objc_init 初始化过程。文章详细阐述了 dyld 的映射、绑定和初始化三个阶段,并补充了相关源码分析,帮助开发者理解应用启动背后的底层逻辑,为性能优化和问题排查提供理论支持。

AiEngineer发布于 2025/2/6更新于 2026/6/1121 浏览
Hello World 背后的启动逻辑

一门语言的开发入门,总是抬手就能整出一个「Hello World Demo」。比如下面这样:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"Hello, World!");
    }
    return 0;
}

显然,熟悉 iOS 开发的同学都知道,上面这个来自 Objective-C。今天,我们就从这熟悉的代码入手,来一起研究一下「Hello World」出世的整个过程。

main 函数

众所周知,main 函数是我们程序的入口,我们不妨从此入手,开始我们的表演。

入手的姿势已经确定,甩手一个断点,拿到下图所示的调用堆栈信息:

![加载过程堆栈截图]

显然,在 main 函数执行之前,先是调用了 start 方法。那这个所谓的 start 方法又是什么呢?哪里来的呢?又是怎么调起来的呢?

从上图我们看得并不很真切,因为 + (void)load {} 方法的调用是在加载阶段(后面验证),而加载完之后才触发 main 函数的调用。所以我们在 load 方法上再加上一个断点(这里可以随便弄个类,重写 load 方法),看看究竟。

+load 方法

再次运行代码之后很容易先确认前面提到的「load 方法调用在 main 调用前」,并拿到下面的调用堆栈(bt 命令可打印更详细的堆栈信息):

![load 方法调用堆栈]

从上图我们可以清楚地看到一切的开始源于一堆 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 源码是可以运行起来的,当然需要一堆配置,下载下来之后,Target 选择 KCObjcBuild 就可以,源码中直接 Debug,体验非常流畅。

load_images 函数

哦了,接着说,我们怎么验证呢?直接在 Runtime 源码中直接搜索 load_images,很容易定位到下面这里:

![load_images 定义位置]

显然是 load_images 定义的地方,直接甩一断点验证看看(源码直接编译优势凸显):

![load_images 断点验证]

这里调用的堆栈信息,很显然符合我们的猜想。

继续,是通知就该有注册的地方,不然怎么就能调用到上面的 load_images 函数呢?前面搜索 load_images 的时候其实就有这么个地儿:

![通知注册位置]

上图中我们看到了什么呢?是的,_dyld_objc_notify_register,盲猜一下,应该就是通知的注册地了吧。

既然如此,我们知道要想实现上面的调用过程,那么,通知的注册应该要在调用触发之前,不然可说不过去。来来来,接着验证,甩手一个断点:

![通知注册断点]

果然如此吧,甚至上面的堆栈信息直接能看出来 _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::forEachSection() 
dyld`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 函数调用的基本逻辑:

  1. dyld 的 start 开始
  2. _objc_init 函数加载(注册了 load_images)
  3. 触发 load_images 函数
  4. 触发 +load 方法
  5. 在最后才调用 main 函数
  6. 最终输出 Hello World

可见,在 main 函数调用之前,确实还是有一大堆我们并没有意识到的操作,大部分是 dyld 在处理,我们姑且称之为加载过程。

但是 main 函数究竟是怎么被唤起的呢?目前看着仍旧不是很明朗,还是要继续撸源码。那块儿的源码呢?从前面的堆栈来看,还是要从 dyld 的 start 跟 prepare 入手。

dyld 唤起 main 函数

我们可以在 dyld 源码中全局搜索 prepare(,最终找到这个地方:

![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 函数入口查找的过程。

Dyld 详细工作流程补充

为了更深入理解上述流程,我们需要了解 dyld 的具体工作阶段。通常分为以下三个主要步骤:

  1. Mapping(映射):将可执行文件和依赖的动态库映射到内存中。这一步由内核协助完成,dyld 负责解析 Mach-O 文件格式,计算所需的内存空间。
  2. Binding(绑定):解析符号引用,将未绑定的符号指向具体的内存地址。这是动态链接的核心,确保程序运行时能找到所有需要的函数和变量。
  3. Initialization(初始化):执行构造函数、静态对象初始化以及 Objective-C 的 +load 方法等。这一步确保了程序在 main 函数执行前,所有必要的资源都已就绪。

理解这三个阶段对于性能优化至关重要。例如,减少动态库的依赖数量可以缩短 Mapping 时间;避免复杂的符号重定位可以减少 Binding 耗时。

总结

综上所述,main 函数调用前的过程我们更清晰了。一切的开始是从 dyld 的 start 函数开始,其通过 prepare() 函数做好了 main 函数调用前的所有准备工作,如初始化、链接所有的动态库及可执行文件等,查找 main 函数的入口并返回到 start 函数后,实现 main 函数的调用触发。

当然,这中间实际上还有一些模糊的地方,比如 _objc_init 里面具体干了啥,load_images 有什么用,以及上面的 appMain 就是我们最开始截图里面的 main 方法吗?(显然不是,参数数量对不上,哈哈,中间还有一些过程)。

有兴趣的小伙伴可以继续研究研究,后续我们再继续讨论。

常见问题与调试技巧

在实际开发中,了解启动流程有助于解决一些疑难杂症:

  • 启动慢:检查 dyld 日志,查看是否有大量的符号绑定或库加载延迟。
  • 崩溃在 main 之前:检查 +load 方法中是否有非法操作,或者 DYLD_LIBRARY_PATH 环境变量是否配置错误。
  • 符号丢失:确保所有依赖库都正确打包进 App Bundle,并且架构匹配。

掌握这些底层机制,能让你的 iOS 开发之路走得更稳更远。

目录

  1. main 函数
  2. +load 方法
  3. load_images 函数
  4. objcinit 函数
  5. 运行轨迹
  6. dyld 唤起 main 函数
  7. Dyld 详细工作流程补充
  8. 总结
  9. 常见问题与调试技巧
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • Python 核心语法实战:从变量到文件操作一站式指南
  • 3 种方法快速判断 Ubuntu 系统 ARM 或 x86 架构
  • 4GB 显存限制下构建 LLM 基础开发环境指南
  • Whisper Turbo:支持 99 种语言的极速语音识别模型
  • SkyWalking 与 Spring Cloud Alibaba 全链路追踪实战
  • JavaQuestPlayer:跨平台互动叙事游戏播放器
  • Docker 拉取镜像超时解决方案:配置镜像加速器与 daemon.json 详解
  • 数据结构:顺序表基础与实现
  • 基于 AI 视觉模型的 Unity 编辑器插件界面优化实战
  • Flutter web_scraper 库在 OpenHarmony 下的网页抓取适配实战
  • Go Map 底层原理深度解析
  • 使用 Dify 搭建企业知识库聊天机器人
  • Qwen3.5 大模型单 GPU 高效部署与股票筛选实战
  • OpenClaw + cpolar:打造本地 AI 智能体,实现外网远程访问与控制
  • Python 基础语法与数学建模应用指南
  • 基于 OpenCASCADE 的分层截面几何体重建与 STL 导出
  • OpenClaw Windows 部署指南:Node.js 22、Kimi 与飞书集成
  • C++ 编程全栈指南:从基础语法到项目实战
  • 基于 Leaflet Trackplayer 的 WebGIS 高速公路轨迹可视化
  • C++ 核心:继承机制详解

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online