Mach-O可执行文件

Mach-O 概述 和 部分命令介绍

我们知道Windows下的文件都是PE文件,同样在OS X和iOS中可执行文件是Mach-o格式的。
Mach-O通常有三部分组成
*头部 (Header): Mach-O文件的架构 比如Mac的 PPC, PPC64, IA-32, x86-64,ios的arm系列.
*加载命令(Load commands): .
*原始段数据(Raw segment data):可以拥有多个段(segment),每个段可以拥有零个或多个区域(section)。每一个段(segment)都拥有一段虚拟地址映射到进程的地址空间。
官方给的图如下:

mach_o_segments

Xcode本身包含了command-line tools,命令行工具本身包含了分析和编译Mach-O,相关如下:

  • lipo /usr/bin/lipo 能够分析二进制文件的架构,可以拆分和合并二进制文件。比如查看一个静态库的架构,可以使用
lipo -info lib1.a
  • otool /usr/bin/otool 列出Mach-O文件的sections和segments信息,具体使用可以参考otool --help
  • pagestuff /usr/bin/pagestuff 展示每一个组成反射(image)的每个逻辑页面的内容,其中包含了sections的名字和每个page里的符号。这个工具不能在有多个架构的包含映射的二进制文件中运行。
  • symbol table的展示工具,/usr/bin/nm,允许你查看对象文件符号表的内容

Mach-O 分析

通常一个iOS App应用会安装在/var/mobile/Applications,系统的原生App会安装在/Applications目录下,大部分情况下,xxx.app/xxx文件并不是Mach-O格式文件,由于现在需要支持不同CPU架构的iOS设备,所以我们编译打包出来的执行文件是一个Universal Binary格式文件(通用二进制文件,也称胖二进制文件),实际上Universal Binary只不过将支持不同架构的Mach-O打包在一起,再在文件起始位置加上Fat Header来说明所包含的Mach-O文件支持的架构和偏移地址信息。
例如:

file CTRIP_WIRELESS
CTRIP_WIRELESS: Mach-O universal binary with 2 architectures
CTRIP_WIRELESS (for architecture i386): Mach-O executable i386
CTRIP_WIRELESS (for architecture x86_64):   Mach-O 64-bit executable x86_64

上面显示程序支持i386和x86_64架构,胖二进制文件定义在 /usr/include/mach-o/fat.h,我们查看一下源码

#include <stdint.h>
#include <mach/machine.h>
#include <architecture/byte_order.h>

#define FAT_MAGIC   0xcafebabe
#define FAT_CIGAM   0xbebafeca  /* NXSwapLong(FAT_MAGIC) */

struct fat_header {
    uint32_t    magic;      /* FAT_MAGIC */
    uint32_t    nfat_arch;  /* number of structs that follow */
};

struct fat_arch {
    cpu_type_t  cputype;    /* cpu specifier (int) */
    cpu_subtype_t   cpusubtype; /* machine specifier (int) */
    uint32_t    offset;     /* file offset to this object file */
    uint32_t    size;       /* size of this object file */
    uint32_t    align;      /* alignment as a power of 2 */
};

结构体struct fat_header:

  • magic字段就是我们常说的魔数(例如通常判断png文件格式,还有快速求平方根的0x5f3759df),加载器通过这个魔数值来判断这是什么样的文件,胖二进制文件的魔数值是0xcafebabe;
  • nfat_arch字段是指当前的胖二进制文件包含了多少个不同架构的Mach-O文件;
    fat_header后会跟着fat_arch,有多少个不同架构的Mach-O文件,就有多少个fat_arch,用于说明对应Mach-O文件大小、支持的CPU架构、偏移地址等;

(1).头部Header

我们可以使用 otool(1) 来观察可执行文件的头部 -- 规定了这个文件是什么,以及文件是如何被加载的。通过 -h 可以打印出头信息:
例如使用otool命令可以查看Mach-O文件的头信息,头信息就是Mach-O文件的第一部分,我们在第一部分介绍Mach-o概述已经介绍


