标签:operator *** success entry dal ken 调用 pytho present
之前负责项目的包体积优化学习了 Mach-O 文件的格式,那么 Mach-O 究竟是怎么样的文件,知道它的组成之后我们又能做点什么?本文会从 Mach-O 文件的介绍讲起,再看看认识它后的一些实际应用。
先让我们看看 Mach-O 的大致构成
再使用 MachOView 一窥究竟
结合可知 Mach-O 文件包含了三部分内容:
Mach-O 文件的头部定义如下:
MH_NOUNDEFS
目标文件没有未定义的符号,MH_DYLDLINK
目标文件是动态链接输入文件,不能被再次静态链接,MH_SPLIT_SEGS
只读 segments 和 可读写 segments 分离,MH_NO_HEAP_EXECUTION
堆内存不可执行…filetype 的定义有:
flags 的定义有:
简单总结一下就是 Headers 能帮助校验 Mach-O 合法性和定位文件的运行环境。
Headers 之后就是 Load Commands,其占用的内存和加载命令的总数在 Headers 中已经指出。
Load Commands 的定义比较简单:
LC_SEGMENT、LC_SEGMENT_64
将 segment 映射到进程的内存空间,LC_UUID
二进制文件 id,与符号表 uuid 对应,可用作符号表匹配,LC_LOAD_DYLINKER
启动动态加载器,LC_SYMTAB
描述在 __LINKEDIT
段的哪找字符串表、符号表,LC_CODE_SIGNATURE
代码签名等这里先来看看 segment 的定义:
#define SEG_PAGEZERO "__PAGEZERO" // 可执行文件捕获空指针的段
#define SEG_TEXT "__TEXT" // 代码段,只读数据段
#define SEG_DATA "__DATA" // 数据段
#define SEG_LINKEDIT "__LINKEDIT" // 包含动态链接器所需的符号、字符串表等数据
接着看看 section 的定义:
__Text
和 __Data
都有自己的 section
Text.__text
主程序代码Text.__cstring
c 字符串Text.__stubs
桩代码Text.__stub_helper
Data.__data
初始化可变的数据Data.__objc_imageinfo
镜像信息 ,在运行时初始化时 objc_init
,调用 load_images
加载新的镜像到 infolist 中
* `Data.__la_symbol_ptr`
* `Data.__nl_symbol_ptr`
* `Data.__objc_classlist` 类列表
* `Data.__objc_classrefs` 引用的类
这节最后探究下 stubs,在 Xcode 中新建 C 项目,代码如下:
#include <stdio.h>
int main(int argc, const char * argv[]) {
printf("Hello, coder\n");
return 0;
}
使用 gcc -c main.c
将其编译成 a.out 文件,调用 nm 命令查看 .o 文件的符号
看到 _printf
是未定义的,也就是说并没有该函数的内存地址。nm 打印出的信息表明dyld_stub_binder
也是未定义的。 打开 Hopper 查看 .o 文件
可以看出 printf 会跳入 __stubs
中,地址也与 MachOView 看到的相对应
双击刚才 __stubs
中的地址,会跳转到 __la_symbol_ptr
在 MachOView 中查看 0x100001010 对应的数据为 0x10000f9c
用 Hopper 搜索 0x10000f9c,跳转到 stub_helper
,可知 __la_symbol_ptr
里的数据被 bind 成了 stub_helper
由此可知,__la_symbol_ptr
中的数据被第一次调用时会通过 dyld_stub_binder
进行相关绑定,而 __nl_symbol_ptr
中的数据就是在动态库绑定时进行加载。
所以 __la_symbol_ptr
中的数据在初始状态都被 bind 成 stub_helper
,接着 dyld_stub_binder
会加载相应的动态链接库,执行具体的函数实现,此时 __la_symbol_ptr
也获取到了函数的真实地址,完成了一次近似懒加载的过程。
写到这里,算是快速过了一遍 Mach-O 文件的基本概念,接着聊聊可以怎样减少项目的体积。
iOS 的包主要由可执行文件、资源文件(图片)等文件组成,所以可以从这两大头文件入手优化。
我们的项目中难免会存在一些没使用的类或方法,由于 OC 的动态特性,编译器会对所有的源文件进行编译,找出并删除没用到的类或方法可以减少可执行文件大小。 上文中提到了 __objc_classlist
和 __objc_classrefs
,它们分别表示项目中全部类列表和项目中被引用的类列表,那么取两者之差,就能删除一些项目中没使用的类文件。但是在删除过程中记住要在项目中全局搜索确认下,看看有没有通过字符串调用无引用的类的方法,原因还是 OC 是动态语言。 在看具体做法之前,顺带提一下我公司的项目组成。我们维护着俩客户端,共用着一个基础库(lib 库),可能有时由于产品的需求变更或者为了产品功能的预留导致 lib 库中只有着某个端使用的代码,我在上述的做法中对脚本做了稍微改进,以防删除了 lib 库的代码,导致另一个端跑不起来,下面介绍通用的做法:
otool -v -s __objc_classlist
和 otool -v -s __objc_classrefs
命令,逆向 __DATA. __objc_classlist
段和 __DATA. __objc_classrefs
段获取当前所有oc类和被引用的oc类。这点就跟本文的主题没什么关系,不感兴趣可以略过。 压缩 app 中的图片是我做的另一个努力,虽然 Xcode 会压一遍,但是经我压缩后打包发现包还是会少个将近 1m,这里用到的工具是 ImageOptim,贴出我的三脚猫 python:
all_file_size = 0
all_file_count = 0
def fileDriector(filePath):
global all_file_size, all_file_count
for file in os.listdir(filePath):
if os.path.isdir(filePath + ‘/‘ + file):
if file != ‘Pods‘ and not file.startswith(‘.‘) and not file.endswith(‘.framework‘) and not file.endswith(‘.bundle‘) and not file.endswith(‘.a‘) and file != ‘libs‘ or file.endswith(‘.xcassets‘) or file.endswith(‘.imageset‘):
the_path = filePath + ‘/‘ + file
fileDriector(the_path)
elif file.endswith(‘.png‘) or file.endswith(‘.jpg‘):
fileName = filePath + ‘/‘ + file
comand_line = "echo %s | imageoptim" % fileName
test = subprocess.Popen(comand_line, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
output = test.communicate()[0]
numberList = re.findall(‘\.?\d+\.?\d*kb‘, output)
lastSize = numberList[-1]
lastSizeList = re.findall(‘\.?\d+\.?\d*‘, lastSize)
saveSize = lastSizeList[0]
if saveSize.startswith(‘.‘):
saveSize = ‘0‘ + saveSize
finalSize = float(saveSize)
all_file_size += finalSize
all_file_count += 1
print output
其他的一些减包方案就不展开了,接下来我试着分析一下 bestswifter 大神的 BSBacktraceLogger
说到调用堆栈,我们很容易联想到 DSYM 文件,我们知道 Xcode build setting 有个 DEBUG INFOMATION FORMAT 的选项
可以看到 Debug 模式下,符号表文件会存入可执行文件中,而 Release 模式则会生成出 DSYM 文件,我们平常使用 Bugly 等工具上传的就是这份 DSYM 文件,DSYM 也是种 Mach-O 文件。在 Debug 模式,由于符号表在内存中,这为我们符号化堆栈提供了可能性。
bool bs_fillThreadStateIntoMachineContext(thread_t thread, _STRUCT_MCONTEXT *machineContext) {
mach_msg_type_number_t state_count = BS_THREAD_STATE_COUNT;
kern_return_t kr = thread_get_state(thread, BS_THREAD_STATE, (thread_state_t)&machineContext->__ss, &state_count);
return (kr == KERN_SUCCESS);
}
thread_get_state
函数获取线程执行状态(例如寄存器),传入 _STRUCT_MCONTEXT
结构体,_STRUCT_MCONTEXT
在不同的 cpu 架构会有所不同。
uintptr_t bs_mach_instructionAddress(mcontext_t const machineContext){
return machineContext->__ss.BS_INSTRUCTION_ADDRESS;
}
const uintptr_t instructionAddress = bs_mach_instructionAddress(&machineContext);
获取当前指令的地址,也就是当前的栈帧,即当前被调用的函数。下面先讲下关于栈帧的概念。
如上图,一个函数调用栈是由若干个栈帧组成,每个栈帧通过 FP 和 SP 划分界线,fun1 函数 SP 和 FP 的指向就是 main 函数的栈帧。所以说只要知道当前函数的栈帧就能获取上一个函数的栈帧,从而回溯出函数调用栈。
程序计数器(PC)作用是给出将要执行的下一条指令在内存中的地址,上面代码的 BS_INSTRUCTION_ADDRESS
。其中 16 位为 %ip,32 位为 %eip,64 位为 %rip,arm 是 pc。
SP 是栈指针寄存器,指向栈顶。
FP 是栈基址寄存器,指向栈起始位置。
LR 寄存器在子程序调用时会存储 PC 的值,即返回值。
为了方便获取栈帧,干脆构造一个栈帧的结构体,以下代码来自 KSCrash,它的注释已经很好的讲明了结构体的原由,BSBacktraceLogger 与之类似。
/** Represents an entry in a frame list.
* This is modeled after the various i386/x64 frame walkers in the xnu source,
* and seems to work fine in ARM as well. I haven‘t included the args pointer
* since it‘s not needed in this context.
*/
typedef struct FrameEntry
{
/** The previous frame in the list. */
struct FrameEntry* previous;
/** The instruction address. */
uintptr_t return_address;
} FrameEntry;
之后,递归获取函数栈帧
for(; i < 50; i++) {
backtraceBuffer[i] = frame.return_address;
if(backtraceBuffer[i] == 0 ||
frame.previous == 0 ||
bs_mach_copyMem(frame.previous, &frame, sizeof(frame)) != KERN_SUCCESS) {
break;
}
}
符号化地址的大致思路分三步:1. 获取地址所在的内存镜像;2. 定位到内存镜像的符号表;3. 再从符号表中找到目标地址的符号。
uint32_t bs_imageIndexContainingAddress(const uintptr_t address) {
const uint32_t imageCount = _dyld_image_count();
const struct mach_header* header = 0;
for(uint32_t iImg = 0; iImg < imageCount; iImg++) {
header = _dyld_get_image_header(iImg);
遍历 image,得到指向 image header 的指针
uintptr_t addressWSlide = address - (uintptr_t)_dyld_get_image_vmaddr_slide(iImg);
uintptr_t cmdPtr = bs_firstCmdAfterHeader(header);
对指针 +1 操作,返回指向 load command 的指针
for(uint32_t iCmd = 0; iCmd < header->ncmds; iCmd++) {
const struct load_command* loadCmd = (struct load_command*)cmdPtr;
if(loadCmd->cmd == LC_SEGMENT) {
const struct segment_command* segCmd = (struct segment_command*)cmdPtr;
if(addressWSlide >= segCmd->vmaddr &&
addressWSlide < segCmd->vmaddr + segCmd->vmsize) {
return iImg;
}
}
如果某个 segment 包含这个地址,那么该地址应大于 segment 的起始地址,小于 segment 的起始地址 + segment 的大小。
__LINKEDIT
段包含了符号表(symbol),字符串表(string),重定位表(relocation)。LC_SYMTAB
指明了 __LINKEDIT
段查找字符串和符号表的位置。我们可以结合 SEG_LINKEDIT
和 LC_SYMTAB
来找到 image 的符号表。 接下来看看段基址的获取: 虚拟地址偏移量 = 虚拟地址(vmaddr) - 文件偏移量(fileoff) 段基址 = 虚拟地址偏移量 + ASLR的偏移量
const uintptr_t imageVMAddrSlide = (uintptr_t)_dyld_get_image_vmaddr_slide(idx);
// ALSR
const uintptr_t addressWithSlide = address - imageVMAddrSlide;
const uintptr_t segmentBase = bs_segmentBaseOfImageIndex(idx) + imageVMAddrSlide;
有了段基址,获取符号表和字符串表就只是计算下 symoff 和 stroff 偏移量了:
const BS_NLIST* symbolTable = (BS_NLIST*)(segmentBase + symtabCmd->symoff);
const uintptr_t stringTable = segmentBase + symtabCmd->stroff;
递归查找离 addressWithSlide 更近的函数入口地址,因为 addressWithSlide 肯定大于某个函数的入口。
for(uint32_t iSym = 0; iSym < symtabCmd->nsyms; iSym++) {
// If n_value is 0, the symbol refers to an external object.
if(symbolTable[iSym].n_value != 0) {
uintptr_t symbolBase = symbolTable[iSym].n_value;
uintptr_t currentDistance = addressWithSlide - symbolBase;
if((addressWithSlide >= symbolBase) &&
(currentDistance <= bestDistance)) {
bestMatch = symbolTable + iSym;
bestDistance = currentDistance;
}
}
}
MachO 文件的 __Text
段有 __objc_classname
和 __objc_methname
来表示类名和方法名,但是这两者之间是如何做到关联的呢?下面我以系统的计算器做例子,试着进一步研究下 MachO 文件。 使用 MachOView 打开系统计算机,先来看看 __objc_classname
和 __objc_methname
在 load commands 里的定义:
我们顺着 __objc_classname
的偏移offset 109518 即 0x1ABCE 来到:
同理 __objc_methname
的偏移为 0x165E8:
那么,怎样像 class-dump 那样将类和自个的方法名对应起来呢? 由于每个类的虚拟地址都在Data 段 __objc_classlist
中:
我们看到起始地址对应的是 0x1000298A8 这个地址,为了得到实际的地址需要用虚拟地址 - 段起始地址 + 文件偏移,经过一番计算,结果是0x298A8,来到文件偏移处,已经在DATA 段的 __objc_data
在这里会对应着类的结构体,代码拷自 class-dump
struct cd_objc2_class {
uint64_t isa;
uint64_t superclass;
uint64_t cache;
uint64_t vtable;
uint64_t data; // points to class_ro_t
uint64_t reserved1;
uint64_t reserved2;
uint64_t reserved3;
};
data 是我们感兴趣的,它指向 class_ro_t
,熟悉 runtime 的话应该知道 class_ro_t
存储了类在编译器就确定的属性、方法、协议等。 所以上图 isa 的数据是 0x1000298D0,继续顺着找下去 0x100020A68 就是 data 的内存地址,再用上面的公式计算得到 0x20A68,我们在 __objc_const
找到那里:
这里就是对应着 class_ro_t
,来看看它在 class-dump 里的定义:
struct cd_objc2_class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
uint32_t reserved; // *** this field does not exist in the 32-bit version ***
uint64_t ivarLayout;
uint64_t name;
uint64_t baseMethods;
uint64_t baseProtocols;
uint64_t ivars;
uint64_t weakIvarLayout;
uint64_t baseProperties;
};
最终 0x20A80 就是name,0x20A88 就是 baseMethods。name 对应的正好是 0x1ABCE,类名是 BitFieldBox。baseMethods 指向内存 0x100020A00,该地址对应的数据是 18 00 00 00 04 00 00 00 表示 entsize 和 count 方法数,在这8个字节之后就是 name 方法名,types 方法类型, imp 函数指针了,所以方法名处的数据为 0x1000165e8 刚好对应 initWithFrame: 将结论用 class-dump 验证可得 BitFieldBox 的第一个方法是 initWithFrame
最初学习 MachO 文件格式觉得挺抽象的,后来经过各种源码的阅读和融合,终于在一次次地探索中比较直观地认识了 MachO 文件,特别是在 MachO 文件关联类的方法名时对类在内存中的布局有了更进一步的认识。虽然我们平常开发基本不和 MachO 文件打交道,但是对它有个基本概念,无论是做崩溃分析、逆向等都是有帮助的。
面试题持续整理更新中,如果你想一起进阶去大厂,不妨添加一下交流群1012951431
面试题资料或者相关学习资料都在群文件中 进群即可下载!
标签:operator *** success entry dal ken 调用 pytho present
原文地址:https://www.cnblogs.com/Julday/p/13168530.html