Mach-O相关概念

2022-05-11 17:12:45 浏览数 (2)

目录

  • 一、什么是Mach-O
  • 二、属于Mach-O格式的文件类型
  • 三、常见的Mach-O文件类型
  • 四、Universal Binary通用二进制文件
  • 五、Mach-O基本结构
    • 5.1 窥探Mach-O的结构
    • 5.2 Mach-O文件包含3个主要区域
      • 5.2.1 Header
      • 5.2.2 Load commands
      • 5.2.3 Section
      • 5.2.4 _debug相关section
  • 六、Mach-O加载过程

一、什么是Mach-O

Mach-OMach Object的缩写,是Mac/iOS上用于存储程序、库的标准格式

二、属于Mach-O格式的文件类型

xnu内核源码-loader.h文件

代码语言:javascript复制
#define	MH_OBJECT	0x1		/* relocatable object file */
#define	MH_EXECUTE	0x2		/* demand paged executable file */
#define	MH_FVMLIB	0x3		/* fixed VM shared library file */
#define	MH_CORE		0x4		/* core file */
#define	MH_PRELOAD	0x5		/* preloaded executable file */
#define	MH_DYLIB	0x6		/* dynamically bound shared library */
#define	MH_DYLINKER	0x7		/* dynamic link editor */
#define	MH_BUNDLE	0x8		/* dynamically bound bundle file */
#define	MH_DYLIB_STUB	0x9		/* shared library stub for static */
					/*  linking only, no section contents */
#define	MH_DSYM		0xa		/* companion file with only debug */
					/*  sections */
#define	MH_KEXT_BUNDLE	0xb		/* x86_64 kexts */
#define	MH_FILESET	0xc		/* set of mach-o's */

三、常见的Mach-O文件类型

  • MH_OBJECT
    • 目标文件(.o)
    • 静态库文件(.a),静态库文件其实就是N个.o合并在一起
  • MH_EXECUTE:可执行文件
  • MH_DYLB:动态库文件
    • .dylib
    • .framework/xx
  • MH_DYLINKER:动态链接编辑器
    • /usr/bin/dyld
  • MH_DSYM:存储着二进制文件符号信息的文件
    • .dSYM/Contents/Resources/DWARF/xx(常用于分析app的奔溃信息)

四、Universal Binary通用二进制文件

五、Mach-O基本结构

5.1 窥探Mach-O的结构

5.2 Mach-O文件包含3个主要区域

  • Header(头部) : 指明了cpu架构、大小端序、文件类型、Load commands个数等一些基本信息
  • Load commands(加载命令) : 描述文件在虚拟内存中的逻辑结构、布局
  • Raw segment data(数据区) : 在Load commands中定义的Segment的原始数据,包含了代码和数据等。
5.2.1 Header

字段

magic

很多类型的文件,其起始的几个字节的内容是固定的,根据这几个字节的内容就可以确定文件类型,因此这几个字节的内容被称为魔数 (magic number)。

cputype

CPU类型以及子类型字段,该字段确保系统可以将适合的二进制文件在当前架构下运行

cpusubtype

CPU指定子类型,对于inter,arm,powerpc等CPU架构,其都有各个阶段和等级的CPU芯片,该字段就是详细描述其支持CPU子类型

filetype

说明该mach-o文件类型(可执行文件,库文件,核心转储文件,内核扩展,DYSM文件,动态库)

ncmds

说明加载命令条数

sizeofcmds

表示加载命令大小

flags

标志位,该字段用位表示二进制文件支持的功能,主要是和系统加载,链接相关

reserved

保留字段

  • magic number 苹果平台有以下几种magic类型: 脚本 - x7FELF,常用于shell及其他解释器,如 Perl, AWK 等 通用二进制格式 - 0xcafebabe、0xbebafeca,包含多种架构支持的二进制格式,只在 macOS 上支持 MachO格式 - 根据苹果xnu内核源码,OSX和iOS上分别有以下几种不同架构对应的Magic number:

MH_CIGAMMH_MAGIC的反写,表示在小端序(litter endian)环境下使用,所以MH_MAGIC是在大端序(big endian)环境下使用

代码语言:javascript复制
    /* Constant for the magic field of the mach_header (32-bit architectures) */
    #define	MH_MAGIC	0xfeedface	/* the mach magic number */
    #define MH_CIGAM	NXSwapInt(MH_MAGIC)

    /* Constant for the magic field of the mach_header_64 (64-bit architectures) */
    #define MH_MAGIC_64	0xfeedfacf	/* the 64-bit mach magic number */
    #define MH_CIGAM_64	NXSwapInt(MH_MAGIC_64)

