什么是MACH-O?
Mach-O(Mach Object)是macOS、iOS、iPadOS存储程序和库的文件格式。对应系统通过应用二进制接口(application binary interface,缩写为 ABI)来运行该格式的文件。
Mach-O格式用来替代BSD系统的a.out格式。Mach-O文件格式保存了在编译过程和链接过程中产生的机器代码和数据,从而为静态链接和动态链接的代码提供了单一文件格式。
程序启动时,系统做了哪些事情?
- 调用
fork
函数,创建一个process
- 调用
execve
或其衍生函数,在该进程上加载,执行我们的Mach-O
文件
当我们调用时execve
(程序加载器),内核实际上在执行以下操作: - 将文件加载到内存
- 开始分析
Mach-O
中的mach_header
,以确认它是有效的Mach-O
文件
要理解MACH-O,那就得清楚它到底是什么格式
使用命令 objdump --macho --private-headers #路径
chenshuangchao@chenshuhaodeMBP ~ % objdump --macho --private-headers /Users/chenshuangchao/Library/Developer/Xcode/DerivedData/ota_demo-dtlxcojkghhplkeseiefdzirpgsj/Build/Products/Debug-iphoneos/OTASDKDemo.app/OTASDKDemo
可以将MACHO理解为:文件配置+二进制代码。MACHO中都是二进制。
Load command中:
cmd类型有:LC_SEGMENT_64
,LC_DYLD_INFO_ONLY
,LC_SYMTAB
,
LC_DYSYMTAB
,LC_LOAD_DYLINKER
,LC_UUID
,
LC_VERSION_MIN_IPHONEOS
,LC_SOURCE_VERSION
,LC_MAIN
,
LC_ENCRYPTION_INFO_64
,LC_LOAD_DYLIB
,LC_LOAD_DYLIB
,
LC_RPATH
,LC_FUNCTION_STARTS
,LC_DATA_IN_CODE
,
LC_CODE_SIGNATURE
LC_MAIN
告诉动态链接器,加载可执行文件的入口,这个入口并不一定是main函数,可以指定。也就是可以修改MACHO中LC_MAIN
的内容,MACHO本质上就是一个二进制文件,是可读可写的。
查看指定的内容 | grep 'LC_MAIN' -A 3
chenshuangchao@chenshuhaodeMBP ~ % objdump --macho --private- headers /Users/chenshuangchao/Library/Developer/Xcode/DerivedData/ ota_demo-dtlxcojkghhplkeseiefdzirpgsj/Build/Products/Debug-iphoneos/OTASDKDemo.app/OTASDKDemo | grep 'LC_MAIN' -A 3
MACHO简单读写版本
通过终端查看MACHO,终端打印出来的信息太过冗余,有些信息是重复的,不需要的。逻辑教育Cat大神写了一个程序machoinfo,用这个程序查看MACHO可以更加清晰一点。
如何使用?
方法一:
1.进入machoinfo可以执行文件的目录
2.使用项目中支持的指令./machoinfo #路径
可以通过读出信息的过程,看出MACHO就是一个二进制,里面是配置文件加二进制。能读出来那些数据,说明macho的排列是按一定规则的。
方式二:
直接在项目中Xcode打印,将ipa的可执行文件路径加入到machoinfo的Scheme -->Run -->Arguments --> Arguments Passed On Launch
。
启动时,将路径作为参数传递到项目中。
编译与链接
编译成可执行文件时,中间要经过一个目标文件,也就是我们经常说的.o文件。编译的过程就是要把代码对应的放到MACHO的配置里面,比如代码是全局符号还是本地符号,是外部符号还是内部符号。根据这些特性,在编译的过程中把这些代码进行分类。
链接是干什么呢?链接的本质就是把多个目标文件组合成一个文件。我们的代码可能会被编译成很多个.o文件,最后会合并成一个可执行文件。在这个过程中,会把多个符号表合并到一起。那么在合并过程中,我们能否再对符号进行处理?比如,写在.h和写在.m文件中的变量,他们的性质不一样,那么我们是否可以在合并的时候,再重新修改它们对外暴露的性质。
初识符号表
Symbol Table
:就是用来保存符号。
String Table
:就是用来保存符号的名称。
Indirect Symbol Table
:间接符号表。保存使用的外部符号。更准确一点就是使 用的外部动态库的符号。是Symbol Table的子集。
例如:我们在程序中使用的NSLog,是属于Foundation
动态库中的符号,这个符号就会放到间接符号表里面去。
如何实现Xcode在编译的时候,将命令显示到终端上?
在Xcode中执行脚本的地方,写一个命令。
/dev/ttys000就是终端路径
脚本语句中添加重定向到终端
如何实现在Xcode中执行我们想要的命令?
根据上一篇文章,我们知道可以在XCConfig中实现,而且在XCConfig中定义的变量,可以通过info.plist实现在程序中使用。同样,在XCConfig中定义的变量,也可以在Xcode内置的脚本文件中使用。
下面是一个Shell脚本,一共是定义3个函数,RunCommand
,EchoError
和RunCMDToTTY
,在脚本最后一行,调用RunCMDToTTY
函数,在RunCMDToTTY
函数中使用了RunCommand
和EchoError
函数。
在RunCMDToTTY
函数中使用了3个参数,$TTY
,$CMD
,${CMD_FLAG}
。
CMD = 运行到命令
CMD_FLAG = 运行到命令参数
TTY = 终端
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" ${CMD_FLAG}
else
EchoError "=========================================="
EchoError "ERROR:Failed to run CMD. THE CMD must not null"
fi
}
RunCMDToTTY
Xcode内置的脚本,可以执行shell,在Shell中可以使用XCConfig中的变量,那么我们只需要在XCConfig中将脚本需要的参数传递过去就可以了。
Shell中的CMD,CMD_FLAG,TTY,是我们定义的Shell变量。在Xcode中也定义了一些自带的shell变量,比如说
$SRCROOT
代表的是当前代码的路径。
$BUILD_DIR
代表的是当前编译的路径。
如何不知道路径究竟代表什么,可以在xcode内置脚本中用echo命令。
echo "$BUILD_DIR" > /dev/ttys000
在XCConfig中设置参数
CMD = nm
// -pa 可执行文件路径
CMD_FLAG = -pa /Users/chenshuangchao/Library/Developer/Xcode/DerivedData/ota_demo-dtlxcojkghhplkeseiefdzirpgsj/Build/Products/Debug-iphoneos/OTASDKDemo.app/OTASDKDemo
TTY = /dev/ttys000
到此时,脚本已经准备完毕,在终端上执行 nm -pa /Users/chenshuangchao/Library/Developer/Xcode/DerivedData/ota_demo-dtlxcojkghhplkeseiefdzirpgsj/Build/Products/Debug-iphoneos/OTASDKDemo.app/OTASDKDemo
执行后在终端输出了很多东西。说明我们的配置起效了。
操作符号的命令
指令的具体意义
nm -pa /Users/chenshuangchao/Library/Developer/Xcode/DerivedData/ota_demo-dtlxcojkghhplkeseiefdzirpgsj/Build/Products/Debug-iphoneos/OTASDKDemo.app/OTASDKDemo
-p: 不排序
-a: 显示所有符号,包含调试符号
这个路径太长了,而且文件是随便找的,如果想要使用当前项目生成的可执行文件要怎么做?再新建一个变量。MACH_PATH=${BUILD_DIR}/$(CONFIGURATION)/$(EFFECTIVE_PLATFORM_NAME)/*
Xcode内置的变量,会根据Project的位置自动改变。到这里,可以说是万事具备,可以开始研究了。
Xcode在哪一步执行脚本
Strip指令脱去无用符号
Strip脱去符号是在脚本执行之前还是之后?
平时我们项目中Strip命令很少起作用,因为xcode默认设置,只有打包的时候才起作用。如果要在开发的时候也能脱去符号,可以将Deployment Postprocessing
设置为Yes后,Strip Style
设置为Non-Global Symbols
或者Debugging Symbols
就起作用了。
是否可以更自由的控制符号剥离操作呢?链接器是将多个目标文件合在一起,那在做融合动作的时候,就可以对目标文件的符号做一些操作。
man ld查看链接器参数
command + K
清除终端
man ld
查看链接器支持的参数
/
代表查找
/-S
自动跳转到-S指令的位置。
-S
的含义 Do not put debug information (STABS or DWARF) in the output file.
脱去调试符号。
在XCConfig中配置 OTHER_LDFLAGS = -Xlinker -S
即可脱去调试符号。
符号相关知识点
- -O1 -Oz 生成目标文件,编译时候优化
- dead code strip 死代码剥离,链接时候优化
- strip 剥离符号,已经生成Mach-O文件后修改
后两步主要是处理符号
strip指令,什么都不加,就是剥离所有的符号。