iOS-Mach-O

知 识 点 / 超 人


Mach-O

目录
1.概要
2.重定向
3.a.out
4.Mach-O
5.Symbol Table(符号表) & String Table(字符表) & Indirect Symbol Table(间接符号表)


概要

程序的构建过程包含 预处理 -> 编译 -> 汇编 -> 链接 等 4 个主要阶段,完成之后就会得到 Mach-O 可执行文件

Mach-O的内容非常的多,也相对比较枯燥无味。以我过来人的经验而言,耐着性子多看几遍本文并自己上手操作会更容易明白。

在讲述Mach-O里会涉及到Linux操作命令和shell。可以结合Linux和shell文档看。建议可以学习一下Linux命令和shell,以后逆向或者编译控制都需要相关基础。

平时我们在App Store下载或者上传到App Store的软件都是ipa包。实际上.ipa是一个压缩包,主要是减少体积,节省用户下载时的流量,所以在下载App的时候会存在,App大小和安装包大小的概念,App大小就是指的App压缩包大小(.ipa大小),当压缩包下载完成后,就会自动解压,解压过程也就是通常所说的安装过程,安装大小就是指压缩包解压后所占用的空间,我们可以把一个ipa文件直接通过unzip命令解压。

//cd 到 ipa包所在的路径下
//XuekaoleCC.ipa 是ipa包的名称
//unzip是解压命令
$unzip XuekaoleCC.ipa
解压后

解压之后,会有一个Payload目录,而Payload里则是一个.app文件,而这个实际上是一个目录,或者说是一个完整的App Bundle。我们可以选中.app文件右键显示包内容,在目录中,里面体积最大的文件通常就是和ipa包同名的一个二进制文件。


.App内容

将该文件copy到任意路径下,使用file命令来看一下这个文件的类型


二进制文件类型

可以从输出中看见,二进制文件中是两个Mach-O格式的可执行文件,分别支持arm_v7和arm64处理器架构。通过objdump —macho —private-headers 文件地址命令,可以查看二进制文件中Mach-O的header信息。在正式讲Mach-O之前,会普及一些知识,便于后面Mach-O的理解。如果想直接了解Mach-O可以直接跳到后面Mach-O部分。


I/O重定向

I/O是输入/输出(input/output)的缩写。I/O重定向可以把命令行的输入重定向为从文件中获取内容,也可以把命令行的输出结果重定向到文件中。I/O重定向功能可以改变输出内容发送的目的地,也可以改变输入内容的来源地。通常来说,输出内容显示在屏幕上,输入内容来自于键盘。但是使用I/O重定向功能可以改变这一惯例。

//Linux的一些基本命令
cat:合并文件。
sort:对文本行排序。
uniq:报告或删除文件中重复的行。
wc:打印文件中的换行符、字和字节的个数。
grep:打印匹配行。
head:输出文件的第一部分内容。
tail:输出文件的最后一部分内容。
less:与 more 类似,但使用 less 可以随意浏览文件,而 more 仅能向前移动,却不能向后移动,而且 less 在查看之前不会加载整个文件。
tee:读取标准输入的数据,并将其内容输出到标准输出和文件中。
ls: 跟dos下的dir命令是一样的都是用来列出目录下的文件名或者文件目录
ls -l :列出目录下的文件或者文件目录的详细信息
标准输入、标准输出和标准错误

标准输出:默认情况下,标准输出的结果都将被连接显示到终端上,输出通常有两种方式

  • 1.生成的数据结果:一种是程序运行的结果,即该程序生成的数据。另一种是状态和错误信息,表示程序当前的运行行的结果,即该程序生成的数据。
  • 2.打印在终端上的信息:状态和错误信息,表示程序当前的运行情况。比如输入ls命令,屏幕上将显示它的运行结果以及它的相关错误信息。

标准输入:默认情况下,标准输入连接到键盘。由键盘输入的内容作为输入源

标准错误:默认情况下,标准错误都将被连接显示到终端上。比如我们执行的标准输出是一个错误的命令,那么具体错误的信息将会被打印在终端中。这就是标准错误

标准输出重定向

I/O重定向功能可以重新定义标准输出内容发送到哪里。使用重定向操作符“>”,后面接文件名,就可以把标准输出重定向到另一个文件中,而不是显示在屏幕上。为什么我们需要这样做呢?它主要用于把命令的输出内容保存到一个文件中。比如,我们可以按照下面的形式把ls命令的输出保存到output.txt文件中,而不是输出到屏幕上。