mach_o_header

头信息的结构可以在 /usr/include/mach-o/loader.h

/*
 * 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 */
};

我们依次介绍这个头信息

1.magic,可以看到文件中的内容最开始部分,是以 cafe babe开头的
对于一个 二进制文件 来讲,每个类型都可以在文件最初几个字节来标识出来,即“魔数”。不同类型的 二进制文件,都有自己独特的"魔数"。
OS X上,可执行文件的标识有这样几个魔数(不同的魔数代表不同的可执行文件类型)
是mach-o文件的魔数,0xfeedface代表的是32位,0xfeedfacf代表64位,cafebabe是跨处理器架构的通用格式,#!代表的是脚本文件。
2.cputype和cupsubtype代表的是cpu的类型和其子类型,图上的例子是模拟器程序,cpu结构是x86_64,如果直接查看ipa,可以看到cpu是arm,subtype是armv7,arm64等
3.接着是filetype,2,代表可执行的文件 #define MH_EXECUTE 0×2
4.ncmds 指的是加载命令(load commands)的数量,例子中一共65个,编号0-64
5.sizeofcmds 表示23个load commands的总字节大小, load commands区域是紧接着header区域的。
6.最后个flags,例子中是0×00200085,可以按文档分析之。
也可以借助UE程序MachOView,MachOView是Mac上查看Mach-O结构的工具,如下图

mach_o_h_view

(2).加载命令(Load commands)

load commmand直接跟在 header 部分的后面,结构定义如下

struct load_command {
    uint32_t cmd;       /* type of load command */
    uint32_t cmdsize;   /* total size of command in bytes */
};

这些加载命令在Mach-O文件加载解析时,被内核加载器或者动态链接器调用,指导如何设置加载对应的二进制数据段,加载命令的种类有很多种,在<mach-o/loader.h>头文件有简单的注释。
具体可以使用命令

otool -v -l CTRIP_WIRELESS | open -f

查看。

(3)段数据(Segments)

Segments包含了很多segment,每一个segment定义了一些Mach-O文件的数据、地址和内存保护属性,这些数据在动态链接器加载程序时被映射到了虚拟内存中。每个段都有不同的功能,一般包括:

  • 1). __PAGEZERO: 空指针陷阱段,映射到虚拟内存空间的第一页,用于捕捉对NULL指针的引用;
  • 2). __TEXT: 包含了执行代码以及其他只读数据。 为了让内核将它 直接从可执行文件映射到共享内存, 静态连接器设置该段的虚拟内存权限为不允许写。当这个段被映射到内存后,可以被所有进程共享。(这主要用在frameworks, bundles和共享库等程序中,也可以为同一个可执行文件的多个进程拷贝使用)
  • 3). __DATA: 包含了程序数据,该段可写;
  • 4). __OBJC: Objective-C运行时支持库;
  • 5). __LINKEDIT: 含有为动态链接库使用的原始数据,比如符号,字符串,重定位表条目等等。

一般的段又会按不同的功能划分为几个区(section),即段所有字母大小,加两个下横线作为前缀,而区则为小写,同样加两个下横线作为前缀

下面列出段中可能包含的section:

__TEXT段:
__text, __cstring, __picsymbol_stub, __symbol_stub, __const, __litera14, __litera18;

__DATA段:
__data, __la_symbol_ptr, __nl_symbol_ptr, __dyld, __const, __mod_init_func, __mod_term_func, __bss, __commom;

__IMPORT段
__jump_table, __pointers;

其中__TEXT段中的__text是实际上的代码部分;__DATA段的__data是实际的初始数据,更加详细的说明见这里

可以通过otool –s查看某segment的某个section。

