洋仔的博客 洋仔的博客
首页
  • 个人心法总结

    • 价值心法
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • iOS基础知识
  • 前端
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 投资体系
  • 毛选
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

洋仔

奋斗的小青年
首页
  • 个人心法总结

    • 价值心法
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • iOS基础知识
  • 前端
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 投资体系
  • 毛选
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 技术文档

  • GitHub技巧

  • Nodejs

  • 博客搭建

  • iOS基础知识

    • iOS底层相关

    • Runloop系列

    • Runtime系列

    • 内存管理系列

    • Block系列

    • 线程系列

    • KVC跟KVO系列以及通知中心

    • UI系列

    • 离屏渲染系列

    • 组件化系列跟架构

    • OC跟webview交互系列

    • 持久化系列

    • APP编译系列

      • APP签名机制
      • 总结面试题有感
      • Mac-O
        • 一、概述
        • 二、mach_header
        • Load Command 源码解读
        • Load Command 和 segment/section 的关系
      • 二进制插桩
    • APP性能优化系列

    • cocoapods系列

    • swift系列

    • Git系列

    • 网络相关

    • 三方库系列

    • 系统原理

    • 总结系列

    • 算法系列

    • 数据结构系列

  • 前端

  • 技术
  • iOS基础知识
  • APP编译系列
洋仔
2023-09-29
目录

Mac-O

# 一、概述

运行时架构(runtime architecture)是针对软件运行环境定义的一系列规则,包括但不限于:

如何为代码和数据(code and data)排位; 在内存中怎样去加载或者追踪程序的部分代码; 告诉编译器应该如何组装代码; 如何调用系统服务,如加载插件; Mac 系统支持多种运行时架构,但是内核可以直接读取的可执行文件只有一种:Mach-O。因此,mac 的运行时架构也被命名为:Mach-O Runtime Architecture;因此,Mach-O 是一种存储标准,用于 Mach-O runtime architecture 架构中对程序的磁盘存储;

Mach-O 是 mach object 的缩写,在 -objc解决分类不加载的问题的官方文档中,明确指出所有的源文件都会被转化成一个 objcet,只不过最后经过链接操作,工程或被转化成静态库、动态库或者是可执行文件(类型不同的 mach-O);

Mach-O 文件分为三大部分:

文件头:

Header(头部),指明了 cpu 架构、大小端序、文件类型、Load Commands 个数等一些基本信息

命令区域:

Load Commands(加载命令),正如官方的图所示,描述了怎样加载每个 Segment 的信息。在 Mach-O 文件中可以有多个 Segment,每个 Segment 可能包含一个或多个 Section,segment 的名字都是大写的,且空间大小为页的整数。页的大小跟硬件有关,在 arm64 架构一页是 16KB,其余为 4KB。

数据区域(包括数据, 代码等等):

Data(数据区),Segment 的具体数据,包含了代码和数据等。

# 二、mach_header

header 位于 Mach-O 文件的头部,其作用是:

识别 Mach-O 的格式; 文件类型; CPU 架构信息; 64 位 header 结构体如下:

struct mach_header_64 {
    uint32_t    magic;      /* 主要用来区分当前Mach-O所支持的CPU架构(当前只有32bit和64bit)。*/
    cpu_type_t  cputype;    /* 主要的CPU类型(32/64bit), 以及其他的属性*/
    cpu_subtype_t   cpusubtype; /* arm 架构下有 arm_v7、arm_all 之类的区别,而 subtype 就是表示这个,部分定义如下: */
    uint32_t    filetype;   /* filetype 就是我们熟知的 Mach-O 文件的类型,比如动态库、主工程生成可执行文件、bundle 等等 */
    uint32_t    ncmds;      /* 也就是下一个segment中得segment的数量。number of load commands */
    uint32_t    sizeofcmds; /* 表示 header 之后的 Load Command 的段数和大小 */
    uint32_t    flags;      /* flags */
    uint32_t    reserved;   /* reserved */
};
1
2
3
4
5
6
7
8
9
10

# Load Command 源码解读

Load Command 由多个 command 组成; command 主要有两种类型:指向具体数据、不指向具体数据; 代码层面上 load_command 结构体相当于基类,很少被使用;

# Load Command 和 segment/section 的关系

上文中讲到 Load Command 主要分为指向数据实体和不指向数据实体两种类型。

不指向数据实体的 command 主要作用是为 dyld 提供信息,而指向数据实体的 command 才是 command 和 segment/section 关系的体现;

如 LC_SEGMENT 指向具体的 segment,这个 segment 的实体部分就是 Mach-O 文件的第三部分,主要内容是代码和数据;