//ls 跟dos下的dir命令是一样的都是用来列出目录下的文件名或者文件目录
//-l 查看详细的文件资料
// /usr/bin 是路径
// > 是重定向操作符
// output.txt 是重定向的标准输出结果
//下面的命令是 将指定目录的详细信息作为输出结果,重定向输出结果到output.txt中。output.txt会输出到当前终端所在目录
$ls -l /usr/bin > output.txt

如果我们把输出的的路径改为一个不存在的路径,那么终端上将会显示No such file or directory。这个 No such file or directory 就是标准错误。为什么错误信息显示在终端上,而不是重定向到output.txt文件中呢?因为是一般的命令不会把它运行的错误信息发送到标准输出文件中。而是把错误信息发送到标准错误文件中。因为我们只重定向了标准输出,并没有重定向标准错误,所以这个错误信息仍然输出到屏幕上。

标准错误重定向

标准错误的重定向并不能简单地使用一个专用的重定向符来实现。要实现标准错误的重定向,不得不提到它的文件描述符(file descriptor)。一个程序可以把生成的输出内容发送到任意文件流中。如果把这些文件流中的前三个分别对应标准输入文件、标准输出文件和标准错误文件,那么shell将在内部用文件描述符分别索引它们为0、1和2。shell提供了使用文件描述符编号来重定向文件的表示法。由于标准错误等同于文件描述符2,所以可以使用这种表示法来重定向标准错误。

// 1>表示重定向标准输出
// 2>表示重定向标准错误
$ls -l /usr2/bin2 1> output.txt 2>error.txt

上面的命名将会输出2个文件,output.txt和error.txt。error.txt中包含的是错误信息。output.txt是一个空文件,由于ls命令执行后没有输出任何内容,只是输出了错误信息,所以重定向操作开始重新改写这个文件,并在出现错误的情况下停止操作,最终导致了该文件内容被删除。所以是一个空文件。

如果要将标准输出和标准错误都输出在同一个文件中,可以使用&符号来表示

// 2>&1表示标准错误的结果与标准输出的文件流一样
ls -l /usr2/bin2 > output.txt 2>&1
标准输入重定向

使用 < 作为输入的重定向符号
这里使用cat命名来测试标准输入重定向。cat将把标准输入内容复制到标准输出文件中。
默认情况下,标准输入是连接的键盘,所以输入cat后终端在等待键盘的输入。我们可以输入任意内容,然后内容将会被复制到标准输出中,默认情况下,标准输出是连接显示终端。所以输入内容会被显示在终端中。

$ cat < input.txt

这里我们准备一个input.txt文件,文件里随便写入一些文本内容。作为输入源,在不重定向输出的情况下,内容会被输出在终端中

管道

命令从标准输入到读取数据,并将数据发送到标准输出的能力,是使用了名为管道的shell特性。使用管道操作符“|”(竖线)可以把一个命令的标准输出传送到另一个命令的标准输入中。

//Command1| command2
//下面的命令是 将当前终端路径下的目录详细信息作为标准输出到终端中,因为加上 | 管道
//输出将会按照管道结合的命令less,进行分页显示
$ls -l | less

a.out

a.out是”assembler output”的缩写格式,代表汇编程序输出。在较早版本的类unix系统中,a.out是一种输出格式,用于可执行文件,目标文件和共享库。早期的 PDP-7系统上没有链接器,程序的创建过程是先把所有源文件连接成一个文件,然后进行汇编,产生的汇编程序保存在a.out中。这样a.out是名副其实的汇编输出,但到PDP-11之后,人们为其编写了链接器,程序的创建是先编译然后链接输出保存到a.out中,这时a.out其实已经是链接输出了,但输出的可执行文件仍然延续这个命名习惯。后来ELF格式的可执行文件出来,慢慢的就取代了a.out

a.out文件由以下七部分组成:

exec header:文件头

主要储存内核参数,内核利用其中一些参数来把二进制文件加载到内存中并执行链接器(.ld)利用另外一些参数来连接其它的二进制文件.这个段是唯一含有命令参数的.

text segment:代码段

包括在程序执行时加载到内存中的机器码和相关数据.有可能是只读的.

data segment:数据段

包括初始化过的数据变量.通常是加载到内存中的可写去中.

text relocations:代码重定向

包含编译连接二进制文件时的记录,这些记录使用来更新代码段中的指针.

data relocations:数据重定向

和代码重定向相似,区别是它针对于数据段的指针.

symbol table:符号表

包含连接器对不同二进制文件中的变量,函数和地址之间的对应关系的记录.

string table:字符串表

包含和符号名字相一致的字符串.

每一种二进制文件都是以这样的一个数据结构开始的:

