Mach-O
Mach-O是Mach Object的缩写,是Mac、iOS上用于存储程序、库的标准格式。属于Mach-O格式的文件类型有以下(具体可以在XNU源码中看到Mach-O的详细定义,详细地址)
-
MH_OBJECT-
目标文件(.O)
-
静态库文件(.a),静态库其实就是多个
.O的合集
-
-
MH_EXECUTE:可执行文件 -
MH_DYLIB:动态库(.dylib、.framework) -
MH_DYLINKER:动态链接编辑器(/usr/lib/dyld) -
MH_DSYM:存储着二进制符号信息的文件(.dSYM/Contents/Resources/DWARF/xx),常用于分析APP的崩溃信息
同样也可以在Xcode中查看target的Mach-O类型
Mach-O的基本结构
一个
Mach-O文件包含3个主要区域
- Header:文件类型,目标架构类型等信息
- Load Commands:描述文件在虚拟内存中的逻辑结构和布局
- Raw Segment Data:在
Load Commands中定义的Segment的原始数据
查看Mach-O的结构
- 命令行工具:
file、otool、lipo -info - GUI工具:
MachOView
使用objdump --macho -p [文件路径]命令查看mach-Header
同样可以使用otool -h [文件路径]命令查看,不过这时候有些字段的意思并不是可以直接阅读的
magic number
对于otool命令查看的magic中包含的意思可通过此表对照
| FEEDFACE | CEFAEDFE | FEEDFACF | CFFAEDFE | |
|---|---|---|---|---|
| IsLittleEndian(小端模式) | YES | NO | YES | NO |
| Is64Bits | NO | NO | YES | YES |
| MachO Type | MH_MAGIC | MH_CIGAM | MH_MAGIC_64 | MH_CIGAM_64 |
查看__TEXT
使用命令objdump -d [文件路径]查看代码段,指令具体含义如下
Tips:想查看objdump命令具体含义及使用参数可使用man objdump
编译链接的过程做了以下操作:
- 代码汇编化:将代码转变为汇编指令
- 符号进行归类,例如
NSLOG这类的外部符号归入重定位符号表 - 多个目标文件进行合并,同样把多张符号表也进行合并为一张表,最后生成可执行文件
- 那么链接的过程就是处理目标文件符号的过程
同样可以通过命令objdump --reloc [文件路径]来查看重定位符号表的信息
对照信息归类如下:
符号
全局符号、本地符号
正常演示
可以看到在两个文件中分别都有定义的staticFunc函数,那么在main函数中调用会不会编译不通过呢?答案是可以通过编译的,因为两个staticFunc都是本地符号,并不会冲突,分别只在自己的那个文件中起到作用,也就是类似于作用域那样,所以在调用的时候会采用就近原则来调用
同时可以在符号表中看到的确是本地符号
如果是全局符号的话便不能如此了,因为全局符号具有唯一性,再定义同名的全局符号会报出重复定义的错误,但是可以定义与全局符号同名的本地符号,但在调用时同样遵循就近原则来调用
这里调用的是本文件的同名本地符号
这里调用的就是全局符号了
调用结果打印如下
链接器中的-exported_symbol和-unexported_symbol
通过man ld可以找到-exported_symbol和-unexported_symbol的定义
可以通过xcconfig文件来使用
首先使用-unexported_symbol意思即为设置符号为本地符号OTHER_LDFLAGS = $(inherited) -Xlinker -S -Xlinker -exported_symbol -Xlinker _localFunc,如果符号本身就是本地符号则没有效果,如果是全局符号则再次查看符号表会发现已经转变为本地符号
-exported_symbol的用法是后面跟的参数是全局符号,比如OTHER_LDFLAGS = $(inherited) -Xlinker -S -Xlinker -exported_symbol -Xlinker _OBJC_CLASS_$_StaticFunc,则会将除了_OBJC_CLASS_$_StaticFunc这个全局符号之外的符号都变为本地符号
使用__attribute__来定义符号的可见性
定义一些变量如下
使用命令objdump --macho --syms [文件路径]查看其符号表
上面定义的变量都可以在里面找到,分为两类,前面带g和l标识的,意思为本地符号和全局符号,变量前加了static来修饰的变为了本地符号,其余默认均为全局符号。
本地符号和全局符号的区别本质在于符号的可见性,如果加上__attribute__((visibility("hidden")))来修饰default_x变量,发现它会变为本地符号
visibility属性,控制文件导出符号,限制符号导出性
- 使用
default定义的符号将被导出 - 使用
hidden定义的符号将不被导出
对变量可见性进行修改:
修改之后符号表:
全局符号和本地符号的演示
创建一个framework和一个测试工程,并在framework中定义一个并未在其头文件中声明的方法,那么在测试工程中去调用这个方法会如何呢?
结果是成功调用了framework中的方法
如果在方法前加static修饰,编译时期便会报错:
那么如果在测试项目中写一个同方法名的方法再去调用,会调用framework中的方法还是会调用本项目的方法还是会报错呢?
结果是成功调用了本项目的方法,这是因为命名空间的问题。
二级命名空间和一级命名空间(two_levelnamespace & flat_namespace)
链接器默认采用二级命名空间,即除了会记录符号名称,还会记录符号是属于哪一个MachO的,比如会记载_NSLog来自Foundation。
链接器对于二级命名空间的注解
所以在上面的举例中,会直接调用到本项目的方法而不会报错。
特殊情况演示
场景:如果对方给出两个静态库,其中有相同的符号名,这样会产生冲突,这里简单模拟这种情况
在Cat和Dog中创建相同名称的类的实现
这样在链接会报出以下错误
这是因为发现了两个同名的全局符号一定会产生冲突,这里可以通过llvm-objcopy工具来对其中一个进行改名操作即可,即为--redefine-sym=old=new来修改
llvm-objcopy --redefine-sym='_OBJC_CLASS_$_Cat'='KKK_OBJC_CLASS_$_Cat' --redefine-sym='_OBJC_METACLASS_$_Cat'='KKK_OBJC_METACLASS_$_Cat' /Users/kkk/Library/Developer/Xcode/DerivedData/Muti-gtbdrmjdsuftegahaajkzluhhguj/Build/Intermediates.noindex/MachOAndSymbol.build/Debug/MachOAndSymbol.build/Objects-normal/arm64/Dog.o
再使用nm -pa查看符号表时发现已经改变了
这时不可再次进行编译,直接使用run without building就可以了,这里仅仅是模仿现实中对方给出两个静态库,是无法修改源码的情况下可以这样操作
导入符号&导出符号
我们经常使用的NSLog是属于Foundation库的,那么对于我们使用到它的文件或者项目来说,NSLog就是一个导入符号,同样对于Foundation来说它是一个导出符号,那么它是要出现在项目中的间接符号表里,这里通过objdump --macho --indirect-symbols [文件路径]进行查看
所以基本可以说全局符号就是导出符号,这里会有一个问题,如果你有制作一个动态库,那么在不经任何处理的情况下,是不是你所有的全局符号都会被导出?
Objc类是默认是导出符号
我们在项目里随意增加一个Objc的类,进行编译后来通过命令objdump --macho --exports-trie [文件路径]查看导出符号表
增加前:
增加后:
可以看出Objc的类在未经任何处理的情况下,默认是添加到导出符号表里的,但是可以通过链接器的一些参数设置来把它变为不导出。
OTHER_LDFLAGS=$(inherited) -Xlinker -unexported_symbol -Xlinker _OBJC_CLASS_$_TestObject
这样设置就可以不导出
弱符号(weak symbol)
弱符号可以分为:弱定义符号和弱引用符号,下面就分别来介绍
弱定义符号(weak definition symbol)
弱定义符号表示此符号是弱定义符号,如果静态链接器或者动态链接器为此符号找到一个另一个非弱定义,则弱定义会被忽略。只能将合并部分中的符号标记为弱定义。
先来看这样一个例子
很显然这样编译后会报错,也是一个经典错误:重复定义的符号错误
但是如果这时候对这个符号定义为弱定义符号
结果就不会再次报错,而且会调用main中的函数实现
Tips
告诉编译器将符号文件信息输出到某个文件中OTHER_LDFLAGS=$(inherited) -Xlinker -S -Xlinker -map -Xlinker [输出文件路径]
弱引用符号(weak reference symbol)
弱引用符号:表示此未定义符号是弱引用。如果动态链接器找不到该符号的定义,则将其设置为0。链接器会将此符号设置为弱链接符号。
使用weak_import来修饰,并且未在任何地方作出weak_import_func函数的定义
在main函数中直接调用
运行程序发现报错,指出并未找到该符号
这时候只需要告诉链接器,这个符号属于动态链接,运行的时候会自己找它的符号,如果没找到则置为0。
OTHER_LDFLAGS=$(inherited) -Xlinker -U -Xlinker _weak_import_func
这样做对于动态库来说会更加的灵活。例如链接器指令中可以将整个库设置为弱引用
符号的重新导出
从前面的分析可以知道_NSLog符号是Foundation的导出符号,那么在我们程序中,它会存在于间接符号表中
给链接器设置一些参数可以对_NSLog这样的符号重新导出,使用命令OTHER_LDFLAGS=$(inherited) -Xlinker -alias -Xlinker _NSLog -Xlinker HD_NSLog,相当于给_NSLog起了一个别名HD_NSLog
可以看到文档里标注不但可以给单个符号起别名,同样可以传入文件以应对多个符号起别名的情况。
那么现在可以通过命令objdump --macho --exports-trie查看导出符号表
或者通过命令nm -m [文件路径] | grep "HD"
这样重新导出的符号可以不直接对某个动态库引用而直接调用我们这边重新导出的符号即可。
swift 符号
前面一直分析的都是Objc的符号,现在来简单看一下Swift的符号
使用命令objdump --macho --syms ${文件路径} | grep 'SwiftClassSymbol'
同时也查看一下导出符号表
进行以下修改
再次查看符号表信息
之前全局符号基本变成了本地符号,也验证了Swift是静态语言,很多东西在编译的时候就已经确定了。
补充
在之前查看符号表的过程中经常可以看到l、g等等这样的标记,现在来总结归类以下
按功能区分:
| Type | 说明 |
|---|---|
| f | File |
| F | Function |
| O | Data |
| d | Debug |
| *ABS* | Absolute |
| *COM* | Commen |
| *UND* | ? |
按符号种类划分:
①:小写代表local symbol
| Symbol Type | 说明 |
|---|---|
| U | undefined(未定义) |
| A | Absolute(绝对符号) |
| text section symbol(__Text.__text) | |
| data section symbol(__ Data.__ data) | |
| bss section symbol(__ Data.__ bss) | |
| C | common symbol(只出现在MH_OBJECT类型的MachO文件中) |
| - | debugger symbol table |
除了上面所述的,存放在其他section的内容,例如未初始化的全局变量存放在(__ Data.__ common)中 | |
| I | indirect symbol(符号信息相同,代表同一符号) |
| u | 动态共享库中小写u表示一个未定义引用对同一库中另一个模块中私有外部符号 |