Magic number显示涉及到大小端字节序,在class-dump中也可以看到下列源码:

代码语言:javascript复制
    _byteOrder = CDByteOrder_LittleEndian;

    CDDataCursor *cursor = [[CDDataCursor alloc] initWithData:data];
    _magic = [cursor readBigInt32];
    if (_magic == MH_MAGIC || _magic == MH_MAGIC_64) {
        _byteOrder = CDByteOrder_BigEndian;
    } else if (_magic == MH_CIGAM || _magic == MH_CIGAM_64) {
        _byteOrder = CDByteOrder_LittleEndian;
    } else {
        return nil;
    }
5.2.2 Load commands

常见的command及作用

command

作用

LC_SEGMENT/LC_SEGMENT_64

将对应的段中的数据加载并映射到进程的内存空间去

LC_SYMTAB

符号表信息

LC_DYSYMTAB

动态符号表信息

LC_LOAD_DYLINKER

标明我们的MachO是被谁加载进去的,即动态加载连接器dyld

LC_UUID

标示该二进制文件唯一的 UUID,128bit

LC_VERSION_MIN_IPHONEOS/MACOSX

要求的最低系统版本(Xcode中的Deployment Target)

LC_MAIN

设置程序主线程的入口地址和栈大小

LC_ENCRYPTION_INFO

加密信息

LC_LOAD_DYLIB

加载的动态库,包括动态库地址、名称、版本号等

LC_FUNCTION_STARTS

函数地址起始表

LC_CODE_SIGNATURE

代码签名信息

LC_SEGMENT/LC_SEGMENT_64用于描述如何加载数据到进程,最为重要,常见的有:

常见Segment

含义

__TEXT

代码段/只读数据段

__PAGEZERO

__PAGEZERO 是在可执行文件有的,动态库里没有。这个段开始地址为0(NULL指针指向的位置),是一个不可读、不可写、不可执行的空间,能够在空指针访问时抛出异常。

__DATA

数据段

__LINKEDIT

包含需要被动态链接器使用的信息,包括符号表、字符串表、重定位项表等。该段是只可读,不可写不可执行

__OBJC

包含会被Objective Runtime使用到的一些数据。

5.2.3 Section
  • 常见的section

Section

含义

__text

主程序可执行的机器码

__stubs

用于动态库链接的桩,本质上是一小段会直接跳入lazybinding的表对应项指针指向的地址的代码。

__stub_helper

动态库链接的桩的辅助函数。上述提到的lazybinding的表中对应项的指针在没有找到真正的符号地址的时候,都指向这。

__cstring

去重后的常量字符串符号表描述信息,通过该区信息,可以获得常量字符串符号表地址

_TEXT __const

初始化过的常量

__unwind_info

用于存储处理异常情况信息

__objc_methname

保存OC里面方法名

__objc_classname

保存OC类的名字

__objc_methtype

保存ObOCjc类的一些信息(函数签名)

__objc_classlist

OC的类列表

__objc_nlclslist

OC的 load 函数列表,比 __mod_init_func 更早执行

__objc_catlist

OC的category列表

__objc_protolist

OC的协议列表

__objc_imageinfo

保存文件中OC执行代码的一些信息

__objc_selrefs

指向selectors的引用

__objc_protorefs

指向protocol的引用

__objc_classrefs

指向classes的引用

__objc_superrefs

指向super classes的引用

__mod_init_func

初始化的全局函数地址,在 main 之前被调用

__bss

未初始化的静态变量

_got

存储引用符号的实际地址,类似于动态符号表

__bss

未初始化的静态变量

__nl_symbol_ptr

非lazy-binding的指针表,每个表项中的指针都指向一个在装载过程中,被动态链机器搜索完成的符号

__la_symbol_ptr

lazy-binding的指针表,每个表项中的指针一开始指向stub_helper

DATA.common

没有初始化过的符号声明

利用dyld调用__mod_init_func机制脱壳

5.2.4 _debug相关section

六、Mach-O加载过程

通过前面内容,我们知道Mach-O有多种文件类型,比如MH_DYLIB文件、MH_BUNDLE文件、MH_EXECUTE文件(这些需要dyld动态加载),MH_OBJECT(内核加载)等。所以一个进程往往不是只需要内核加载器就可以完成加载的,需要dyld来进行动态加载配合。

加载过程底层执行:

代码语言:javascript复制
execve
__mac_execve
exec_activate_image
exec_mach_imgact
load_machfile
parse_machfile
load_dylinker

一、内核加载流程

  • 分配虚拟内存空间。
  • fork进程。
  • 加载 Mach-O 到进程空间。
  • 加载动态连接器 dyld 并将控制权交给 dyld 处理。

二、dyld处理流程

主要有以下步骤:Load dylibs -> Rebase -> Bind -> ObjC -> Initializers

  • 处理环境变量 大部分可以在Xcode进行相关的配置,进行对应的操作(如Log相关信息)
  • 解析Mach-O执行文件
  • 加载共享动态库 默认的动态库会合并成一个大缓存文件,放到/System/Library/Cache/com.apple.dyld/目录下,按不同的架构分别保存着。其中包括UIKitFoundation等基础库。
  • Rebase/Bind 在系统动态加载Mach-O文件的时候,会经过Rebase以及Bind两个阶段,其中Rebase是将内部指针进行固定数值的偏移,而Bind则正式用于将外部符号转为实际指针的步骤。Rebase数据描述了哪些是对指向Mach-O内部的引用并将其修正,而Bind数据描述哪些是指向外部的引用并进行修正。rebasingbinding包括weak_bind以及lazy_bind,它们在__LINKEDIT段内数据流的编码协议基本相同,都是以操作数(opcode)、立即数(immediate)以及uleb128/sleb128编码的偏移组成。 Rebase - 程序每次启动后地址都会随机变化,这样程序里所有的代码地址都是错的,需要重新对代码地址进行修复才能正常访问,这个操作就是Rebaserebasing的协议和操作相对简单,都是找到地址后给其值加上偏移即可。 rebase协议:通过byte & 0xF0得到opcode(操作数),byte & 0x0F得到immediate(立即数),根据操作数(opcode)进行分支处理。 Bind - 由于符号在不同的库里面,所以需要符号绑定(Bind)这个过程。 binding相对rebasing较复杂一些,它多了查找依赖库的部分,不过总体协议是相似的。包含non-lazy bindinglazy bindingweak binding。在ObjC中,类继承关系以及protocol等是non-lazy的,启动时就需要开始绑定,而在函数里的调用外部函数等等都是lazy binding的,在第一次调用时才会进行绑定。 binding协议:和rebasing相同,通过byte & 0xF0得到opcode(操作数),byte & 0x0F得到immediate(立即数),根据操作数(opcode)进行分支处理。每次binding是在 rebasing之后进行的,他们交替进行,每个Mach-O镜像加载完成后需要将内部的地址引用都修正为偏移之后的正确地址,然后执行binding来修改外部引用地址。 Export - export数据描述了对外可见的符号,通过objdump命令可查看外部可见符号; 在进行rebasing之前,内核只是将Mach-O数据映射到虚拟内存,还未加载到内存。当rebasing 阶段开始在__DATA段进行读取时,发现没有数据,产生了page fault内核异常,这个时候内核才会从磁盘将相应的页(page)读到内存继续进行rebasing.
  • 准备Objc环境 dyld将主程序Mach-O基址指针和包含的ObjC相关类信息传递到libobjcObjC Runtime__DATA段中获取ObjC类信息,由于ObjC是动态语言,可以通过类名获取其实例,所以Runtime维护了一个映射所有类的全局类名表。当加载的数据包含了类的定义,类的名字就需要注册到全局表中。 获取 protocolcategory 等类相关属性并与对应类进行关联。ObjC 的调用都是基于 selector 的,所以需要对 selector 全局唯一性进行处理。 以上步骤由 dyld 启动 libSystem.dylib 统一对基础库进行调用执行,这里面就包含了 libobjcRuntime,同时 Runtime 会在 dyld 绑定回调,当 dyld 处理完相关数据后就会调用 ObjC Runtime 执行 Setup 工作。
  • Initializers 通过 ObjC Runtimedyld 注册的通知,当 Mach-O 镜像准备完毕后,dyld 会回调到 ObjC 中执行 load 方法,包括以下步骤: (1)获取所有 non-lazy class 列表。 (2)按继承以及 category 的顺序将类排入待加载列表。 (3)对待加载列表中的类进行方法判断并调用 load 方法。 执行 C/C 初始化构造器,如通过 attribute((constructor)) 注解的函数。 如果包含 C ,则 dyld 同样会回调到 libc 库中对全局静态变量、隐式初始化等进行调用。

0 人点赞