struct exec {
    unsigned long   a_midmag;//保存的是主机字节序, I由这些宏来访问其中的部分bit位: N_GETFLAG(), N_GETMID(), N_GETMAGIC(), 由宏 N_SETMAGIC().来设置这个字段.
    unsigned long   a_text;//代码段的大小
    unsigned long   a_data;//数据段的大小
    unsigned long   a_bss;//bss segment中字节数和被内核用来初始化数据段之后的BRK (bss = block started by symbol)
    unsigned long   a_syms;符号表的大小
    unsigned long   a_entry;//保存在程序被内核加载到内存中后程序的起始地址,内核由此地址开始执行程序
    unsigned long   a_trsize;//代码重定向表的大小
    unsigned long   a_drsize;//数据重定向表的大小
};


Mach-O

是Mach Object文件格式的缩写,是a.out格式的一种替代,是一种可执行文件、目标代码、共享程序库、动态加载代码和核心dump,是macOS、iOS、iPadOS储存程序和库的文件格式。Mach-O提供更多的可扩展性和更快的符号表信息存取。Mach-O应用在基于Mach核心的系统上,目前NeXTSTEP、Darwin、Mac OS X(iPhone)都是使用这种可执行文件格式。Mach-O保存了在编译过程和链接过程中产生的机器代码和数据,从而为静态链接和动态链接的代码提供了单一文件格式。熟悉Mach-O文件格式,有助于了解苹果底层软件运行机制,更好的掌握dyld加载Mach-O的步骤。Mach-O是可读可写的

Mach-O由以下三部分组成:

Mach Header:描述了Mach-O的CPU架构,保存了一些基本信息,包括了该文件运行的平台、文件类型、Load Commands的个数等等。Header的主要作用就是帮助系统迅速的定位Mach-O文件的运行环境,文件类型。保存了一些dyld重要的加载参数

Load Commands:加载命令(Load Command)的集合,描述了 Data 在二进制文件和虚拟内存中的布局信息,通过这个布局信息能够知道 Data 在二进制文件中和虚拟内存中是怎样排布的,它相当于修房子时的图纸一样。

Data:存储了实际的内容,主要是程序的指令和数据,它们的排布完全依照 Load Commands 的描述。Data由Segment(段)和 Section (节)的方式来组成,好比学校中的年级和班级、公司中的部门和小组一样的关系,把有共同特点的内容组织到一块,可以方便管理,提高效率。它们存放了具体的数据与代码,主要包含代码、数据,例如符号表,动态符号表等等。

Mach Header

文章开头有说道可以通过objdump —macho -private-headers 文件地址查看Mach-O的header部分,也可以通过Xcode 自带一个 otool 工具来单独查看headerotool -h 文件地址。可以在mach-o/loader.h中查看mach_header的结构体来了解。Header的内容苹果官网有相关的解释

Mach Header

Header字段说明:
magic:是mach-o文件的魔数。获取 Load Command,时会根据 header 的 magic 来判断是 64 位 还是 32 位,目标机与主机CPU是否相反。MH_MAGIC表示32位,MH_MAGIC_64表示64位,MH_CIGAM与MH_CIGAM_64表示目标机的字节排序方案与主机CPU相反。
cputype:CPU类型
cpusubtype:CPU的子类型(详细的CPU类型可以在mach/machine.h中查看)
caps:
filetype:是目标文件的类型,可以有很多类型,例如:静态库(.a)、单个目标文件(.o)都可以通过这个类型标识来区分识别
ncmds:cmd就是Load Commands加载命令,ncmds是加载命令的个数
sizeofcmds:是ncmds加载命令所占的大小
flags:里包含的标记很多,比如TWOLEVEL是指符号都是两级格式的,符号自身+加上自己所在的单元,PIE标识是位置无关的。

扩展说明:

一、little-endian(小端)和big-endian(大端)
计算机中通常采用字节存储机制,主要有两种模式: Big-Endian和Little-Endian,即大端模式和小端模式。
Little-Endian:就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
Big-Endian:就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

大端小端

为什么会有大小端模式之分呢?
计算机是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8bit。但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的long型等,对于位数大于8位的处理器,例如32位或者64位的处理器,由于寄存器宽度大于一个字节,因此就需要去考虑字节存储是的方式,是低位到高位,还是高位到低位。因此就导致了大端存储模式和小端存储模式。

例如一个16bit的short型x,在内存中的地址为0x0010,x的值为0x1122,那么0x11为高字节,0x22为低字节。对于大端模式,就将0x11放在低地址中,即0x0010中,0x22放在高地址中,即0x0011中。小端模式,刚好相反。我们常用的x86结构是小端模式,而KEIL C51则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。

如何判断当前CPU是大端还是小端?

