目录
一、Mach-O与链接器
Mach-O
Mach-O(Mach Object)
是macOS、iOS、 iPadOS
存储程序和库的文件格式。对应系统通过应用二进制接口(application binary interface
,缩写为ABI
)来运行该格式的文件。Mach-O
格式用来替代BSD
系统的a.out
格式。Mach-O
文件格式保存了在编译过程和链接过程中产生的机器代码和数据,从而为静态链接和动态链接的代码提供了单一文件格式
。
Mach-O
文件就是一个可读可写的二进制文件
可执行文件调用过程:
- 调用
fork
函数,创建一个process
进程 - 调用
execve
或其衍生函数
,在该进程上加载,执行我们的Mach-O
文件
当我们调用时execve
(程序加载器),内核实际上在执行以下操作:
- 将文件加载到内存
- 开始分析
Mach-O
中的mach header
,以确认它是有效的Mach-O
文件
通过终端命令查看Mach-O
文件:objdump --macho --private-headers 可执行文件地址
通过自己的项目查看Mach-O
文件
machoinfo项目是用来查看Mach-O
文件的,编译后生成machoinfo
的可执行文件可作为命令使用:
拷贝machoinfo
可执行文件到桌面
使用machoinfo项目调试Mach-O
文件读取过程
通过以下操作将TestCode
项目编译的可执行文件路径拖入到machoinfo
项目启动参数中,这样TestCode
可执行文件路径就会传入到machoinfo
项目main
函数的argv
参数中
main
函数中添加断点后运行machoinfo
项目就能看到参数传入进来了,然后通过这个路径就能读取到TestCode
可执行文件并进行Mach-O
文件读取过程
二、符号的种类与作用
Symbol Table
- Symbol Table:就是用来保存符号。
- String Table:就是用来保存符号的名称。
- Indirect Symbol Table:间接符号表。保存使用的外部符号,也就是使用的外部动态库的符号(比如
NSLog
)。是Symbol Table
的子集。
通过编译项目生成可执行文件-->然后在终端使用命令查看Mach-O
的中的符号操作比较繁琐,现在通过配置Shell
脚本,当项目编译成功后直接在终端执行命令并展示命令的结果。
首先通过重定向在Xcode中让当前终端显示特定内容:
Xcode让终端显示xcconfig
文件中的变量:
现在通过xcode_run_cmd.sh
脚本执行相关的命令:
#!/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
# CMD:终端需要运行的命令
# CMD_FLAG:运行的命令的参数
# TTY:终端标志
if [[ -n "$CMD" ]]; then
RunCommand "$CMD" ${CMD_FLAG}
else
EchoError "=========================================="
EchoError "ERROR:Failed to run CMD. THE CMD must not null"
fi
}
RunCMDToTTY
该xcode_run_cmd.sh
脚本需需要三个参数CMD 、CMD_FLAG 、TTY
,这三个参数在xcconfig
文件中定义就能获取到
// -p:不排序
// -a: 显示除了调试符号的其他所有符号
MACHO_PATH = ${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/$(FULL_PRODUCT_NAME)/$(PRODUCT_NAME)
CMD = nm
CMD_FLAG = -pa ${MACHO_PATH}
TTY = /dev/ttys000
添加shell
脚本执行命令/bin/sh "$SRCROOT/xcode_run_cmd.sh"
并编译即可在终端看到nm
命令执行的结果
在
Xcode
编译日志中可以看到项目编译后-签名前执行的shell
脚本,因此执行shell
脚本时Mach-O
文件已经生成了
三、strip命令
strip
命令可以用来剥离Mach-O
文件中的符号,比如调试符号等。strip
命令修改的是Symbol Table
符号表、不能修改Indirect Symbol Table
间接符号表。
Xcode
默认会在Release
编译情况下剥离所有符号,但是Debug
编译情况下不会剥离符号。
设置Debug
编译情况下剥离调试符号:
- 从编译日志中也可以看到,
Xcode
的strip
命令是在shell
脚本之后执行的- 在实际开发项目中测试
Xcode
的strip
,没有剥离符号的Mach-O
大小为34M,剥离符号后大小为20.8M,可见剥离符号对于瘦包还是非常有用的
现在不使用Xcode
的strip
命令,改在shell
脚本中执行strip
命令
因为Xcode
的strip
使用的是clang
的命令,shell
脚本使用的是ld
链接器的命令,所以可以在终端查看ld
链接器的参数,终端输入命令man ld
回车后输入/-S
进行搜索:
xcconfig
文件添加ld
的参数
OTHER_LDFLAGS = -Xlinker -S
在Xcode
的Build Sttings
中可以看到添加成功了:
编译后可发现终端输出中少了很多调试符号。
strip 参数如下:
- -x: Non-Global
- 无参数: All Symbol
- -S: 调试符号
四、在LLVM项目中调试nm命令
LLVM
项目下载安装参考:iOS-底层探索29:自定义Clang插件
填入启动参数和machoinfo项目查看Mach-O
文件添加启动参数的方法相同:
运行LLVM
项目(也就是运行LLVM
项目中llvm-nm Scheme
的 llvm-nm Target
),控制台打印如下,可以看到和shell
中使用nm
命令输出到终端的信息相同:
此外在llvm-nm.cpp
源码的main
函数中添加断点后运行项目即可断点调试llvm-nm
命令的源码,从llvm-nm
的源码中我们就能看到nm
命令是如何读取Mach-O
文件的。
五、总结
通过对符号的
strip
不仅可以减少ipa
包体积还可以减少动态库、静态库的体积对
ipa
包瘦身主要有以下操作:
- 编译时期:
-O0、-Os
生成目标文件- 链接时期:
dead code strip
死代码剥离(也是剥离符号)- 生成
Mach-O
后:strip
剥离符号,对Mach-O
文件进行修改
快捷键
Command + K
清空终端显示内容