1. Mach-O与链接器
Mach-O(Mach Object)
是MacOS,iOS,iPadOS存储 程序和库的文件格式
eg:动态库,静态库。
对应系统通过应用 二进制接口(application binary interface,缩写为ABI)
来运行该格式的的文件。
Mach-O格式用来替代BSD系统的a.out格式。Mach-O文件格式保存了在 编译过程
和 链接过程
中产生的 机器代码和数据
,从而为静态链接和动态链接的代码提供了单一文件格式。
程序编译流程:链接
的本质就是 把多个目标文件组合成一个文件
链接器
主要是 配置一些参数,一些符号参数
,eg:怎么把导出符号隐藏了,通过链接器配置符号的可见性
可执行文件的调用过程
- 调用
fork
函数,创建一个process
- 调用
execve
或其衍生函数,在该进程上加载,执行我们的Mach-O
文件 - 当我们调用
execve(程序加载器)
,内核实际上在执行以下操作:
- 将文件夹杂到内存
- 开始分析
Mach-O
中的mach-header
,以确认它是有效的Mach-O
文件
2. 查看Macho文件
2.1 Macho 文件
其中Macho Header的结构
在64位系统下Macho Header的结构:
/*
* The 64-bit mach header appears at the very beginning of object files for
* 64-bit architectures.
*/
struct mach_header_64 {
uint32_t magic; /* mach magic number identifier */
cpu_type_t cputype; /* cpu specifier */
cpu_subtype_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 */
};
32位系统下Macho Header的结构:
/*
* The 32-bit mach header appears at the very beginning of the object file for
* 32-bit architectures.
*/
struct mach_header {
uint32_t magic; /* mach magic number identifier */
cpu_type_t cputype; /* cpu specifier */
cpu_subtype_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 */
};
2.2 查看Macho headers
objdump --macho --private-headers /Users/mac/Library/Developer/Xcode/DerivedData/LoginApp-eqeqbztqkuqnligyfeahplzeimtv/Build/Products/Debug-iphoneos/LoginApp.app/LoginApp
在Mach-O 中定义了程序的入口main函数(定义在LC_MAIN的Load command命令),告诉动态链接器去加载程序的入口
objdump --macho --private-headers /Users/mac/Library/Developer/Xcode/DerivedData/LoginApp-eqeqbztqkuqnligyfeahplzeimtv/Build/Products/Debug-iphoneos/LoginApp.app/LoginApp | ag 'LC_MAIN' -A 3
如果遇到如下报错 请先安装 the_silver_searcher: 终端输入 brew install the_silver_searcher
2.3 解析可执行文件的Macho结构的命令
源码请移步 链接: https://pan.baidu.com/s/1SAqnepcUIvwdYr3WtSZrKQ 提取码: 3t7e
拷贝machinfo到常用目录 /Users/mac/Desktop,终端cd /Users/mac/Desktop
执行命令
./machoinfo '可执行文件路径’
也可以在machoinfo的工程中编辑Scheme,添加Run的Arguments,加入一个可执行文件的路径,此参数会被传递到main函数的第二个参数
2.4 配置 查看可执行文件结构并输出到终端 的环境
寻找终端
在终端输入
tty //输出终端的位置 /dev/tty006
输出重定向
echo "HT" > /dev/tty006
xcode_run_cmd.sh放到工程目录 ```objectivec
!/bin/sh
RunCommand() {
判断全局字符串VERBOSE_SCRIPT_LOGGING是否为空。-n string判断字符串是否非空
[[是 bash 程序语言的关键字。用于判断
if [[ -n “$VERBOSE_SCRIPT_LOGGING” ]]; then
#作为一个字符串输出所有参数。使用时加引号"$*" 会将所有的参数作为一个整体,以"$1 $2 … $n"的形式输出所有参数
if [[ -n "$TTY" ]]; then
echo "♦ $@" 1>$TTY
else
echo "♦ $*"
fi
echo "------------------------------------------------------------------------------" 1>$TTY
fi
与$*相同。但是使用时加引号,并在引号中返回每个参数。”$@” 会将各个参数分开,以”$1” “$2” … “$n” 的形式输出所有参数
if [[ -n “$TTY” ]]; then
echo $@ &>$TTY
else
“$@”
fi
显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误。
return $? }
EchoError() {
#在shell脚本中,默认情况下,总是有三个文件处于打开状态,标准输入(键盘输入)、标准输出(输出到屏幕)、标准错误(也是输出到屏幕),它们分别对应的文件描述符是0,1,2
# > 默认为标准输出重定向,与 1> 相同
# 2>&1 意思是把 标准错误输出 重定向到 标准输出.
# &>file 意思是把标准输出 和 标准错误输出 都重定向到文件file中
# 1>&2 将标准输出重定向到标准错误输出。实际上就是打印所有参数已标准错误格式
if [[ -n "$TTY" ]]; then
echo "$@" 1>&2>$TTY
else
echo "$@" 1>&2
fi
}
RunCMDToTTY() { if [[ ! -e “$TTY” ]]; then EchoError “==========================================” EchoError “ERROR: Not Config tty to output.” exit -1 fi
if [[ -n "$CMD" ]]; then
RunCommand "$CMD"
else
EchoError "=========================================="
EchoError "ERROR:Failed to run CMD. THE CMD must not null"
fi
}
RunCMDToTTY
在xcconfig中定义TTY,CMD<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/1994311/1611073451507-7c777fb5-a001-4cfe-91d6-e06757a7af9b.png#align=left&display=inline&height=207&originHeight=414&originWidth=2232&size=177895&status=done&style=none&width=1116)
- 创建 查看Macho header的xcconfig文件,并输入如下代码
```bash
CMD_FLAG=${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/${PRODUCT_NAME}
//objcdump --macho -private-header ${MACH_PATH}
//otool -h ${MACH_PATH}
CMD=objcdump --macho -private-header ${MACH_PATH}
TTY=/dev/tty006
- 在工程中的Debug下配置xcconfig
- 在
Build Phases
先的Run Script
中输入shell命令/bin/sh "$SRCROOT/xcode_run_cmd.sh"
点击编译,就可以在终端查看到macho header
2.5 查看可执行文件的的__TEXT段
在2.4的配置基础上修改xcconfig
在终端输出:
2.6 查看重定位符号表
汇编生成.o文件过程,内存还没有被虚拟化,没有形成虚拟内存,它所干的事是:
1.将源代码能汇编,生产汇编指令,并对照ABI生成机器指令
2.符号归类,API的符号放到重定位符号表中
在2.4配置的基础上修改xcconfig文件
//查看.o文件中的重定位符号表
MACH_PATH=${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/${PRODUCT_NAME}
CMD = objdump --macho --reloc test.o
TTY=/dev/ttys000
2.7 查看符号表
在2.4配置的基础上修改xcconfig文件
MACH_PATH=${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/${PRODUCT_NAME}
//查看符号表
CMD = objdump --macho --syms ${MACH_PATH}
TTY=/dev/ttys006
终端输出结果:
可以给全局符号添加hidden属性,变成本地符号 double defaultx ; 这样定义的符号是全局符号 g double defaultx __attribute((visibility(“hidden”))); 全局符号添加hidden属性后变成了本地符号 l
补充attribute给编译器传参数,参数的可选值:
- “hidden”
- “default”
- “protected”: 注意clang不支持protected
- always_inline : 内联
- cleanup(ASUnlockSet) : 结束时清理函数
- deprecated : 标记方法被废除
2.8 查看整个Macho的符号链接的信息
在2.4的配置下,修改xcconfig文件
// -map 查看整个Macho的符号和导出的信息
OTHER_LDFLAGS=$(inherited) -Xlinker -S -Xlinker -map -Xlinker /Volumes/disk/ios V9.0/强化班/第一节、符号与链接(下)/上课代码/Symbol.text
3. 符号的种类与作用
Symbol Table:就是用来保存符号的表(符号叫什么+符号地址)
String Table: 就是用来保持符号的名称的表
Indirect Symbol Table:间接符号表。保存使用的外部符号(eg:NSLog)。更准确一点就是使用的外部动态库的符号。是Symbol Table的子集。
读取符号表是通过读取LC_SYMTAB的Load Command,并找到符号表的内存地址和长度还获取的
3.1 分类
- 按照存在的空间区分:
non private external
weak private external
- 按照模块区分:
weak global
weak local
‘ ‘
- 按照功能分:
f : File
F : Function
O : Data
d: Debug
ABS : Absolute
COM : Common
UND : ?
⚠️ 去除调试符号,配置xcconfig 调试符号:由汇编器生成.o文件时,会生成一个DWARF格式的调试信息,它会被放到DWARF段,在链接是,会将DWARF段放到符号表中,链接后所有的符号都放在符号表中
//去除编译符号
OTHER_LDFLAGS=$(inherited) -Xlinker -S
- 按照符号种类划分:
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表示一个未定义引用对同一库中另一模块中私有外部符号
3.2 导入(Import)导出(Export)符号
export symbol:导出符号意味着,告诉别的模块,有这样一个符号,你可以将其引入(Import)
符号直接影响api的体积
查看导出符号表:在2.4的配置基础上修改xcconfig文件
MACH_PATH=${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/${PRODUCT_NAME}
//查看导出符号表
CMD = objdump --macho --exports-trie ${MACH_PATH}
TTY=/dev/ttys006
查看间接符号表:在2.4的配置基础上修改xcconfig文件
MACH_PATH=${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/${PRODUCT_NAME}
//查看间接符号表
CMD = objdump --macho --indirect-symbols ${MACH_PATH}
TTY=/dev/ttys006
strip用来脱动态库的符号的时候,脱去的是非全局符号,也就是说导出符号不能被脱去,而全局符号都是导出符号
因此自定义动态库的时候,如果希望提交尽可能的小,我们需要把不想暴露的符号本地化,或者告诉编译器不导出符号
//不导出符号
OTHER_LDFLAGS = $(inherited) -Xlinker -unexported_symbol -Xlinker _OBJC_METACLASS_$_LGOneObject
⚠️ 不导出符号
OTHER_LDFLAGS = $(inherited) -Xlinker -unexported_symbol -Xlinker _OBJC_METACLASS_$_LGOneObject
和hidden属性修饰?将符号定义为不导出符号后,将变成一个local符号,然后strip动态库的时候,就可将符号脱掉
批量不到出符号
-unexported_symbol_list
3.3 two_leveInamespace & flat_namespace
二级命名空间与一级命名空间。链接器默认采用二级命名空间,也就是除了会记录符号名称,还会记录符号属于哪个Macho的,比如会记录下_NSLog来自Foundation
在同一个工作空间的两个Project中(LGOneFramework,LGApp)
在LGOneFramework中的一个.m中定义一个全局符号
在LGApp项目中可以声明和调用
也可在LGApp中重新定义重名的全局符号(符号在编辑时,采用的是二级命名空间,不仅记录符号,还记录符号属于哪个Macho)
⚠️ 在同一个项目里,全局符号都可见,只可以定义一次
3.4 Weak Symbol
- Weak Reference Symbol:表示此未定义符号是弱引用。如果动态链接器找不到该符号的定义,则将其设置为0。静态链接器会将此符号设置弱引用链接标志。 ```objectivec // 在.h文件中弱引用声明 void weakimportfunction(void) __attribute((weak_import));
//在.m文件中实现 void weak_import_function(void) { NSLog(@”weak_import_function”); }
在其他文件中使用,声明 `void weak_import_function(void);` 也可以不声明,配置连接器参数:
```objectivec
//告诉编译器 指定符号未定义,需要链接器动态查找,编译不检测是否定义
OTHER_LDFLAGS=$(inherited) -Xlinker -U -Xlinker _weak_import_function
链接器对未定义的符号 默认是报错,但开发者可以指定动态查找
使用场景: 将动态库声明为弱引用,当工程没有导入动态库的时候,不会报错
- Weak defintion Symbol:表示此符号为弱定义符号。如果静态链接器或动态链接器为此符号找到另一个(非弱)定义,则弱定义将被忽略。只能将合并部分中的符号标记为弱定义。
声明为弱定义的符号,不影响它作为导出全局符号
// weak def 弱定义
void weak_function(void) __attribute__((weak));
// weak 本地符号
void weak_hidden_function(void) __attribute__((weak, visibility("hidden")));
⚠️ 弱定义符号的优势:在全局其他文件中可以
再次定义一个同名符号不会编译冲突
报错。
3.5 Common Symbol
在定义时,未初始化的全局符号。
链接器设置:
-d :强制定义Common Symbol
-commons:指定对待Common Symbol如何响应
3.6 重新导出符号
给间接符号起别名
//给间接符号_NSLog起别名为CAT_NSLog
OTHER_LDFLAGS=$(inherited) -Xlinker -alias -Xlinker _NSLog -Xlinker CAT_NSLog
查看符号表:
//查看符号表,刷选'Cat'
nm -m ${MACH_PATH} | grep 'Cat'
应用场景:main中 导入一个动态库framework1时,动态库framework1中导入另一个framework2,main是不见framework2的,这时就可以在framework1中重新导出framework2的api,使main可以调用framework2的api
全局符号,本地符号
隐藏全局符号(将全局符号变成本地符号)
- 方式一: 给全局符号加上 static关键字修改
- 方式二:定义全局符号时,给编译器添加hidden属性
3.7 查看符号表
在2.4的基础上修改xcconfig
MACH_PATH=${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/${PRODUCT_NAME}
// 查看符号表
// objdump --macho --syms ${MACH_PATH}
CMD = objdump --macho --syms ${MACH_PATH}
TTY=/dev/ttys002
3.8 run Script执行的时机
4. strip命令
移除或修改符号表中的符号。
App 进行脱符号,可以将本地符号,全局符号全部脱去(All symbols),只留下间接符号表中的符号。
1)如何脱去调试符号
2) 如何脱去All Symbols静态库进行脱符号,放置在重定位符号表中的符号不能脱去,能脱去的只要调试符号(Debugging Symbols)
调试符号:由汇编器生成.o文件时,会生成一个DWARF格式的调试信息,它会被放到DWARF段,在链接是,会将DWARF段放到符号表中,链接后所有的符号都放在符号表中
- 动态库进行脱符号,只要不是全局符号都可以被干掉(Non-Global Symbols)
如何脱去Non-Global Symbols
链接一个静态库,静态库中的符号会合并到APP中,但不会存入间接符号表,然后进行APP脱符号时,可以体积缩小
链接一个动态库,动态库中的符号表会放到APP的间接符号表,然后进行APP脱符号时,间接符号表不能被脱去
4.1 Build Setting的Strip Style选择
- Debugging Symbols
- All Symbols
- Non-Global Symbols
4.3 Strip执行的时机
编译完成执行脚本后 ,签名之前,所以这是后剥离符号 已经不起作用了
4.3 Strip剥离符号
- -x : non_global
- 无参数: 代表全部符号
- -S : 剥离调试符号
app瘦身: 1) 在编译的时生产目标文件时优化 -O1,-Oz;2) dead code strip 在链接的时死代码剥离 ;3) strip 生产Macho文件后剥离符号 ;
4.4 dead strip
链接器的参数