int i = 1; //声明一个值为1的int类型变量i,int为4个字节, 1在二进制中为0000 0000 0000 0000 0000 0000 0000 0001,0000位低位,0001位高位

char *p = (char *)&i; //用指针p指向1

if(*p == 1)  //如果是小端模式,则是高位放在高地址,则0001放在高位,指针首地址应该为1

 printf("小端模式"); 

else *// (*p == 0)*

 printf("大端模式");

//也可以使用联合体union判断,因为联合体union存放的顺序是所有成员都从低地址开始存放

二、magic number

magic number

Load Command

加载命令是整个Mach-O中最重要部分。它说明了操作系统应当如何加载文件中的数据,对系统内核加载器和动态链接器起指导作用。一来它描述了文件中数据的具体组织结构,二来它也说明了进程启动后,对应的内存空间结构是如何组织的。我们可以通过otool -l 文件地址来查看Mach-O中的Load command信息。也可以在mach-o/loader.h文件中查看其结构体

Load command

Load command字段说明
cmd :是load command的类型。
cmdsize :代表load command的大小(0×58个字节)。cmdsize可以被添加到当前加载命令的偏移量或指针中。32位架构的cmdsize必须是4字节的倍数,对于64位架构必须是8字节的倍数,填充的字节必须为零。
segname: 16字节的段名字,当前是__PAGEZERO。
vmaddr :段的虚拟内存起始地址
vmsize: 段的虚拟内存大小
fileoff: 段在文件中的偏移量
filesize: 段在文件中的大小
maxprot: 段页面所需要的最高内存保护(4=r,2=w,1=x),为了避免代码逻辑被肆意篡改,r读权限,w写权限,x执行权限
initprot: 段页面初始的内存保护
nsects: 段中包含section的数量
flags: 其他杂项标志位

load command的类型
类型 说明
LC_SEGMENT 加载的主要命令,指导内核来设置进程的内存空间。32位
LC_SEGMENT_64 与LC_SEGMENT一样,但表示64位
LC_LOAD_DYLIB 这是一个需要动态加载的链接库,它使用dylib_command结构体表示
LC_MAIN 记录了可执行文件的主函数main()的位置,它使用entry_point_command结构体表示
LC_CODE_SIGNATURE 代码签名的加载命令,描述了Mach-O的代码签名信息,它属于链接信息,使用linkedit_data_command结构体表示
LC_DYLD_INFO_ONLY 动态链接相关信息
LC_SYMTAB 符号表地址
LC_DYSYMTAB 动态符号地址表
LC_UUID 二进制文件的标识ID,dSYM文件、crash中都存在这个值,确定两个文件是否匹配,分析出对应的崩溃位置
LC_VERSION_MIN_MACOSX 二进制文件要求的最低系统版本,和xcode中配置的target有关
LC_SOURCE_VERSION 构建二进制文件使用的源代码版本
LC_FUNCTION_STARTS 定义函数的起始地址表,使我们的调试器很容易看到地址
LC_DATA_IN_CODE 定义在代码段内的非指令数据
Data段

Data中的保存的是segment(段)数据,每个segment又包一个或含多个section(节)的数据,多个section时,section区信息会以数组形式紧随着存储段加载命令后面。不同类型的数据放入不同的段中。__TEXT段中的__text是实际上的代码部分;__DATA段的__data是实际的初始数据。section中存放了具体的数据与代码,主要包含代码、数据,例如符号表,动态符号表等等。

Section字段说明
sectname: 节名,一般第一个是__text ,就是表示为主程序代码,命名则是两个下划线紧跟着小写字母(__text)
segname :段名称,该section所属的 segment名(段名称),段的命名规则是两个下划线紧跟着大写字母(如__TEXT)

  • __PAGEZERO:表示运行时 Mach-O 加载进内存后 __PAGEZERO 在内存中占中的大小,它不可读,不可写,主要用来捕捉 NULL 指针的引用。如果访问 __PAGEZERO 段,会引起 EXC_BAD_ACCESS 错误。__PAGEZERO 在 Mach-O 中实际上并不占用 Data 部分的空间。
  • __DATA_CONST:保存程序的代码指令和数据
  • __TEXT:代码段:只有可执行代码和其他只读数据
  • __DATA:用于读取和写入数据的一个段,保存程序的代码指令和数据
  • __LINKEDIT:包含给动态链接库链接器(ldyd)的原始数据段,包含符号表和字符串表,压缩动态链接信息,以及动态符号表

addr: 该section在内存的启始位置,0x0000bcb0。
size :该section的大小,0x004891e0
offset: 该section的文件偏移,31920
align :字节大小对齐 ,16
reloff :重定位入口的文件偏移,0
nreloc: 需要重定位的入口数量,0
flags: 表示节区的标志属性,标志属性如果是 SG_PROTECTED_VERSION_1,表示该段是经过加密的
reserved1和reserved1为保留项

