Mach-O与链接器

590 阅读6分钟

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文件;如下图:

qhb_02_01_dot_app_file.png

2. 查看Mach-O

Finder 中找到 MachOApp.app,右键显示包内容,可看到如下结果: qhb_02_02_macho_file.png

通过 file 命令来查看MachOApp文件类型(这里用的模拟器,所以是x86_64架构。): qhb_02_03_check_macho_type.png

3. Mach-O组成

1. Mach-O 结构图:

qhb_02_04_macho_strurct.png

从图中可以看出Mach-O主要分为三大部分:Mach HeaderLoad CommandData(__TEXT、__DATA、符号表)

2. Mach Header

Mach Header 里有 Mach-OCUP信息,以及Load Command的信息。可以使用 otoolobjdump 命令来查看

  • otool命令:otool -v -h MacOApp

  • objdump命令:objdump --macho --private-header MachOApp

  • 结果如下:

    qhb_02_06_objdump_header.png

3. Load Command

Load Command 包含 Mach-O里的命令类型信息,名称和二进制文件的位置(类似一本书的目录)。同样可以使用 otoolobjdump 命令来查看

  • otool:otool -v -l a.out
  • objdump: objdump --macho --private-headers MachOApp(注意这里是 headers)
  • 结果如下:

qhb_02_07_load_command.png

4. Data
  • DataSegment 的数据组成,是 Mach-O 占比最多的部分,有代码有数据,比如符号表(类似一本书的具体内容)。
  • Data 共三个 Segment__TEXT、__DATA、__LINKEDIT。其中 __TEXT__DATA 对应一个或多个 Section__LINKEDIT 没有 Section,需要配合 LC_SYMTAB 来解析 symbol tablestring table。这些里面是 Mach-O 的主要数据。
  • 使用 xcrun size 命令来查看 Data 内容的分部
    • xcrun size 命令:xcrun size -x -l -m MachOApp
    • 结果如下: qhb-02_08_macho_data.png 可以看到 __objc 前缀的都是 Objective-C 相关的

链接器

连接器的本质

  • 链接的本质就是吧多个目标文件组成一个文件

1. 目标文件、最终文件生成的过程

1.1 目标文件

qhb_02_09_target_file_generate.png

  • 链接器llvm_ld并没有被执行
  • 目标文件不会包含Unix程序在被装载和执行时所必须包含的信息
1.2 最终文件

qhb_02_10_final_file_generate.png

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架构的) qhb_02_11_macho_text.png

和上面的比较发现多了很多东西,如:NSLog此时变成了一条指令 bl(跳转指令)地址 0x100003f84

也就是说在编译的时候: * 把能变成汇编的先变成回汇编,即 机器码。 * 把属性转成符号,进行归类然后放入重定位符号表(重定位符号表就是放.m/.o用到的API) * .o -> 连接器 -> 一张表 -> 可执行文件exec

之所以要放入重定位符号表,是因为在生成.o文件时,其地址没虚拟化连接器进行链接时,会对重定位符号表 进行 合并

通过上面我们可知 链接 就是 处理目标文件的符号的过程

符号(Symbol)

通过指令objdump --macho --syms ${MACHO_PATH}来查看符号表 qhb_02_12_symbol_table.png

下面对这些符号进行了总结:

  • l:locol 本地符号
  • g:global 全局符号
  • d:Debug 符号
  • o:Data
  • F:Function

按功能分:

Type说明
ffile
FFunction
OData
dDebug
ABSAbsolute
COMCommon
UND?

按符号种类分:

Symbol Type说明
Uundefined(未定义)
AAbsolute(绝对符号)
Ttext section symbol(__TEXT.__text)
Ddata section symbol(__DATA.__data)
Bbss section symbol(__DATA.__bss)
Ccommon symbol(只能出现在MH_OBJECT类型的Mach-O文件中)
-debugger symbol table
S除了上面所述的,存放在其它section的内容,例如:未初始化的全局变量存放在(__DATA.__common)
Iindirect symbol(符号信息相同,代表同一符号)
u动态共享库中的小写u表示一个未定义引用对同一个库中另一个模块中私有外部符号

1. 导入、导出符号

