初始mach-o文件及在项目中应用
本文字数:2250字
预计阅读时间:15分钟
01
认识mach-o的必要性
了解mach-o的结构可以帮助认识系统加载二进制文件的动态链接和静态链接。应用层面,使用initialize的c++函数计算启动时间耗时也需要以mach-o的结构知识为铺垫。还可以用在使用clang自注册启动任务上。后续会一一展开说明。
02
mach-o的定义
mach-o是mach object的缩写,是存储程序或库的标准格式。app的mach-o又称为可执行文件,静态库的.a文件也为mach-o文件,还有诸如此类的一些文件。
.o目标文件:MH_OBJECT
静态库文件.a : MH_OBJECT
可执行文件:MH_EXECUTE
动态库:MH_DYLIB
dyld:MH_DYLINKER
符号信息:MH_DSYM。可在loader.h的源码中看到全部的mach-o文件。
想要深入了解mach-o,可以自己创建一个,在xocde的build setting --> mach-o type 下选择类型,依据xcode的提示步骤可以创建出。如果已经有了文件,想知道是否为mach-o,也可以使用xcode打开文件,在build setting --> mach-o type下查看属于哪种类型。xcode的查看示意见下图:
03
mach-o的结构
查看mach-o的内部结构需要借助于工具mach-o View下载地址。目前下载下来之后需要在mac上运行使用。举例,使用mach-o View查看app的可执行文件,首先需要编译项目,这样会生成.app文件,然后项目中搜索.app:
右键show in finder,就会找到app包。如下图:
查找可执行文件,文件以项目名称命名的。下图中第一个就是:
通过打开mach-o View可以看到,mach-o分为三大部分,无论是什么类型的mach-o文件,都分为3大部分。
mach-o header 描述了Mach-o的cpu框架以及加载命令等信息;
load Commands 记录虚拟内存中的布局例如有哪些段,段从哪开始,段占用多大空间;
data 记录段的具体数据。如下图,三大部分在mach-o中分布:
mach-o header 的结构如下图中红框展示:
magic number :系统加载器通过该字段判断文件适用于32位还是64位;
cpu type:cpu类型,该字段确保系统可以将合适的二进制文件在当下架构下进行,为x86,arm64等;
file type :说明文件类型(可执行文件、库文件、核心转储文件、内核扩展文件、DYSM文件、动态库等)mach-o为MH-EXECUTE.;
number of load command 说明加载命令的条数;
size of load commands 表示加载命令的大小;
如上所述,header介绍了文件的基础信息。
2、第二部分:mach-o内容mach-o的内容部分分为load commands和 data。
load commands 如图所示:
每一个命令的含义下表:
命令名称 | 命令含义 |
---|---|
LC_SEGMENT_64 | 将文件中的段映射到进程地址空间 |
LC_DYLD_INFO_ONLY | dyld相关信息 |
LC_SYMTAB | 加载全局符号表信息 |
LC_DYSYMTAB | 动态链接符号表信息 |
LC_DYLD_INFO_ONLY | dyld相关信息 |
LC_LOAD_DYLINKER | 加载一个动态链接器,也就是加载dyld |
LC_UUID | app的uuid |
LC_VERSION_MIN_IPHONEOS | 支持最低系统版本 |
LC_MAIN | 设置程序主线程的入口地址 |
LC_LOAD_DYLIB(动态库名称) | 加载相应的动态库 |
LC_FUNCTION_STARTS | 函数启示地址表 |
LC_CODE_SIGNATURE | 代码签名 |
loader.h文件中可查看命令的官方注释。
data部分的内容如图所示:
__text各个段的含义:
名称 | 作用 |
---|---|
TEXT.text | 只有可执行的机器码 |
TEXT.cstring | 去重后的C字符串 |
TEXT.const | 初始化过的常量 |
TEXT.stubs | 符号桩。本质上是一小段会直接跳入lazybinding的表对应项指针指向的地址的代码。 |
TEXT.stub_helper | 辅助函数。上述提到的lazybinding的表中对应项的指针在没有找到真正的符号地址的时候,都指向这。 |
TEXT.unwind_info | 用于存储处理异常情况信息 |
TEXT.eh_frame | 调试辅助信息 |
_objc_classname | 类名称 |
objc_methlist | 方法列表 |
__text段在mach-o中的释义:
__data 各个段的含义:
名称 | 作用 |
---|---|
DATA.data | 初始化过的可变的数据 |
DATA.nl_symbol_ptr | 非lazy-binding的指针表,dyld 加载会立即绑定 |
DATA.la_symbol_ptr | lazy-binding的指针表,每个表项中的指针一开始指向stub_helper |
DATA.const | 没有初始化过的常量 |
DATA.mod_init_func | 初始化函数,在main之前调用 |
DATA.mod_term_func | 终止函数,在main返回之后调用 |
DATA.bss | 没有初始化的静态变量 |
DATA.common | 没有初始化过的符号声明 |
DATA.__objc_nlclslist | 实现了 load 方法的类 |
__data 在mach-o中的展示:
04
mach-o的应用
认识了mach-o,可以将其运用在统计启动时期c++ static initializer 阶段耗时。c++ static initializer 阶段系统做了什么,一个是c++的构造函数属性函数,一个是非基础类型的c++静态全局变量的创建(通常是类或结构体)。在构造函数上打断点,可以得到如图:
从dyld的源码中可以看到doModeInitFunction()的具体执行。如下图所示:
hook mod_init_func中的所有地址 。因为__mod_init_func section 位于__DATAsegment.__DATA segment 是数据段,是可以在运行时被修改的。并且,+load方法的执行是在dyld读取这些initializer之前。所以hook mod_init_func中的所有地址是可行的;
修改mod_init_func数据。利用getsectiondata获取到segment的每一个数据,将自己写的方法替换表中的方法;
调用原来的initializer。自己的Initializer中逐个获取每一个原函数地址,调用并计算耗时获取。
通过以上步骤,我们可以得出这一项的耗时,从而做出优化。
认识mach-o,是注册启动任务的必备知识。做注册启动任务的必要性有两点。
启动代码集中在AppDelegate中,代码逐渐臃肿,易读性降低,且代码之间耦合度高;
各个业务方加启动任务,都需要启动业务配合。
我们利用mach-o结构的__DATA可读写性。所以可以通过clang的section函数在编译阶段写入macho文件中一个__DATA段。__DATA段存储函数指针的指针。具体的使用步骤为:
编写注册方法的宏,提供给外部使用;
业务方注册任务,注册的每个时机都会在编译期间新增一个__DATA类型section,存储任务函数;
App运行,在注册的时机函数中使用getsectbynamefromheader_64遍历取出相应Section中的函数,并依次执行。
代码如下:
static void Launch_Func_(void);\
__attribute__((used, section("__DATA, "#period""))) static const void * __Func__= Launch_Func_;\
static void Launch_Func_(void)
#define RegisterLaunchTaskOnWillFinishLaunchPeriod\
RegisterLaunchTask(willFinishLaunch)
#define RegisterLaunchTaskOnDidFinishLaunchPeriod\
RegisterLaunchTask(didFinishLaunch)
#define RegisterLaunchTaskOnDidFinishADPeriod\
RegisterLaunchTask(didFinishAD)
#define RegisterLaunchTaskOnDidFinishHomepagePeriod\
RegisterLaunchTask(didFinishHome)
各个业务的使用代码如下:
RegisterLaunchTaskOnDidFinishHomepagePeriod{
/*do sth*/
}
参考资料:
1.https://everettjf.github.io/2017/02/06/a-method-of-hook-static-2.initializers/# https://github.com/fangshufeng/MachOView