下面是对Mach-O中section常见字段的说明
section 说明
__TEXT.__text 主程序代码
__TEXT.__cstring C语言字符串
__TEXT.__const const 关键字修饰的常量
__TEXT.__stubs 用于 Stub 的占位代码,很多地方称之为桩代码。
__TEXT.__stubs_helper 当 Stub 无法找到真正的符号地址后的最终指向
__TEXT.__objc_methname Objective-C 方法名称
__TEXT.__objc_classname Objective-C 类名称
__DATA.__data 初始化过的可变数据
__DATA.__la_symbol_ptr lazy binding 的指针表,表中的指针一开始都指向 __stub_helper
__DATA.nl_symbol_ptr 非 lazy binding 的指针表,每个表项中的指针都指向一个在装载过程中,被动态链机器搜索完成的符号
__DATA.__const 没有初始化过的常量
__DATA.__cfstring 程序中使用的 Core Foundation 字符串(CFStringRefs)
__DATA.__bss BSS,存放为初始化的全局变量,即常说的静态内存分配
__DATA.__commont 没有初始化过的符号声明
__DATA.__objc_classlist Objective-C 类列表
__DATA.__objc_protolist bjective-C 原型列表
__DATA.__objc_imginfo Objective-C 镜像信息
__DATA.__objc_selfrefs Objective-C self 引用
__DATA.__objc_protorefs Objective-C 原型引用
__DATA.__objc_superrefs Objective-C 超类引用
__TEXT.__objc_methtype Objective-C 方法类型

Symbol Table(符号表) & String Table(字符表) & Indirect Symbol Table(间接符号表)

在编译时,.h/.m文件会经过编译变成.o的目标文件,header里面有说明整个文件的架构环境等情况,Load command里面有说明加载指令与数据的关系,Data里是具体的数据,代码,符号表,重定位符号表等。 一般程序是由很多的.h/.m文件组成的,会编译成很多的.o文件,但最终程序的可执行文件却只有一个。因为编译器会通过链接的方式将多个.o文件里的符号表合并在一起,最终只有一个符号表,然后会通过合并归类后的新符号表生成一个新的重定位符号表,最终生成一个Mach-O可执行文件。

在运行时,一个函数的命令被存放在一段内存中,当进程需要执行这个函数的时候,必须知道要去内存的哪个地方找到这个函数,然后执行它的命令。也就是说,进程要根据这个函数的名称,找到它在内存中的地址,而这个名称与地址的映射关系,是存储在 symbol table中。
symbol table中的 symbol就是这个函数的名称,进程会根据这个 symbol找到它在内存中的地址,然后跳转过去执行。
Load Command 中的 LC_SYMTAB与LC_DYSYMTAB可以定位到具体的符号表位置。可以用个objdump —macho -d 文件地址查看每个段的符号表信息。

Symbol Table(符号表)

符号表是一种存储键值对的数据结构,key是符号的名称,value是符号的地址。主要是将变量、函数等以符号的形式存储。由于函数的具体内存地址是在运行时才能决定,所以编译是仅仅只是对符号表进行归类,并放入重定位符号表中,为的是在运行时能重定位到具体的内存地址。

重定位符号表

表内主要是存储的当前文件使用了那些API,声明并实现但未使用的API不会存在重定位符号表中。在重定位的时候根据重定位符号表去重新生成信息。可以使用objdump —macho -reloc 文件地址可以查看重定位符号表。
下面是重定位符号表的详细内容说明

重定位符号表

String Table(字符表)

用来保存字符串的名字

Indirect Symbol Table(间接符号表)

Symbol Table的子集,主要是用于保存外部符号,更准确一点就是使用的外部动态库的符号。例如:使用NSLog,它本身 是Foundation库里的方法,属于外部引用,间接符号表会记录具体使用了什么文件,什么地方,调用那些符号,符号对应的函数。

LC_SYMTAB & LC_DYSYMTAB

LC_SYMTAB:当前Mach-O中的符号表信息。
LC_DYSYMTAB:描述动态链接器使用其他的Symbol Table信息。
通过LC_SYMTAB与LC_DYSYMTAB用来描述Symbol Table的大小和位置,以及其他元数据。

LC_SYMTAB

用来描述该文件的符号表。不论是静态链接器还是动态链接器在链接此文件时,都要使用该Load command。调试器也可以使用该 Load command找到调试信息。

symtab_command
是定义LC_SYMTAB加载命令的结构体,具体属性如下。在/usr/include/mach-o/loader.h中定义