otool -s __TEXT __text a.out 
a.out:
(__TEXT,__text) section
0000000100000e80 55 48 89 e5 48 83 ec 20 c7 45 fc 00 00 00 00 89 
0000000100000e90 7d f8 48 89 75 f0 e8 a7 00 00 00 48 8b 35 8e 02 
0000000100000ea0 00 00 48 8b 0d 6f 02 00 00 48 89 f7 48 89 ce 48 
0000000100000eb0 89 45 e0 e8 90 00 00 00 48 8b 35 61 02 00 00 48 
0000000100000ec0 89 c7 e8 81 00 00 00 48 89 45 e8 48 8b 45 e8 48 
0000000100000ed0 8b 35 52 02 00 00 48 89 c7 e8 6a 00 00 00 c7 45 
0000000100000ee0 fc 00 00 00 00 48 8b 7d e0 e8 4e 00 00 00 8b 45 
0000000100000ef0 fc 48 83 c4 20 5d c3 90 90 90 90 90 90 90 90 90 
0000000100000f00 55 48 89 e5 48 83 ec 10 48 89 7d f8 48 89 75 f0 
0000000100000f10 e8 1b 00 00 00 48 8d 35 1c 01 00 00 48 89 f7 48 
0000000100000f20 89 c6 b0 00 e8 0d 00 00 00 48 83 c4 10 5d c3 

由于 -s __TEXT __text 很常见,otool 对其设置了一个缩写 -t 。我们还可以通过添加 -v 来查看反汇编代码:

otool -v -t a.out
a.out:
(__TEXT,__text) section
_main:
0000000100000e80    pushq   %rbp
0000000100000e81    movq    %rsp, %rbp
0000000100000e84    subq    $0x20, %rsp
0000000100000e88    movl    $0x0, -0x4(%rbp)
0000000100000e8f    movl    %edi, -0x8(%rbp)
0000000100000e92    movq    %rsi, -0x10(%rbp)
0000000100000e96    callq   0x100000f42
0000000100000e9b    movq    0x28e(%rip), %rsi
0000000100000ea2    movq    0x26f(%rip), %rcx
0000000100000ea9    movq    %rsi, %rdi
0000000100000eac    movq    %rcx, %rsi
...

了解Mach-O的作用

(1)Xcode中配置LinkMap

LinkMap文件是Xcode产生可执行文件(Mach-O)的同时生成的链接信息,用来描述可执行文件的构造成分,包括代码段(__TEXT)和数据段(__DATA)的分布情况。XCode -> Project -> Build Settings -> 搜map -> 把Write Link Map File选项设为yes,并指定好linkMap的存储位置,如下图


linkmap

编译后,到编译目录里找到该txt文件,文件名和路径就是上述的Path to Link Map File
位于~/Library/Developer/Xcode/DerivedData/XXX-eumsvrzbvgfofvbfsoqokmjprvuh/Build/Intermediates/XXX.build/Debug-iphoneos/XXX.build/

LinkMap里展示了整个可执行文件的全貌,分为三段,分别是:

  • # Object files:为分割标志,列出所有.o目标文件的信息(包括静态链接库.a里的),
  • # Sections:为分割标志,描述各个段在最后编译成的可执行文件中的偏移位置及大小,包括了代码段(__TEXT,保存程序代码段编译后的机器码)和数据段(__DATA,保存变量值),字段的含义在Mach-o中已详细介绍。
  • # Symbols:为分割标志,列出具体的按每个文件列出每个对应字段的位置和占用空间,例如:
# Symbols:
# Address   Size        File  Name
0x100002070 0x000000B0  [  1] -[PackageManager init]
0x100002120 0x00000080  [  1] +[PackageManager share]
0x1000021A0 0x00000050  [  1] ___23+[PackageManager share]_block_invoke
0x1000021F0 0x00000080  [  1] +[PackageManager getPackageType]
0x100002270 0x000000E0  [  1] +[PackageManager isProductionEnv]
0x100002350 0x000000E0  [  1] +[PackageManager isSpecialPackageForTest]
0x100002430 0x000000E0  [  1] +[PackageManager isDevEnv]
0x100002510 0x00000020  [  1] +[PackageManager isAUTOMATIC_TEST_ENV]
0x100002530 0x00000020  [  1] +[PackageManager isPRO_PACKAGE]
0x100002550 0x00000020  [  1] +[PackageManager ApplicationVersion]
0x100002570 0x00000020  [  1] -[PackageManager packageType]
0x100002590 0x00000040  [  1] -[PackageManager setPackageType:]
0x1000025D0 0x00000033  [  1] -[PackageManager .cxx_destruct]
0x100002610 0x0000005B  [  2] _main
0x10000266B 0x00000255  [  3] -[CTMyCtripOROrderAction isEqual:]

