MACH-O文件格式
Mach Header
的最开始是 Magic Number
,表示这是一个 Mach-O
文件,除此之外还包含一些Flags
,这些flags
会影响 Mach-O
的解析。
Mach-O
中的Load Command __TEXT
中记录了代码的大小、第一行代码的起始位置,dyld
根据这些信息就能读取到__TEXT
代码段中的代码。由于Mach-O
中都是二进制数据,因此dyld根据结构体内存对齐规则逐个读取到Load Command
。
作为Mac OS X
程序的二进制接口(ABI),Mach-O
文件格式提供中间物(构建过程中产生的)和最终存储有机器码和数据的文件。它被设计成一个灵活的BSD a.out
的替代品。这种文件被编译器和静态链接器使用,并在运行时包含静态链接的可执行代码。随着Mac OS X
目标的发展,动态链接的特性也被添加进来,从而为静态链接和动态链接的代码形成了单一的文件格式。
每个Mach-O
文件的开始部分都有一个Header
结构,这个Header
标志这个文件是Mach-O
文件。这个头部也包含了其他基础文件类型信息,标明目标体系结构,包含指定选项的标志,这些标志会影响对文件剩余部分的解释。
在header之后是一系列大小可变的加载命令,它们指定文件的布局和链接特征。在其他信息中,load命令可以指定:
- 文件在虚拟内存中的初始化布局
- 符号表的位置(被用来动态链接使用)
- 程序主线程的初始执行状态
- 包含主可执行文件导入符号定义的共享库的名称
在加载指令之后,所有的Mach-O
文件都包含一个或多个段的数据。每个段有零个或多个section
,段的每个section
都包含特定类型的代码或数据。每个段定义了虚拟内存区域,被连接器用来映射到进程的地址空间中,段和section
的确切数量和布局由load
命令和文件类型指定。
Mach-O
文件中,最后一个段是链接编辑段。此段包含链接编辑信息的表,如符号表、字符串表等,动态加载程序使用这些信息将可执行文件或Mach-O
包链接到其附属库。
stabs
取名于symbol table strings
,因为开始的时候,调试信息是以字符串的形式存储在Unix的a.out目标文件的符号表中。 stabs以字符串的形式编码程序的信息。最开始的时候,stabs很简单,但是后来变得越来越复杂,难解,而且不一致。此外,stabs没有形成标准,文档也不够详细。
DWARF
已经被广泛使用,包括GCC
和LLVM
。DWARF也是基于嵌套结构存储调试信息。
段(segment)
段定义了Mach-O
文件中的一个字节范围,以及地址和内存保护属性,当动态链接器加载应用程序时,这些字节被映射到虚拟内存中。因此,段总是与虚拟内存页对齐。一个段包含零个或多个节。
- 静态连接器会创建一个
__PAGEZERO
段作为可执行文件的第一个段。这个段位于虚拟内存的0位置,而且没有分配任何保护权限,它们的组合会导致对NULL
的访问,这是一种常见的C编程错误,会立即崩溃。__PAGEZERO
段对于现在的架构就是一页虚拟内存页的大小(对于基于Intel和Power-PC内核的Mac计算机,一般是4096字节或者十六进制0x1000).因为__PAGEZERO
段没有数据,在文件中没有占用任何空间(段命令中的文件大小是0) -
__TEXT
段包含了可执行代码和只读数据。允许内核直接从可执行文件映射到共享内存中,静态连接器设置这个段虚拟内存的权限为不允许写入。当段被映射到内存中时,它可以在所有对其内容感兴趣的进程之间共享。(这个主要用在frameworks
,bundles
,和共享库,但是可以在Mac OS X
中运行同一可执行文件的多个副本,这也适用于这种情况。)只读属性也意味着这些内存也组成的__TEXT
段是不可以会写到磁盘中的。当内核需要释放物理内存的时候,他可以放弃一个或多个__TEXT
,如果下次有需要,就从磁盘重新读取他们 -
__DATA
段包含的是可写数据。静态连接器设置这段虚拟内存的权限为可读可写。因为是可写的,因为它是可写的,所以框架或其他共享库的数据段在逻辑上被复制到与该库链接的每个进程。当组成__DATA
段的内存页可读可写时,内核将它们标记为“写时复制”;因此当一个进程写入这些页的其中一页时,该进程会收到当前进程私有的当前页备份。
*__OBJC
段包含了OC语言runtime支持库所用到的数据。 -
__IMPORT
段包含了符号桩和指向可执行文件中未定义的符号的非懒加载指针。这个段仅在IA-32架构的目标可执行文件中生成。 -
__LINKEDIT
段包含了动态连接器所用到的原始数据,例如符号、字符串、和重定位表记录
MACHO格式
查看MACHO的命令
1.使用objdump查看mach-header
MACH_PATH=${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/${PRODUCT_NAME}
CMD = objdump --macho -private-header ${MACH_PATH}
2.使用otool查看mach-header
otool -l ${MACH_PATH}
otool命令执行的结果可读性要差很多。
3.查看 macho __text代码段
objdump --macho -d ${MACH_PATH}
Xcode的编译过程
编译时做哪些工作:
将代码汇编化,变成汇编代码或机器码。
将代码进行归类,比如数据放在数据段,代码放到代码段;将动态库里面的符号先暂存起来。归类后,将这些信息放到重定位符号表。
为什么要放到重定位符号表?
在生成.o文件的时候,整个目标文件的地址没有虚拟化,没有变成虚拟内存的地址。
有些动态链接的外部符号没法找到,就在编译阶段暂时将它放到重定位符号表。
查看.o文件中需要重定位符号的命令
objdump --macho --reloc test.o
重定位符号表保存了当前项目中用到的符号,可以通过检测.o文件的重定位符号表,来查看程序中对某种API的使用情况。
链接过程
处理整个编译情况,将多个目标文件.o合并到一起,就意味着每个目标文件的符号表和重定位符号表,最终都会合并到一张表中。最后再生成可执行文件EXEC
。
链接的过程就是处理目标文件符号的过程。
符号
全局符号与本地符号
按全局符号和本地符号的类别来查看文件符号
CMD = objdump --macho --syms ${MACH_PATH}
代码中定义的全局变量,在MACHO中
都是全局符号。不管是初始化或者未初始化的,都是全局符号。所有Static
开头的,都变成了本地符号。全局符合和本地符号的区别在于可见性。
OC中可见性默认有两种:1、hidden
2、default
__attribute__
将编译器支持的参数传递给编译器。
Visibility
编译器上符号可见性设置
int hidden_y __attribute__((visibility("hidden"))) = 99;
double default_y __attribute__((visibility("default"))) = 100;
int global_init_value = 10;
double default_x __attribute__((visibility("hidden")));
//// 静态变量 -> 本地变量
static int static_init_value = 9;
static int static_uninit_value;
全局符号对整个项目可见,本地符号仅对当前文件可见。链接器默认采用二级命名空间,记录符号是属于哪个MACHO。
导入符号与导出符号
1.查看导出符号的命令
objdump --macho -exports-trie ${MACHO_PATH}
全局符号默认会生成导出符号。但是可以通过链接器管理它。
2.查看间接符号表
objdump --macho --indirect-symbols ${MACH_PATH}
间接符号表不可能被删除,也就是动态库中所有全局符号,都不能被删除。所有的全局符号变导出符号都无法被删除。再去脱符号时,也无法脱去全局符号。
3.OC中的符号
可以通过执行导出符号命令,知道OC默认都是导出符号。即使没有在.h文件中声明,也一样是导出符号。
如果都是导出符号会产生什么问题?最终生成的IPA包会很大。如果要缩减包体积,我们要怎么做?
可以借助链接器,将不想暴露的符号声明为不导出符号
OTHER_LDFLAGS = $(inherited) -Xlinker -unexported_symbol -Xlinker _OBJC_CLASS_$_ViewController
OTHER_LDFLAGS = $(inherited) -Xlinker -unexported_symbol -Xlinker _OBJC_METACLASS_$_ViewController
弱引用与弱定义符号
1.弱引用符号
Weak Reference Symbol
: 表示此未定义符号是弱引用。如果动态链接器找不到该符号的定义,则将其设置为0。链接器会将此符号设置弱链接标志。
// 弱引用
void weak_import_function(void) __attribute__((weak_import));
实际需求:当使用别人的符号时,这个符号有可能实现,也有可能不会实现。弱引用的作用是让编译能正常。在执行的时候再去寻找它的实现。
实际场景:使用__attribute__((weak_import))
将weak_import_function
声明为若引用符号
此时项目中没有weak_import_function
函数的实现
#import <Foundation/Foundation.h>
#import "WeakImportSymbol.h"
int main(int argc, char *argv[]) {
if (weak_import_function) {
weak_import_function();
}
return 0;
}
此时编译报错,提示未定义符号。
当导入.h
头文件并使用符号时,类似于API
的使用,只要找到符号的声明即可。即使函数没有被实现,也可以生成目标文件。但链接生成可执行文件时,需要知道符号的具体位置,如果函数没有被实现,会出现错误提示:未定义符号。
设置不检测弱引用符号
OTHER_LDFLAGS = $(inherited) -Xlinker -U -Xlinker _weak_import_function
通过-U
参数,告诉链接器此符号是动态链接的,所以在链接阶段,即使它是未定义符号,忽略,不用管它。因为在运行时,动态链接器会自动找到它
此时项目可以正常编译成功,dyld
运行起来的时候会自动寻找相应的符号。因为main
函数中调用weak_import_function
函数之前有if (weak_import_function)
的判断;当动态链接器找不到该符号的定义,则将其设置为0。所以weak_import_function
函数并不会被调用。
弱引用符号的作用
- 将一个符号声明为弱引用符号,可以避免编译链接时报错。在调用之前增加条件判断,运行时也不会报错
- 使用动态库的时候,可以将整个动态库声明为弱引用,此时动态库即使没有被导入,也不会出现未找到动态库的错误
2.弱定义符号
Weak defintion Symbol
: 表示此符号为弱定义符号。如果静态链接器或动态链接器为此符号找到另一个(非弱)定义,则弱定义将被忽略。只能将合并部分中的符号标记为弱定义。
//弱定义
void weak_function(void) __attribute__((weak));
使用__attribute__((weak))
将weak_function
声明为弱定义符号。
#import "WeakSymbol.h"
#import <Foundation/Foundation.h>
void weak_function(void) {
NSLog(@"weak_function");
}
此时weak_function
是一个全局符号,同样也是导出符号。弱定义并不影响它的全局属性。
实际场景:在WeakSymbol.m
和main.m
中,都实现一个weak_function
函数。同一个Project中,出现两个相同的全局符号,此时编译报错,提示出现重复符号。
将其中一个weak_function
函数声明为弱定义符号,此时编译成功。
弱定义符号的作用:可以解决同名符号的冲突;链接器按照符号上下顺序,找到一处符号的实现后,其他地方的同名符号将被忽略。
Common Symbol
在定义时,未初始化的全局符号。例如:main.m
文件中,未初始化的global_uninit_value
全局变量,它就属于Common Symbol
。
打开main.m
文件,定义两个同名的全局变量,一个初始化,另一个不进行初始化,这种操作并不会报错。
int global_init_value = 10;
int global_init_value;
Common Symbol的作用
- 在编译和链接的过程中,如果找到定义的符号,会自动将未定义符号删掉
- 在链接过程中,链接器默认会把未定义符号变成强制定义的符号
** 链接器设置:**
-d
:强制定义Common Symbol
-commons
:指定对待Common Symbol
如何响应
重新导出符号
对于当前程序来说,NSLog属于系统动态库foundation的导出符号,存储在间接符号表中的未定义符号。
NSLog可以在当前程序使用,如果想让使用此程序的其他程序也能使用,就要将此符号重新导出。重新导出之后的符号会放在导出符号表中,此时才能被外界查看并使用。
-alias
:只能给间接符号表中的符号创建别名,别名符号具有全局可见性。
OTHER_LDFLAGS=$(inherited) -Xlinker -alias -Xlinker _NSLog -Xlinker Cat_NSLog
给_NSLog
符号创建Cat_NSLog
别名,使用nm -m ${MACH_PATH} | grep "Cat_NSLog"
命令查看符号表,指定"Cat_NSLog"关键字。此时Cat_NSLog
是一个间接外部符号,是_NSLog
符号的别名。使用objdump --macho --exports-trie ${MACH_PATH}
命令查看导出符号。Cat_NSLog为导出符号,并且标记为[re-export],代表重新导出符号。
重新导出符号的作用
- 将一个间接符号表中的符号声明为重新导出符号,可以让使用此程序的其他程序也能使用
- 当程序链接A动态库,而A动态库又链接B动态库时,B动态库对于程序来说是不可见的。此时可以使用重新导出的方式,让B动态库对程序可见
查看项目使用的三方库和符号等信息
通过链接器,可以查看当前项目中使用的三方库和符号等信息
-map
:将所有符号详细信息导出到指定文件。打开xcconfig
文件,添加OTHER_LDFLAGS
配置项
OTHER_LDFLAGS=$(inherited) -Xlinker -map -Xlinker $(PROJECT_DIR)/export.txt
文件内包含了编译链接时生成的目标文件,项目中使用的三方库,还包含项目中的Sections和Symbols等信息
控制台断点相关的命令
- br read -f 断点文件路径 读取
- br write -f 断点文件路径 写入
- br list strip 断点加入到strip分组
- br enable strip 开启strip分组的断点