struct symtab_command (
uint32_t cmd;//共有属性。指明当前描述的加载命令,当前被设置为L C_SYMTAB
uint32_t cmdsize;//共有属性。指明加载命令的大小,当前被设置为sizeof(symtab_command)
uint32_t symoff;//表示从文件开始到symbol table所在位置的偏移量。symbol table用[nlist]来表示 uint32_t 
uint32_t nsyms;//符号表内符号的数量
uint32_t stroff;//表示从文件开始到s tring table所在位置的偏移量。
uint32_t strsize;//表示string table大小(以byte为单位)
}

nlist
定义符号的具体表示含义

struct nlist {
//表示该符号在s tring tabl e的索引
  union {
    char *n_name;//在Mach-O中不使用此字典
    long n_strx;//索引   
  } n_un;
  uint8_t n_type;/* type flag, see below */
  uint8_t n_sect;/* section number or NO_SECT */
  int16_t n_desc;/* see  */
  uint32_t n_value;/* value of this symbol (or stab offset) */
}; 

n_type
该字段1字节,通过四位掩码保存数据

  • N_STAB(0xe0):如果当前的n_type包含这3位中的任何一位,则该符号为调试符号表(stab)。在这种情况下,整个n_type字段将被 解释为 stab value。请参阅 /usr/include/mach-o/stab.h 以获取有效的 stab value。
  • N_PEXT(0x10):如果当前的n_type包含此位。则将此符号标记为私有外部符号 —private_extern (visibility=hidden),只在程 序内可引用和访问。当文件通过静态链接器链接的时候,不要将其转换成静态符号(可以通过ld的-keep_private_externs关闭静态 链接器的这种行为)。
  • N_EXT(0x01):如果当前的n_type包含此位。则此符号为外部符号.该符号在该文件外部定义或在该文件中定义,但可在其他文件中使用。
  • N_TYPE(0x0e):如果当前的n_type包含此位。则使用预先定义的符号类型。
    • N_TYPE字段的值包括:
      • N_UNDF(0x0):该符号未定义。未定义符号是在当前模块中引用,但是被定义在其他模块中的符号。n_sect字段设置为NO_SECT。
      • N_ABS(0x2):该符号是绝对符号。链接器不会更改绝对符号的值。n_sect字段设置为NO_SECT。
      • N_SECT(0xe):该符号在n_sect中指定的段号中定义。
      • N_PBUD(0xc):该符号未定义,镜像使用该符号的预绑定值。n_sect字段设置为NO_SECT。
      • N_INDR(0xa):该符号定义为与另一个符号相同。n_value字段是string table中的索引,用于指定另一个符号的名称。链接该符号 时,此符号和另一个符号都具有相同的定义类型和值。

n_sect
整数,用来在指定编号的section中找到此符号;如果在该image的任何部分都找不到该符号,则为NO_SECT。根据section在 LC_SEGMENT加载命令中出现的顺序,这些section从1开始连续编号。

n_desc
16-bit值,用来描述非调试符号。低三位使用REFERENCE_TYPE :

  • REFERENCE_FLAG_UNDEFINED_NON_LAZY(0X0):该符号是外部非延迟(数据)符号的引用。
  • REFERENCE_FLAG_UNDEFINED_LAZY(0X1):该符号是外部延迟性符号(即对函数调用)的引用。
  • REFERENCE_FLAG_DEFINED(0x2):该符号在该模块中定义。
  • REFERENCE_FLAG_PRIVATE_DEFINED(0x3):该符号在该模块中定义,但是仅对该共享库中的模块可见。
  • REFERENCE_FLAG_PRIVATE_UNDEFINED_NON_LAZY(0x4):该符号在该文件的另一个模块中定义,是非延迟加载(数据)符号,并且仅对该 共享库中的模块可见。
  • REFERENCE_FLAG_PRIVATE_UNDEFINED_LAZY(0x5):该符号在该文件的另一个模块中定义,是延迟加载(函数)符号,仅对该共享库中的 模块可见。

另外还可以设置如下标识位:

  • REFERENCED_DYNAMICALLY(0x10):定义的符号必须是使用在动态加载器中(例如dlsym和NSLookupSymbolInImage )。而不是普通 的未定义符号引用。strip使用该位来避免删除那些必须存在的符号(如果符号设置了该位,则strip不会剥离它)。
  • N_DESC_DISCARDED(0x20):在完全链接的image在运行时动态链接器有可能会使用此符号。不要在完全链接的image中设置此位。
  • N_NO_DEAD_STRIP(0x20):定义在可重定位目标文件(类型为MH_OBJECT )中的符号设置时,指示静态链接器不对该符号进行 dead-strip。(请注意,与N_DESC_DISCARDED(0x20)用于两个不同的目的。)
    .N_WEAK_REF(0x40):表示此未定义符号是弱引用。如果动态链接器找不到该符号的定义,则将其符号地址设置为0。静态链接器会将此符 号设置弱链接标志。
  • N_WEAK_DEF(0x80):表示此符号为弱定义符号。如果静态链接器或动态链接器为此符号找到另一个(非弱)定义,则弱定义将被忽略。 只能将合并部分中的符号标记为弱定义。