同样首列是数据在文件的偏移地址,第二列是占用大小,第三列是所属文件序号,对应上述Object files列表,最后是名字。
例如第二行代表了文件序号为2(反查上面就是PackageManage.o)的share方法占用了8*16=128byte大小。

根据上面符号文件的分析,我们可以写一个脚本统计我们程序的每一个静态库和framwork以及每一个实现文件的大小,有便于我们分析程序文件大小,为代码优化,减少二进制包大小提供了优化方向。我写了一个脚本,代码如下:

#!usr/bin/python
## -*- coding: UTF-8 -*-
#
#使用简介:python linkmap.py XXX-LinkMap-normal-xxxarch.txt 或者 python linkmap.py XXX-LinkMap-normal-xxxarch.txt -g
#使用参数-g会统计每个模块.o的统计大小
#
__author__ = "zmjios"
__date__ = "2016-07-27"

import os
import re
import shutil
import sys

class SymbolModel:
    file = ""
    size = 0

def verify_linkmapfile(args):
    if len(sys.argv) < 2:
        print("请输入linkMap文件")
        return False
    
    path = args[1]

    if not os.path.isfile(path):
        print("请输入文件")
        return False

    file = open(path)
    content = file.read()
    file.close()

    #查找是否存在# Object files:
    if content.find("# Object files:") == -1:
        print("输入linkmap文件非法")
        return False
    #查找是否存在# Sections:
    if content.find("# Sections:") == -1:
        print("输入linkmap文件非法")
        return False
    #查找是否存在# Symbols:
    if content.find("# Symbols:") == -1:
        print("输入linkmap文件非法")
        return False

    return True 

def symbolMapFromContent():
    symbolMap = {}
    reachFiles = False
    reachSections = False
    reachSymblos = False
    file = open(sys.argv[1])
    for line in file.readlines():
        if line.startswith("#"):
            if line.startswith("# Object files:"):
                reachFiles = True
            if line.startswith("# Sections:"):
                reachSections = True
            if line.startswith("# Symbols:"):
                reachSymblos = True
        else:
            if reachFiles == True and reachSections == False and reachSymblos == False:
                #查找 files 列表,找到所有.o文件
                location = line.find("]")
                if location != -1:
                    key = line[:location+1]
                    if  symbolMap.get(key) is not None:
                        continue
                    symbol = SymbolModel()
                    symbol.file = line[location + 1:]
                    symbolMap[key] = symbol
            elif reachFiles == True and reachSections == True and reachSymblos == True:
                #'\t'分割成三部分,分别对应的是Address,Size和 File  Name
                symbolsArray = line.split('\t')
                if len(symbolsArray) == 3:
                    fileKeyAndName = symbolsArray[2]
                    #16进制转10进制
                    size = int(symbolsArray[1],16)
                    location = fileKeyAndName.find(']')
                    if location != -1:
                        key = fileKeyAndName[:location + 1]
                        symbol = symbolMap.get(key)
                        if symbol is not None:
                            symbol.size = symbol.size + size
    file.close()
                            
    return symbolMap
    
def sortSymbol(symbolList):
     return sorted(symbolList, key=lambda s: s.size,reverse = True)

def buildResultWithSymbols(symbols):
    results = ["文件大小\t文件名称\r\n"]
    totalSize = 0
    for symbol in symbols:
        results.append(calSymbol(symbol))
        totalSize += symbol.size
    results.append("总大小: %.2fM" % (totalSize/1024.0/1024.0))
    return results

