希望通过本文来记录对于iOS开发对Mach-O需要有的基本了解。
苹果推出Mach-O的背景:
- 过渡至基于 Mach 内核的操作系统:苹果于 2001 年推出了 macOS(当时称为 Mac OS X)操作系统,该操作系统采用了基于 Mach 内核的架构。为了适应新的操作系统架构,苹果需要引入一种新的文件格式来支持该架构,用于可执行文件、静态库和动态库的存储和交互。
- 提高性能和可扩展性:Mach-O 文件格式相对于旧的目标文件格式(如 a.out 格式)具有更好的性能和可扩展性。它采用了更加紧凑和高效的数据结构,使得应用程序的加载、链接和执行更高效。这在日益复杂的应用程序和需求下变得尤为重要。
- 支持 Objective-C 和 Cocoa 框架:苹果广泛采用 Objective-C 编程语言和 Cocoa 框架来开发 macOS 和 iOS 上的应用程序。Mach-O 文件格式与 Objective-C 运行时和 Cocoa 框架的集成紧密相关,使得开发者可以更好地利用这些技术进行应用程序开发。
- 跨平台支持和移植性:Mach-O 文件格式不仅用于 macOS 和 iOS,还可以支持其他基于 Darwin 内核的操作系统,如 tvOS 和 watchOS。这种一致的文件格式使得开发者可以更方便地在不同的苹果平台上共享和移植代码,提高开发效率和代码复用性。
- 操作系统集成:Mach-O 文件格式与苹果操作系统的内核(XNU)紧密集成。苹果控制了 Mach-O 格式的规范和解析器,从而使得操作系统和应用程序可以更紧密地进行交互和整合。
一、认识Mach-O
在Xcode
工程中,我们可以看到编译设置里面有一个Mach-O type
, 可以看到主工程的格式是Executable
(可执行文件)。
而在组件化工程里,有一些本地或私有库我们可能会在podspec
中声明s.static_framework = true
,这样就会是静态库;三方库没有这个声明默认是动态库。
静态库 | 动态库 |
---|---|
Mach-O
是 Mach Object
的缩写,它是Mac/iOS 中用于存储程序、库的标准格式。作为 a.out
格式的替代,Mach-O
提供了更强的扩展性,并提升了符号表中信息的访问速度。
类型 | 代表文件 |
---|---|
Executable(可执行文件) | xxx.app/xxx、推送扩展 |
Dynamic Library(动态库文件) | .dylib(一般是系统动态库)和xxx.framework/xxx (三方动态库) |
Bundle | 一种特定结构的文件夹,可以包含可执行文件、动态库、静态库和各种资源文件,以及配置文件等,通常作为插件或扩展。需通过dlopen加载。 |
Static Library | 静态库文件(.a文件,是多个.o文件的集合),如pod库声明s.static_framework = true ,产物是静态框架 |
Relocatable Object File | 目标文件(.o文件,编译源代码得到的中间文件) |
二、Mach-O的类型
1. 有哪些类型
我们可以在Xcode
中shift + command + o
,输入loader.h,可在文件中查看到Mach-O
的类型定义如下,
C++
#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 /* a file composed of other Mach-Os to be run in the same userspace sharing a single linkedit. */
#define MH_GPU_EXECUTE 0xd /* gpu program */
#define MH_GPU_DYLIB 0xe /* gpu support functions */
2. 常见的Mach-O类型
MH_OBJECT
:目标文件即 .o 文件 以及静态库文件即 .a 文件(多个.o文件合并在一起);
MH_EXECUTE
:可执行文件,即App编译运行后生成的可执行文件,在/Products路径下;
MH_DYLIB
:动态库文件,即.dylib文件 或者 .framework文件;
MH_DYLINKER
:/usr/lib/dyld路径下的dyld文件;
MH_DSYM
:Xcode打包后生成的符号表文件,即.dSYM文件;
3. 如何查看mach-o文件类型
找到我们的APP后可通过命令行查看类型。
- 通过file命令
arduino
// 1.查看APP的可执行文件
file xxx.app/xxx
输出: Mach-O 64-bit executable arm64
// 2.查看pod三方库的machO
file xxx.app/Frameworks/AFNetworking.framework/AFNetworking
输出:Mach-O 64-bit dynamically linked shared library arm64
三、Mach-O文件结构
项目 | 内容 |
---|---|
结构图 | |
Header | 包含Mach-O文件的基本信息,例如文件类型,支持的CPU架构类型,加载指令的数量,所占内存大小等 |
Load Command | 不同数据段segment的加载命令,指导加载器加载数据 |
Data | 在Load Command中定义的Segment的原始数据。 |
四、查看Mach-O的方式
- 使用
MachOView
:github.com/gdbinit/Mac... - otool命令
xml
otool -l <file>:显示 Mach-O 文件的加载命令信息。
otool -t <file>:显示 Mach-O 文件的文本节信息。
otool -L <file>:显示 Mach-O 文件的依赖库信息。
使用 man otool 命令查看 otool 的帮助文档
- lipo命令
scss
lipo -info 文件 // 查看架构信息
lipo <file> -thin 目标架构 -output 输出文件 // 导出某种架构
lipo <file1> <file2> -output 输出文件 // 合并多个架构
- objdump命令
css
objdump --macho --private-headers <file>
五、文件结构中各部分内容细节
还是可以从loader.h
文件中找源码定义。
1. Header
源码中结构
arduino
struct mach_header_64 {
uint32_t magic; /* mach magic number identifier */
int32_t cputype; /* cpu specifier */
int32_t cpusubtype; /* machine specifier */
uint32_t filetype; /* type of file */
uint32_t ncmds; /* number of load commands */
uint32_t sizeofcmds; /* the size of all the load commands */
uint32_t flags; /* flags */
uint32_t reserved; /* reserved */
};
通过otool命令查看一个Mach-O的头部信息
css
otool -hv xxx
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
MH_MAGIC_64 ARM64 ALL 0x00 EXECUTE 138 13384 NOUNDEFS DYLDLINK TWOLEVEL WEAK_DEFINES BINDS_TO_WEAK PIE
2. Load Commands
加载命令段,这部分的作用是本质就是确定如何加载段segment数据,主结构是:
arduino
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};
可以通过MachOView来查看有哪些段命令:
项目 | 内容 |
---|---|
加载命令 | |
LC_SEGMENT_64 | segment段加载指令 |
LC_DYLD_INF0_0NLY | 加载动态链接库信息(重定向地址、弱引用绑定、懒加载绑定、开放函数等的偏移值等信息) |
... | ... |
2.1 其中LC_SEGMENT_64
命令的结构
arduino
struct segment_command { /* for 32-bit architectures */
uint32_t cmd; /* LC_SEGMENT 加载命令的类型*/
uint32_t cmdsize; /* includes sizeof section structs 加载命令的所占内存大小*/
char segname[16]; /* segment name */
uint32_t vmaddr; /* memory address of this segment 段Segment的虚拟内存地址*/
uint32_t vmsize; /* memory size of this segment 段Segment的虚拟内存大小*/
uint32_t fileoff; /* file offset of this segment 段Segment的在文件中的偏移量*/
uint32_t filesize; /* amount to map from the file 段Segment在文件中所占的内存大小*/
vm_prot_t maxprot; /* maximum VM protection 表示页面所需要的最高内存保护*/
vm_prot_t initprot; /* initial VM protection 表示页面初始的内存保护*/
uint32_t nsects; /* number of sections in segment 段Segment包含节区sections的数量*/
uint32_t flags; /* flags 表示段的标志信息*/
}
-
结构体中
segname
是加载目标段Segment
的名称,常见的段segment有四个(可以从上图中看到)__PAGEZERO
: 在可执行文件有的,动态库里没有,这个段开始地址为0(NULL指针指向的位置),是一个不可读、不可写、不可执行的空间,能够在空指针访问时抛出异常。__TEXT
:代码段,里面主要是存放代码的,该段是可读可执行,但是不可写;__DATA
:数据段,里面主要是存放数据,该段是可读可写,但不可执行;__LINKEDIT
:用于存放签名信息,该段是只可读,不可写不可执行;
2.2 每个命令包含的内容
我们在MachOView展开LC_SEGMENT_64(__TEXT)
或LC_SEGMENT_64(__DATA)
,可以看到很多的section header
. 这个是数据段和代码段的各个section 的头文件。
arduino
struct section_64 { /* for 64-bit architectures */
char sectname[16]; /* name of this section */
char segname[16]; /* segment this section goes in */
uint64_t addr; /* memory address of this section */
uint64_t size; /* size in bytes of this section */
uint32_t offset; /* file offset of this section */
uint32_t align; /* section alignment (power of 2) */
uint32_t reloff; /* file offset of relocation entries */
uint32_t nreloc; /* number of relocation entries */
uint32_t flags; /* flags (section type and attributes)*/
uint32_t reserved1; /* reserved (for offset or index) */
uint32_t reserved2; /* reserved (for count or sizeof) */
uint32_t reserved3; /* reserved */
};
sectname
:section的名称,常见的section有_text、stubs等等;
segname
:当前section所隶属的Segment,例如__TEXT(代码段);
addr
: section在内存的起始位置;
size
: section所占内存大小;
offset
: section在文件中的偏移量;
align
:字节大小对齐,2的align次方;
reloff
:重定位入口的文件偏移;
nreloc
: 需要重定位的入口数量;
flags
:包含section的type和attributes;
3. Data部分
Data部分主要放的是__Text
段和__DATA
段的数据,根据不同功能分为不同的节(section)。 段数据的头部信息是存放在Load Commands
中的. 代码段和数据段的各个节分别代表什么可以通过这篇linkmap文章来了解 通过LinkMap来了解Mach-O
六、MachO里面各部分占用大小
我们还可以通过size命令行查看一个Mach-O大小信息:
arduino
// 假设app工程名xxx,
// -l 参数可以显示目标文件的完整节(section)和段(segment)信息
// -m 参数用于指定目标文件的格式
size -l -m xxx.app/xxx
输出文件信息包含四个部分:
Segment __PAGEZERO
Segment __TEXT
Segment __DATA
Segment __LINKEDIT
具体的信息可以打印你APP的对照看
接下来看看每个段的打印信息具体是什么意思。
Segment __PAGEZERO: 4294967296 (zero fill) (vmaddr 0x0 fileoff 0)
该段信息指的是一个名为 __PAGEZERO 的段(segment),其大小为 4GB,对应的是零填充的内存。__PAGEZERO 段的虚拟内存地址(vmaddr)为 0x0,文件偏移量(fileoff)为 0。Segment __TEXT: 43319296 (vmaddr 0x100000000 fileoff 0)
. 该段信息指的是一个名为__TEXT
的段(segment),其大小为 约等于 41.31 MB。__TEXT
段的虚拟内存地址(vmaddr
)为 0x100000000,文件偏移量(fileoff
)为 0。虚拟内存地址指示了段在程序运行时被加载到内存的位置,而文件偏移量指示了段在目标文件中的位置。Segment __DATA: 5849088 (vmaddr 0x102950000 fileoff 43319296)
该段信息指的是一个名为 __DATA 的段(segment),其大小约等于 5.572 MB。__DATA 段的虚拟内存地址(vmaddr)为 0x102950000,文件偏移量(fileoff)为 43319296。Segment __LINKEDIT: 2736128 (vmaddr 0x102ee4000 fileoff 48840704)
该段信息指的是一个名为 __LINKEDIT 的段(segment),其大小约等于 2.61 MB。__LINKEDIT 段的虚拟内存地址(vmaddr)为 0x102ee4000,文件偏移量(fileoff)为 48840704。
1. __PAGEZERO
在 Mach-O 文件格式中,__PAGEZERO 段用于标识虚拟内存空间的起始位置,并指示该段之前的内存区域应该被清零填充。__PAGEZERO(又称为 Page Zero)是一个特殊的节(Section)名称,它在 Mach-O(Mach Object)文件中定义了虚拟地址空间的第一个页。它没有任何实际的可执行代码或数据,仅作为一种占位符存在,用于确保虚拟内存空间的连续性和保护。
一些主要的特点和作用如下:
-
安全保护:__PAGEZERO 节的存在是为了提供一种安全机制,用于检测和防止针对软件漏洞的攻击。通过将可执行文件或可加载文件的第一个页设置为没有权限的页,可以防止非法访问者利用指向第一个页的指针进行漏洞利用。
-
空节:__PAGEZERO 节本身不包含实际的代码或数据。它的大小通常为0字节,所以在执行时不会占用任何实际内存。
-
地址空间布局:__PAGEZERO 节通常位于可执行文件或可加载文件的开始位置,即位于虚拟地址空间的最低部分。它的存在确保了后续节的虚拟地址是从一个明确定义的位置开始的。
2.__TEXT
在 Mach-O 文件格式中,__TEXT
段包含了可执行程序的实际代码和只读数据。它是二进制文件中的一个重要段,存储了程序的代码段和只读数据段。
3.__DATA
在 Mach-O 文件格式中,__DATA 段存储了可执行程序的静态变量和全局变量等数据。它包含了程序在运行时的可写数据段,即存储程序在运行过程中产生的数据的空间。
4. __LINKEDIT
在 Mach-O 文件格式中,__LINKEDIT 段存储与链接器相关的信息,比如符号表、重定位信息等。链接器主要负责将不同的目标文件合并成可执行文件,__LINKEDIT 段存储与该过程相关的一些信息,因此该段也被称为链接器的信息段。
具体来说,__LINKEDIT 段包含以下内容:
- 符号表(Symbol Table):用于存储程序中定义和引用的各种符号(变量、函数、类等)的信息,链接器通过符号表进行符号解析和重定位等操作。
- 字符串表(String Table):存储符号表中的字符串,用于标识符号的名称。
- 动态符号表(Dynamic Symbol Table):包含一些在运行时动态加载的符号信息。
- 重定位表(Relocation Table):存储在链接过程中需要进行地址重定位的部分,包括指令中需要修改的地址和重定位类型等信息。 __LINKEDIT 段的存在使得链接器能够在程序运行时解析和重定位符号,从而正确地连接和加载各种模块。