如果该文件是两级命名two-level namespace image (即如果mach_header中设置了 MH_TWOLEVEL标志),则n_desc的高8位表示定义 此未定义符号的库的编号。使用宏GET_LIBRARY_ORDINAL来获取此值,或者使用宏SET_LIBRARY_ORDINAL来设置此值。。指定当前image 。1到253根据文件中LC_LOAD_DYLIB命令的顺序表明库号。254用于需要动态查找的未定义符号(仅在OSXv10.3和更高版本中受支持)。 对于从可执行程序加载符号的插件。255,用来指定可执行image。对于flat namespace images,高8位必须为0。

n_value
符号值。对于symbol table中的每一项,该值的表达的意思都不同(具体由n_type字段说明)。对于N_SECT符号类型,n_value是符 号的地址。有关其他可能值的信息,请参见n_type字段的描述。

Common symbols必须为N_UNDF类型,并且必须设置N_EXT位。Common symbols的n_value是符号表示的数据的大小(以 字节为单位)。在C语言中,Common symbol是在该文件中声明但未初始化的变量。Common symbols只能出现在
MH_OBJECT类型的Mach-O文件中。

扩展:

stab value 包括:

#define N_GSYM 0x20        /* 全局符号:name,,NO_SECT,type,0 */
#define N_FNAME 0x22       /* procedure name (f77 kludge): name,,NO_SECT,0,0 */
#define N_FUN 0x24         /* 方法/函数:name,,n_sect,linenumber,address */
#define N_STSYM 0x26       /* 静态符号:name,,n_sect,type,address */
#define N_LCSYM 0x28       /* .lcomm 符号:name,,n_sect,type,address */
#define N_BNSYM 0x2e       /* nsect 符号开始:0,,n_sect,0,address */
#define N_OPT 0x3c         /* emitted with gcc2_compiled and in gcc source */
#define N_RSYM 0x40        /* 寄存器符号:name,,NO_SECT,type,register */
#define N_SLINE 0x44       /* 代码行数:0,,n_sect,linenumber,address */
#define N_ENSYM 0x4e       /* nsect 符号结束:0,,n_sect,0,address */
#define N_SSYM 0x60        /* 结构体符号:name,,NO_SECT,type,struct_offset */
#define N_SO 0x64          /* 源码名称:name,,n_sect,0,address */
#define N_OSO 0x66         /* 目标代码名称:name,,0,0,st_mtime */
#define N_LSYM 0x80        /* 本地符号:name,,NO_SECT,type,offset */
#define N_BINCL 0x82       /* include file 开始:name,,NO_SECT,0,sum */
#define N_SOL 0x84         /* #included file 名称:name,,n_sect,0,address */
#define N_PARAMS 0x86      /* 编译器参数:name,,NO_SECT,0,0 */
#define N_VERSION 0x88     /* 编译器版本:name,,NO_SECT,0,0 */
#define N_OLEVEL 0x8A      /* 编译器-0 级别:name,,NO_SECT,0,0 */
#define N_PSYM 0xa0        /* 参数:name,,NO_SECT,type,offset */
#define N_EINCL 0xa2       /* include file 结束:name,,NO_SECT,0,0 */
#define N_ENTRY 0xa4       /* alternate entry: name,,n_sect,linenumber,address */
#define N_LBRAC 0xc0       /* 左括号:0,,NO_SECT,nesting level,address */
#define N_EXCL 0xc2        /* deleted include file: name,,NO_SECT,0,sum */
#define N_RBRAC 0xe0       /* 右括号:0,,NO_SECT,nesting level,address */
#define N_BCOMM 0xe2       /* 通用符号开始:name,,NO_SECT,0,0 */
#define N_ECOMM 0xe4       /* 通用符号结束:name,,n_sect,0,0 */
#define N_ECOML 0xe8       /* end common (local name): 0,,n_sect,0,address */
#define N_LENG 0xfe        /* entry with length information */
/*
* for the berkeley pascal compiler, pc(1):
*/
#define N_PC 0x30         /* global pascal symbol: name,,NO_SECT,subtype,line */