def buildCombinationResultWithSymbols(symbols):
    #统计不同模块大小
    results = ["库大小\t库名称\r\n"]
    totalSize = 0
    combinationMap = {}
    
    for symbol in symbols:
        names = symbol.file.split('/')
        name = names[len(names) - 1].strip('\n')
        location = name.find("(")
        if name.endswith(")") and location != -1:
            component = name[:location]
            combinationSymbol = combinationMap.get(component)
            if combinationSymbol is None:
                combinationSymbol = SymbolModel()
                combinationMap[component] = combinationSymbol

            combinationSymbol.file = component
            combinationSymbol.size = combinationSymbol.size + symbol.size
        else:
            #symbol可能来自app本身的目标文件或者系统的动态库
            combinationMap[symbol.file] = symbol
    sortedSymbols = sortSymbol(combinationMap.values())

    for symbol in sortedSymbols:
        results.append(calSymbol(symbol))
        totalSize += symbol.size
    results.append("总大小: %.2fM" % (totalSize/1024.0/1024.0))

    return results

def calSymbol(symbol):
    size = ""
    if symbol.size / 1024.0 / 1024.0 > 1:
        size = "%.2fM" % (symbol.size / 1024.0 / 1024.0)
    else:
        size = "%.2fK" % (symbol.size / 1024.0)
    names = symbol.file.split('/')
    if len(names) > 0:
        size = "%s\t%s" % (size,names[len(names) - 1])
    return size

def analyzeLinkMap():
    if verify_linkmapfile(sys.argv) == True:
        print("**********正在开始解析*********")
        symbolDic = symbolMapFromContent()
        symbolList = sortSymbol(symbolDic.values())
        if len(sys.argv) >= 3 and sys.argv[2] == "-g":
            results = buildCombinationResultWithSymbols(symbolList)
        else:
            results = buildResultWithSymbols(symbolList)
        for result in results:
            print(result)
        print("***********解析结束***********")


if __name__ == "__main__":
    analyzeLinkMap()

(2).查找无用selector和无用class

WeMobileDev公众号之前介绍了iOS微信安装包瘦身也做了相关介绍。无论是Mach-O或者是linkMap文件,都能做相关操作。具体原理是过正则表达式([+|-][.+\s(.+)]),我们可以提取当前可执行文件里所有objc类方法和实例方法(SelectorsAll)。再使用otool命令otool -v -s __DATA __objc_selrefs逆向__DATA.__objc_selrefs段,提取可执行文件里引用到的方法名(UsedSelectorsAll),我们可以大致分析出SelectorsAll里哪些方法是没有被引用的(SelectorsAll-UsedSelectorsAll)。注意,系统API的Protocol可能被列入无用方法名单里,如UITableViewDelegate的方法,我们只需要对这些Protocol里的方法加入白名单过滤即可。
万能的github(https://github.com/nst/objc_cover)已经有人写了相关脚本,有需要可以参考。

(3)class-dump和越狱相关

class-dump正式利用Mach-O文件导出出Mac或者iOS app的头文件的命令行工具。

推荐阅读更多精彩内容

  • 熟悉Linux和windows开发的同学都知道,ELF是Linux下可执行文件的格式,PE32/PE32+是win...
    Klaus_J阅读 1,470评论 1 7
  • 可执行文件是怎么来的?(以C语言为例) C代码(.c) - 经过编译器预处理,编译成汇编代码(.asm) - 汇编...
    那只大象阅读 3,240评论 2 8
  • 最终诉求? 拍摄、保存、播放、上传。就这四个步骤,当然首先拍摄就有许许多多的优化小功能,切换摄像头、单击跳帧焦距、...
    Carden阅读 6,035评论 0 46
  • 原文地址 写在之前 之前工作中对Mach-O文件有一定的接触, 原本早就想写一篇文章分享一下,但是奈何只是不够深入...
    南栀倾寒阅读 1,597评论 2 19
  • 13. Hook原理介绍 13.1 Objective-C消息传递(Messaging) 对于C/C++这类静态语...
    Flonger阅读 520评论 0 1