Mac-O加载流程

execve       // 用户点击了app,用户态会发送一个系统调用 execve 到内核
  ▼ __mac_execve  // 主要是为加载镜像进行数据的初始化,以及资源相关的操作,以及创建线程
    ▼ exec_activate_image // 拷贝可执行文件到内存中,并根据不同的可执行文件类型选择不同的加载函数,所有的镜像的加载要么终止在一个错误上,要么最终完成加载镜像。
      // 在 encapsulated_binary 这一步会根据image的类型选择imgact的方法
      /*
       * 该方法为Mach-o Binary对应的执行方法;
       * 如果image类型为Fat Binary,对应方法为exec_fat_imgact;
       * 如果image类型为Interpreter Script,对应方法为exec_shell_imgact
       */
      ▼ exec_mach_imgact   
        ▶︎ // 首先对Mach-O做检测,会检测Mach-O头部,解析其架构、检查imgp等内容,判断魔数、cputype、cpusubtype等信息。如果image无效,会直接触发assert(exec_failure_reason == OS_REASON_NULL); 退出。
          // 拒绝接受Dylib和Bundle这样的文件,这些文件会由dyld负责加载。然后把Mach-O映射到内存中去,调用load_machfile()
        ▼ load_machfile
          ▶︎ // load_machfile会加载Mach-O中的各种load command命令。在其内部会禁止数据段执行,防止溢出漏洞攻击,还会设置地址空间布局随机化(ASLR),还有一些映射的调整。
            // 真正负责对加载命令解析的是parse_machfile()
          ▼ parse_machfile  //解析主二进制macho
            ▶︎ /* 
               * 首先,对image头中的filetype进行分析,可执行文件MH_EXECUTE不允许被二次加载(depth = 1);动态链接编辑器MH_DYLINKER必须是被可执行文件加载的(depth = 2)
               * 然后,循环遍历所有的load command,分别调用对应的内核函数进行处理
               *   LC_SEGMET:load_segment函数:对于每一个段,将文件中相应的内容加载到内存中:从偏移量为 fileoff 处加载 filesize 字节到虚拟内存地址 vmaddr 处的 vmsize 字节。每一个段的页面都根据 initprot 进行初始化,initprot 指定了如何通过读/写/执行位初始化页面的保护级别。
               *   LC_UNIXTHREAD:load_unixthread函数,见下文
               *   LC_MAIN:load_main函数
               *   LC_LOAD_DYLINKER:获取动态链接器相关的信息,下面load_dylinker会根据信息,启动动态链接器
               *   LC_CODE_SIGNATURE:load_code_signature函数,进行验证,如果无效会退出。理论部分,回见第二节load_command `LC_CODE_SIGNATURE `部分。
               *   其他的不再多说,有兴趣可以自己看源码
               */
            ▼ load_dylinker // 解析完 macho后,根据macho中的 LC_LOAD_DYLINKER 这个LoadCommand来启动这个二进制的加载器,即 /usr/bin/dyld
              ▼ parse_machfile // 开始解析 dyld 这个mach-o文件
                ▼ load_unixthread // 解析 dyld 的 LC_UNIXTHREAD 命令,这个过程中会解析出entry_point
                  ▼ load_threadentry  // 获取入口地址
                    ▶︎ thread_entrypoint  // 里面只有i386和x86架构的,没有arm的,但是原理是一样的
                  ▶︎ //上一步获取到地址后,会再加上slide,ASLR偏移,到此,就获取到了dyld的入口地址,也就是 _dyld_start 函数的地址
        ▼ activate_exec_state
          ▶︎ thread_setentrypoint // 设置entry_point。直接把entry_point地址写入到用户态的寄存器里面了。
          //这一步开始,_dyld_start就真正开始执行了。

▼ dyld
  ▼ __dyld_start  // 源码在dyldStartup.s这个文件,用汇编实现
    ▼ dyldbootstrap::start() 
      ▼ dyld::_main()
        ▼ //函数的最后,调用 getEntryFromLC_MAIN,从 Load Command 读取LC_MAIN入口,如果没有LC_MAIN入口,就读取LC_UNIXTHREAD,然后跳到主程序的入口处执行
        ▼ 这是下篇内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
编辑 (opens new window)
上次更新: 2024/10/23, 23:26:17
总结面试题有感
二进制插桩

← 总结面试题有感 二进制插桩→

最近更新
01
数组
10-25
02
数组双指针系列之对撞指针
10-25
03
数组双指针系列之快慢指针
10-25
更多文章>
Theme by Vdoing | Copyright © 2019-2024 Evan Xu | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式