nm命令
打印nlist结构的符号表(symbol table)。
常用nm命令参数

nm -pa a.o
-a:显示符号表的所有内容
-g:显小全局符号
-p:不排序。显示符号表本来的顺序
-r:逆转顺序
-u:显示未定义符号
-m: 显示N_SECT类型的符号(Mach-O符号)显示。

常用的otool命令

命令代码 命令举例 命令说明
otool -f xxx otool -f HYJADCrash 查看fat headers信息
otool -a xxx otool -a HYJADCrash 查看archive headers信息
otool -h xxx otool -h HYJADCrash 查看Mach-O头结构信息
otool -l xxx otool -l HYJADCrash 查看load commands信息
otool -f xxx otool -f HYJADCrash 查看fat headers信息
otool -L xxx otool -L HYJADCrash 查看依赖的动态库,包括动态库名称、当前版本号、兼容版本号
otool -D xxx otool -D HYJADCrash 查看所支持的框架类型
otool -t -v xxx otool -t -v HYJADCrash 查看text section
otool -d xxx otool -d HYJADCrash 查看objective-C segment信息
otool -o xxx otool -o HYJADCrash 查看fat headers信息
otool -I xxx otool -I HYJADCrash 查看symbol table信息
otool -v -s __TEXT __objc_methname xxx otool -v -s __TEXT __objc_methname HYJADCrash 获取所有方法名称

总结:Mach-O是整个可执行文件的详细信息,header中确定了Mach-O的架构,文件类型和Load command信息,通过header可以让系统知道是否能运行该可执行文件,该如何运行,并能确定内部加载命令需要多大的空间。通过Load command明确的去加载Data里的数据。在运行中通过Data里的各种符号表去找到具体执行的内容。

额外扩展:

2008 年 7 月,搭载了 App Store 的 iPhone 3G 正式发售,下载限制仅为 10 MB
2010 年 2 月,苹果将 iPhone 3G 的下载限制从 10 MB 提升到 20 MB
2012 年 3 月,iOS 5.1 正式版后,下载限制从 20 MB 提升到 50 MB
2013 年 9 月,iOS 7 正式版后,下载限制从 50 MB 提升至 100 MB
2017 年 9 月,iOS 11 正式版后,下载限制从 100 MB 提升至 150 MB
2019 年 5 月,下载限制从 150 MB 提升至 200 MB
2019 年 9 月,iOS 13 正式版后,若下载大小超过 200 MB,用户可选择是否使

苹果关于可执行文件大小限制
苹果对可执行文件大小有明确限制,超过该限制会导致 App 审核被拒。
具体限制如下:
iOS 7 之前,二进制文件中所有的 __TEXT 段总和不得超过 80 MB
iOS 7.X 至 iOS 8.X ,二进制文件中,每个特定架构中的 __TEXT 段不得超过 60 MB
iOS 9.0 之后,二进制文件中所有的 __TEXT 段总和不得超过 500 MB

//因为二进制文件超出限制导致的上传包时报错说明
ERROR: ERROR ITMS-90122: "Invalid ExecutaBe Size. The size of your app's executaBe file 'News.app/News' is 68534272 bytes for architecture 'arm64', which exceeds the maximum allowed size of 60 MB."

所以可以通过将可执行文件的 __TEXT 段中的部分section(节)移动到其它的segment(段),提高了可执行文件的压缩效率,使二进制文件变小,因为二进制文件是包里体积最大的个体,所以二进制文件变小意味着IPA包体积减小

上传到 App Store Connect 后,苹果会对 App 中的可执行文件进行 DRM 加密,然后将 App 压缩成 ipa 文件,才发布到 App Store。加密对可执行文件的大小本身影响很小,但加密会影响可执行文件的压缩效率,导致压缩后的 ipa 大小增加,这就是导致下载大小增大的原因。由于加密的内容实际上都位于 __TEXT 中,而苹果只会扫描 __TEXT 段,所以将 __TEXT 段的内容移动到其他地方,减小__TEXT 段大小,App大小也就减小了

因为操作系统只关心段的读/写/执行权限,并不关心段或节的名称。即便是使用了 -rename_section 移动 Segment/Section,各符号的地址也会由链接器修正好,因此段移动后程序也可以正常运行。

Mach-O 文件代码的解密发生在 Mach-O 文件被加载的时候,由 Mach Loader 进行。Mach Loader 会读取 Mach-O 中的 LC_ENCRYPTION_INFO 这条 Load Command 来判断可执行文件是否加密。

后续会补上符号表相关的处理、死代码脱离、App二进制优化等内容

参考资料:
a.out的由来
linux下的a.out文件

你可能感兴趣的:(iOS-Mach-O)