Mach-O
1. Mach-O简介
- Mach-O(Mach Objcect)是
macOS、iOS、iPadOS存储程序和库的⽂件格式。对应系统通过应用⼆进制接口 (application binary interface,缩写为ABI)来运行该格式的文件。 - Mach-0 格式用来替代
BSD系统的a.out格式。Mach-O文件格式保存了在编译、链接过程中产生的机器代码和数据,从而为静态链接和动态链接的代码提供了单一的文件格式
2. Mach-O的构建
这里直接使用Xcode来构建:
1. 生成.app文件
当通过Xcode运行项目时,会在Products目录下生成.app文件;如下图:
2. 查看Mach-O
在 Finder 中找到 MachOApp.app,右键显示包内容,可看到如下结果:
通过 file 命令来查看MachOApp文件类型(这里用的模拟器,所以是x86_64架构。):
3. Mach-O组成
1. Mach-O 结构图:
从图中可以看出Mach-O主要分为三大部分:Mach Header、Load Command、Data(__TEXT、__DATA、符号表)
2. Mach Header
Mach Header 里有 Mach-O 的CUP信息,以及Load Command的信息。可以使用 otool 或 objdump 命令来查看
-
otool命令:
otool -v -h MacOApp -
objdump命令:
objdump --macho --private-header MachOApp -
结果如下:
3. Load Command
Load Command 包含 Mach-O里的命令类型信息,名称和二进制文件的位置(类似一本书的目录)。同样可以使用 otool 或 objdump 命令来查看
- otool:
otool -v -l a.out - objdump:
objdump --macho --private-headers MachOApp(注意这里是 headers) - 结果如下:
4. Data
Data由Segment的数据组成,是Mach-O占比最多的部分,有代码有数据,比如符号表(类似一本书的具体内容)。Data共三个Segment,__TEXT、__DATA、__LINKEDIT。其中__TEXT和__DATA对应一个或多个Section,__LINKEDIT没有Section,需要配合LC_SYMTAB来解析symbol table和string table。这些里面是Mach-O的主要数据。- 使用
xcrun size命令来查看Data内容的分部- xcrun size 命令:
xcrun size -x -l -m MachOApp - 结果如下:
可以看到
__objc前缀的都是Objective-C相关的
- xcrun size 命令:
链接器
连接器的本质
- 链接的本质就是吧多个目标文件组成一个文件
1. 目标文件、最终文件生成的过程
1.1 目标文件
- 链接器
llvm_ld并没有被执行 - 目标文件不会包含
Unix程序在被装载和执行时所必须包含的信息
1.2 最终文件
2. 代码辅助
在 main.m 文件中写如下代码:
// 全局变量
int global_uninit_value;
int global_init_value = 10;
double default_x __attribute__ ((visibility("hidden")));
// 静态变量(即本地变量,当前文件可见)
static int static_init_value = 9;
static int static_uninit_value;
int main(int argc, const char * argv[]) {
static_uninit_value = 10;
NSLog(@"%d", static_init_value);
return 0;
}
然后运行项目,得到项目的 MachO 文件,在查看MachO文件中__TEXT。通过命令:objdump --macho -d ${MACHO_PATH}(这里用的M1的本,所以汇编是arm架构的)
和上面的比较发现多了很多东西,如:NSLog此时变成了一条指令 bl(跳转指令)地址 0x100003f84
也就是说在编译的时候:
* 把能变成汇编的先变成回汇编,即 机器码。
* 把属性转成符号,进行归类然后放入重定位符号表(重定位符号表就是放.m/.o用到的API)
* .o -> 连接器 -> 一张表 -> 可执行文件exec
之所以要放入重定位符号表,是因为在生成.o文件时,其地址还没虚拟化,连接器在进行链接时,会对重定位符号表 进行 合并。
通过上面我们可知 链接 就是 处理目标文件的符号的过程
符号(Symbol)
通过指令objdump --macho --syms ${MACHO_PATH}来查看符号表
下面对这些符号进行了总结:
l:locol 本地符号g:global 全局符号d:Debug 符号o:DataF:Function
按功能分:
| Type | 说明 |
|---|---|
| f | file |
| F | Function |
| O | Data |
| d | Debug |
| ABS | Absolute |
| COM | Common |
| UND | ? |
按符号种类分:
| Symbol Type | 说明 |
|---|---|
| U | undefined(未定义) |
| A | Absolute(绝对符号) |
| T | text section symbol(__TEXT.__text) |
| D | data section symbol(__DATA.__data) |
| B | bss section symbol(__DATA.__bss) |
| C | common symbol(只能出现在MH_OBJECT类型的Mach-O文件中) |
| - | debugger symbol table |
| S | 除了上面所述的,存放在其它section的内容,例如:未初始化的全局变量存放在(__DATA.__common)中 |
| I | indirect symbol(符号信息相同,代表同一符号) |
| u | 动态共享库中的小写u表示一个未定义引用对同一个库中另一个模块中私有外部符号 |
1. 导入、导出符号
我们知道 NSLog是Foundation框架内的(下图23行);对main.m来说是导入了NSLog符号,对Foundation来说是导出了NSLog符号,让其它地方使用(导出符号有是全局符号)
通过命令objdump --macho -exports-trie ${MACHO_PATH}来查看main.m中的导出符号。结果如下图:
我们看到有
导出符号有4个,它们正好对应上面打印符号表中的4个‘g’标识的符号。这正好说明全局符号默认为导出符号,可以在其它地方使用。
下面我们创建一个OC类,再查看其导出符号(这里创建了YJTestObject,并添加到编译源中)
再次通过命令objdump --macho -exports-trie ${MACHO_PATH}来查看main.m中的导出符号。结果如下图:
OC类默认为导出符号
间接符号表
我们知道动态库是在运行时加载,也就意味着在编译、链接阶段只需要提供动态库符号(导出)就可以了。间接符号表中存放的就是项目中使用到的动态库中的导出符号。
下面通过命令objdump --macho --indirect-symbols ${MACHO_PATH}来查看间接符号表:
这里就只认识
NSLog,这个是Foundation提供的导出符号
总结
-
全局符号默认导出符号,可供外界使用 -
间接符号不能删除,即动态库全局符号不能删除,在strip动态库时,不能strip全局符号 -
OC类在编译时都会默认为导出符号,那么我们在写OC动态库时,是否可以改变默认的导出为不导出从而使动态库的体积变小些?答案是可以的- 通过设置 Other Linker Flags 来实现:
OTHER_LDFLAGS=$(inherited) -Xlinker -unexported_symbol -Xlinker _OBJC_CLASS_$_YJTestObject - 再次查看导出符号表:
发现这次没有
_OBJC_CLASS_$_YJTestObject了,相同的方法可以指定其它符号也不导出
- 通过设置 Other Linker Flags 来实现:
2. Weak Symbol
Weak Symbol分两种:
-
Weak Reference Symbol:表示此未定义符号是弱引用。如果动态链接器找不到该符号的定义,则将其设置为0。链接器会将此符号设置弱链接标志。 -
Weak defintion Symbol:表示此符号为弱定义符号。如果静态链接器或动态链接器为此符号找到另一个(非弱)定义,则弱定义将被忽略。只能将合并部分中的符号标记为弱定义。
2.1 Weak Reference Symbol
使用**__attribute__** ((weak_import))来标识弱引用符号(这里还只是声明,没有定义)
代码中应用:
运行项目发现报错了:
这里为什么会报错呢?因为:我们在说
编译链接原理的时候说过,符号怎么来查找的呢?我们在WeakImportSymbol.h写了声明,在main函数中使用,就是用的API,但是在链接的时候,我们需要知道符号具体的地址在什么地方,否则提示找不到
我们可以告诉链接器,我这个符号是动态链接的,不需要管它的具体位置即使它是弱引用的,到时候dyld运行起来,自己会查找的
通过-U参数来告诉连接器指定符号没有定义是可以的:
OTHER_LDFLAGS=$(inherited) -Xlinker -U -Xlinker _weak_import_function
再次运行发现ok了,那么这个有什么用处呢?比如:我们可以判断其它库里,有没有这个符号,有这个符号我就调用,没有这个符号我就不调用。还有个用途就是在动态库上,我们可以将整个动态库文件声明成一个弱引用,这个有什么好处呢?也就意味着如果你这个库没有导入的话,也不会报动态库找不到的错误。
2.2 Weak defintion Symbol
使用**__attribute__** ((weak))来标识弱定义
上面我们讲到,弱定义符号:
如果链接器找到另一个非弱定义,则弱定义将被忽略,下面通过代码来看看具体效果:
正常情况下,由于方法名相同,运行应该会报错,但是由于这个方法被弱定义,此时编译不会报错
在main函数中调用,看输出结果:
我们看到输出结果是执行 main.m 中定义的 weak_function方法,并没有执行WeakSymbol.m中的weak_function,也就是上面说的链接器为此符号找到了非弱定义,则弱定义自动忽略
3. 重新导出符号
当我们NSLog在main函数中使用,当我想让其它项目使用这个main.m时,也能够使用NSLog,这就需要我们对NSLog进行重新导出(其实在Foundation中已经对NSLog做了重新导出,否则外界是无法使用的)
当我们重新导出NSLog,需要对间接符号的符号起别名,通过命令OTHER_LDFLAGS=$(inherited) -Xlinker -alias -Xlinker _NSLog -Xlinker _YJ_NSLog起别名:
我们看到这个YJ_NSLog变成了NSLog的别名,我们再看下导出符号表符号:
作用:在我们的动态库中链接另一个动态库的时候,其中一个动态库对你链接的程序是不可见的,我们就可以用这种重新导出方式让这个动态库可见,可以让一个符号可见,也可以让一个动态库可见
总结
通过上面的符号可以知道一下几点:
- 间接符号表中的符号不能删除,意味着
动态库中的全局符号不能删除,也就说明在strip动态库时,不能strip全局符号 静态库是.o文件合计以及重定位符号表,由于重定位符号不能删除,所以只能strip .o 文件中的调试符号
扩展
代码优化的三个阶段
-
生成目标文件时
-
dead code strip(死代码剥离)链接时
-
strip 剥离符号(操作MacO)