我们知道 NSLogFoundation框架内的(下图23行);对main.m来说是导入NSLog符号,对Foundation来说是导出NSLog符号,让其它地方使用(导出符号有是全局符号qhb_02_13_xcode_01.png

通过命令objdump --macho -exports-trie ${MACHO_PATH}来查看main.m中的导出符号。结果如下图:

qhb_02_14_exports_trie.png 我们看到有 导出符号有4个,它们正好对应上面打印符号表中的4个‘g’标识的符号。这正好说明全局符号默认为导出符号,可以在其它地方使用。

下面我们创建一个OC类,再查看其导出符号(这里创建了YJTestObject,并添加到编译源中)

qhb_02_15_xcode_02.png

再次通过命令objdump --macho -exports-trie ${MACHO_PATH}来查看main.m中的导出符号。结果如下图: qhb_02_16_exports_trie_02.png OC类默认为导出符号

间接符号表

我们知道动态库是在运行时加载,也就意味着在编译、链接阶段只需要提供动态库符号(导出)就可以了。间接符号表中存放的就是项目中使用到的动态库中的导出符号

下面通过命令objdump --macho --indirect-symbols ${MACHO_PATH}来查看间接符号表: qhb_2_17_indirect_symbol.png

这里就只认识NSLog,这个是Foundation提供的导出符号

总结
  • 全局符号默认导出符号,可供外界使用

  • 间接符号不能删除,即动态库全局符号不能删除,在strip动态库时,不能strip全局符号

  • OC类在编译时都会默认为导出符号,那么我们在写OC动态库时,是否可以改变默认的导出为不导出从而使动态库的体积变小些?答案是可以的

    • 通过设置 Other Linker Flags 来实现: OTHER_LDFLAGS=$(inherited) -Xlinker -unexported_symbol -Xlinker _OBJC_CLASS_$_YJTestObject
    • 再次查看导出符号表:
      qhb_02_18_exports_trie_03.png 发现这次没有_OBJC_CLASS_$_YJTestObject了,相同的方法可以指定其它符号也不导出

2. Weak Symbol

Weak Symbol分两种:

  • Weak Reference Symbol:表示此未定义符号弱引用。如果动态链接器找不到该符号定义,则将其设置为0链接器会将此符号设置弱链接标志

  • Weak defintion Symbol:表示此符号为弱定义符号。如果静态链接器动态链接器为此符号找到另一个(非弱)定义,则弱定义将被忽略。只能将合并部分中的符号标记为弱定义

2.1 Weak Reference Symbol

使用**__attribute__** ((weak_import))来标识弱引用符号(这里还只是声明,没有定义) qhb_02_19_weak_reference.png 代码中应用: qhb_02_20_use_weak_reference.png 运行项目发现报错了: qhb_02_21_weka_reference_error.png 这里为什么会报错呢?因为:我们在说编译链接原理的时候说过,符号怎么来查找的呢?我们在WeakImportSymbol.h写了声明,在main函数中使用,就是用的API,但是在链接的时候,我们需要知道符号具体的地址在什么地方,否则提示找不到

我们可以告诉链接器,我这个符号是动态链接的,不需要管它的具体位置即使它是弱引用的,到时候dyld运行起来,自己会查找

通过-U参数来告诉连接器指定符号没有定义是可以的: OTHER_LDFLAGS=$(inherited) -Xlinker -U -Xlinker _weak_import_function

再次运行发现ok了,那么这个有什么用处呢?比如:我们可以判断其它库里有没有这个符号这个符号我就调用没有这个符号我就不调用。还有个用途就是在动态库上,我们可以将整个动态库文件声明成一个弱引用,这个有什么好处呢?也就意味着如果你这个库没有导入的话,也不会报动态库找不到的错误

2.2 Weak defintion Symbol

使用**__attribute__** ((weak))来标识弱定义 qhb_02_22_weak_defintion.png 上面我们讲到,弱定义符号:如果链接器找到另一个非弱定义,则弱定义将被忽略,下面通过代码来看看具体效果:

qhb_02_23_weak_defintion_02.png

正常情况下,由于方法名相同,运行应该会报错,但是由于这个方法被弱定义,此时编译不会报错

main函数中调用,看输出结果: qhb_02_24_weak_refintion_03.png

我们看到输出结果是执行 main.m 中定义的 weak_function方法,并没有执行WeakSymbol.m中的weak_function,也就是上面说的链接器为此符号找到了非弱定义,则弱定义自动忽略

3. 重新导出符号

当我们NSLogmain函数使用,当我想让其它项目使用这个main.m时,也能够使用NSLog,这就需要我们对NSLog进行重新导出(其实在Foundation已经对NSLog做了重新导出否则外界是无法使用的

当我们重新导出NSLog,需要对间接符号的符号起别名,通过命令OTHER_LDFLAGS=$(inherited) -Xlinker -alias -Xlinker _NSLog -Xlinker _YJ_NSLog起别名: qhb_02_25_rereport.png

我们看到这个YJ_NSLog变成了NSLog的别名,我们再看下导出符号表符号:

qhb_02_26_exprots_trie_06.png

作用:在我们的动态库中链接另一个动态库的时候,其中一个动态库对你链接程序不可见的,我们就可以用这种重新导出方式让这个动态库可见,可以让一个符号可见,也可以让一个动态库可见

总结

通过上面的符号可以知道一下几点:

  1. 间接符号表中的符号不能删除,意味着动态库中的全局符号不能删除,也就说明在strip动态库时,不能strip全局符号
  2. 静态库.o文件合计以及重定位符号表,由于重定位符号不能删除,所以只能strip .o 文件中的调试符号

扩展

代码优化的三个阶段

  • 生成目标文件时 qhb_01_code_strip_targetFile.png

  • dead code strip(死代码剥离)链接时 qhb_01_code_strip_linking.png

  • strip 剥离符号(操作MacO) qhb_01_code_strip_maco.png