从Mach-O获取Bitcode

当我们设置了 Enable Bitcode=YES ,进行Archive时,bitcode会被嵌入到链接后的Mach-O中,用于提交到App Store。从编译日志中可以看出,Archive时多了一个编译参数 -fembed-bitcode

非Archive编译时,Enable Bitcode 将会增加编译参数 -fembed-bitcode-marker, 该参数用于在Mach-O中做标记,但是不会真正产生Bitcode。因为本地编译调试时并不需要bitcode,去掉这个不必要的步骤加快编译速度。

对于静态库等打开了Bitcode编译,通过MachOview查看会发现有一个__LLVM, __bitcode 段;而全工程编译出来对应的是 __LLVM, __bundle 段;可以使用 segedit 命令将指定的Section导出:

    segedit XXX.o -extract __LLVM __bitcode result.bc
viewBundle.png

如上图,需要注意的是, __LLVM, __bundle导出的并不是直接的bitcode格式,通过MachOView查看可得知是Xar文档格式,xar格式包含了一个xml格式的文件头(TOC),里面用于存放各种文件的基本属性以及一些附加附加信息,可以通过xar命令查看并解压

xar -d toc.xml -f bundle # 导出文件头

查看xml的结构,包括以下内容:

  • ld 的基本参数,我们链接时使用的是clang,实际上clang内部调用了ld,这里记录的是ld的参数
    • version: bitcode bundle 的版本号
    • architecture: 目标架构
    • platform: 目标平台
    • sdkversion: sdk版本
    • dylibs: 链接的动态库
    • link-options: 其他链接参数
  • 文件目录
    • checksum类型
    • 创建时间
    • 每个文件的信息
      • 文件名,这里并非原始文件名,而是按照链接时输入的顺序被重命名为数字序号
      • 基本属性,包括checksum、偏移、大小等
      • 文件类型,一般是Bitcode,还有两种特殊类型,Object以及Bundle,这里卖个关子,大家有兴趣可已自行研究(想想如果一个源代码文件是.s格式,要如何支持bitcode)
      • 编译器类型(clang/swift)及编译参数,这部分就是object文件中 __LLVM,__cmdline 的内容
    • 下一个文件的信息(如有)
    • 重复

如要提取bitcode,需要进一步解压:

xar -xf result.bc

通过otool检查二进制文件是否开启了bitcode:

otool -arch armv7 -l xxxx.a | grep __LLVM | wc -l 

通过判断是否包含 __LLVM 来判断是否支持bitcode,但是这种方式区分不了bitcode和bitcode-marker,确定是否包含bitcode,还需要检查otool输出中__LLVM Segment 的长度,如果长度只有1个字节,则并不能代表真正开启了bitcode:

$ otool -l test_bitcode.o | grep -A 2  __LLVM | grep size      
    size 0x0000000000000b10      
    size 0x0000000000000042 
$ otool -l test_bitcode_marker.o | grep -A 2  __LLVM | grep size      
    size 0x0000000000000001      
    size 0x0000000000000001

另外,Archive版本引入了 Symbol HidingDebug info Striping 机制,在链接时,bitcode中所有非导出符号均被隐藏,取而代之的是 __hidden#0_ 或者 __ir_hidden#1_ 这样的形式,debug信息也只保留了line-table,所有跟文件路径、标识符、导出符号等相关的信息全部都从bitcode中移除,相当于做了一层混淆,这种情况从IR代码中打印出的方法名全为__hidden#XXX,目前在尝试从中拿到内存地址再到符号表中去找映射关系。

符号表其实是DWARF的集合形式,它是内存地址与函数名,文件名,行号的映射表。DWARF 全名是 Debugging with Attribute Record Formats ,是一种调试信息的存放格式。

DWARF 第一版发布于 1992 年,主要是为 UNIX 下的调试器提供必要的调试信息,例如内存地址对应的文件名以及代码行号等信息,通常用于源码级别调试使用。另外通过 DWARF,还能还原运行时的地址成为可读的源码符号(及行号)。

DWARF 调试信息简单的来说就是在机器码和对应的源代码之间建立一座桥梁,大大提高了调试程序的能力。

iOS 中引入 DWARF 这种调试信息格式,其实也是顺应历史的潮流,因为 DWARF 已经在类 UNIX 系统中逐步替换 stabs(symbol table strings),成为一种主流的调试信息格式。使用 GCC 或者 LLVM 系列编译器都可以很方便的生成 DWARF 调试信息。

知道了Bitcode在Mach-O的位置以及获取方法,再来看看Mach-O的其他内容,Mach-O 其实是 Mach Object 文件格式的缩写。

属于 Mach-O 格式的常见文件

  • 目标文件 .o
  • 库文件
    • .a
    • .dylib
    • Framework
  • 可执行文件
  • dyld ( 动态链接器 )
  • .dsym ( 符号表 )

使用 file 命令可以查看文件类型

一张网上到处都有的结构图:

mach-o.png

1. Header

描述了 Mach-O 的 CPU 架构、文件类型以及加载命令等信息。

可进入 Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/mach-o/loader.h 查看头文件

struct mach_header_64 {
    uint32_t    magic;      /* 魔数 0xfeedface、0xcafebabe、0xfeedfacf 分别对应64、fat、64*/
    //如0xfeedface表示32位二进制格式,0xfeedfacf表示64位;
    cpu_type_t  cputype;    /* cpu 类型 比如 ARM */
    cpu_subtype_t   cpusubtype; /* cpu 具体类型 比如arm64 , armv7 */
    uint32_t    filetype;   /* 文件类型 例如可执行文件 .. 具体见下面枚举 */
    uint32_t    ncmds;      /* load commands 加载命令条数 */
    uint32_t    sizeofcmds; /* load commands 加载命令大小*/
    uint32_t    flags;      /* 标志位标识二进制文件支持的功能 , 主要是和系统加载、链接有关  具体见下面枚举*/
    uint32_t    reserved;   /* reserved , 保留字段 */
};
filetype:
#define    MH_OBJECT    0x1        /* Target 文件:编译器对源码编译后得到的中间结果 */
#define    MH_EXECUTE    0x2        /* 可执行二进制文件 */
#define    MH_FVMLIB    0x3        /* VM 共享库文件 */
#define    MH_CORE        0x4        /* Core 文件,一般在 App Crash 产生 */
#define    MH_PRELOAD    0x5        /* preloaded executable file */
#define    MH_DYLIB    0x6        /* 动态库 */
#define    MH_DYLINKER    0x7        /* 动态连接器 /usr/lib/dyld */
#define    MH_BUNDLE    0x8        /* 非独立的二进制文件,往往通过 gcc-bundle 生成 */
#define    MH_DYLIB_STUB    0x9        /* 静态链接文件 */
#define    MH_DSYM        0xa        /* 符号文件以及调试信息,在解析堆栈符号中常用 */
#define    MH_KEXT_BUNDLE    0xb        /* x86_64 内核扩展 */
flags:
#define    MH_NOUNDEFS    0x1        /* Target 文件中没有带未定义的符号,常为静态二进制文件 */
#define MH_INCRLINK 0x2     /* the object file is the output of an
                       incremental link against a base file
                       and can't be link edited again */
#define MH_DYLDLINK 0x4     /* the object file is input for the
                       dynamic linker and can't be staticly
                       link edited again */
#define MH_BINDATLOAD   0x8     /* the object file's undefined
                       references are bound by the dynamic
                       linker when loaded. */
#define MH_PREBOUND 0x10        /* the file has its dynamic undefined
                       references prebound. */
#define MH_SPLIT_SEGS    0x20  /* Target 文件中的只读 Segment 和可读写 Segment 分开  */
#define MH_TWOLEVEL    0x80        /* 该 Image 使用二级命名空间(two name space binding)绑定方案 */
#define MH_FORCE_FLAT    0x100 /* 使用扁平命名空间(flat name space binding)绑定(与 MH_TWOLEVEL 互斥) */
#define MH_WEAK_DEFINES    0x8000 /* 二进制文件使用了弱符号 */
#define MH_BINDS_TO_WEAK 0x10000 /* 二进制文件链接了弱符号 */
#define MH_ALLOW_STACK_EXECUTION 0x20000/* 允许 Stack 可执行 */
#define    MH_PIE 0x200000  /* 对可执行的文件类型启用地址空间 layout 随机化 */
#define MH_NO_HEAP_EXECUTION 0x1000000 /* 将 Heap 标记为不可执行,可防止 heap spray 攻击 */

2. Load Commands

描述了文件中数据的具体组织结构,不同的数据类型使用不同的加载命令表示。

Load Commands 详细保存着加载指令的内容 , 告诉链接器如何去加载这个 Mach-O 文件.

通过查看内存地址我们发现 , 在内存中 , Load Commands 是紧跟在 Mach_header 之后的 .

名称 含义
LC_SEGMENT_64 将文件中(32位或64位)的段映射到进程地址空间中
LC_DYLD_INFO_ONLY 动态链接相关信息
LC_SYMTAB 符号地址
LC_DYSYMTAB 动态符号表地址
LC_LOAD_DYLINKER 加载方式,dyld
LC_UUID 文件的UUID
LC_VERSION_MIN_MACOSX 支持的最低操作系统版本
LC_SOURCE_VERSION 源代码版本
LC_MAIN 设置程序主线程的入口地址和栈大小
LC_LOAD_DYLIB 依赖库的路径,包含三方库
LC_FUNCTION_STARTS 函数起始地址表
LC_CODE_SIGNATURE 代码签名

3. Data

每一个段(Segment)的数据都保存在其中,段的概念和 ELF 文件中段的概念类似,都拥有一个或多个 Section ,用来存放数据和代码。

Segment:
#define    SEG_PAGEZERO    "__PAGEZERO" /* 当时 MH_EXECUTE 文件时,捕获到空指针 */
#define    SEG_TEXT    "__TEXT" /* 代码/只读数据段 */
#define    SEG_DATA    "__DATA" /* 数据段 */
#define SECT_DATA   "__data"    /* the real initialized data section */
                    /* no padding, no bss overlap */
#define SECT_BSS    "__bss"     /* the real uninitialized data section*/
                    /* no padding */
#define SECT_COMMON "__common"  /* the section common symbols are */
                    /* allocated in by the link editor */
#define    SEG_OBJC    "__OBJC" /* Objective-C runtime 段 */
#define SECT_OBJC_SYMBOLS "__symbol_table"  /* symbol table */
#define SECT_OBJC_MODULES "__module_info"   /* module information */
#define SECT_OBJC_STRINGS "__selector_strs" /* string table */
#define SECT_OBJC_REFS "__selector_refs"    /* string table */

#define SEG_ICON     "__ICON"   /* the icon segment */
#define SECT_ICON_HEADER "__header" /* the icon headers */
#define SECT_ICON_TIFF   "__tiff"   /* the icons in tiff format */
#define    SEG_LINKEDIT    "__LINKEDIT" /* 包含需要被动态链接器使用的符号和其他表,包括符号表、字符串表等 */
Segment数据结构:
struct segment_command_64 { 
    uint32_t    cmd;        /* LC_SEGMENT_64 */
    uint32_t    cmdsize;    /* section_64 结构体所需要的空间 */
    char        segname[16];    /* segment 名字,上述宏中的定义 */
    uint64_t    vmaddr;        /* 所描述段的虚拟内存地址 */
    uint64_t    vmsize;        /* 为当前段分配的虚拟内存大小 */
    uint64_t    fileoff;    /* 当前段在文件中的偏移量 */
    uint64_t    filesize;    /* 当前段在文件中占用的字节 */
    vm_prot_t    maxprot;    /* 段所在页所需要的最高内存保护,用八进制表示 */
    vm_prot_t    initprot;    /* 段所在页原始内存保护 */
    uint32_t    nsects;        /* 段中 Section 数量 */
    uint32_t    flags;        /* 标识符 */
};
Section数据结构:
struct section_64 { 
    char        sectname[16];    /* Section 名字 */
    char        segname[16];    /* Section 所在的 Segment 名称 */
    uint64_t    addr;        /* Section 所在的内存地址 */
    uint64_t    size;        /* Section 的大小 */
    uint32_t    offset;        /* Section 所在的文件偏移 */
    uint32_t    align;        /* Section 的内存对齐边界 (2 的次幂) */
    uint32_t    reloff;        /* 重定位信息的文件偏移 */
    uint32_t    nreloc;        /* 重定位条目的数目 */
    uint32_t    flags;        /* 标志属性 */
    uint32_t    reserved1;    /* 保留字段1 (for offset or index) */
    uint32_t    reserved2;    /* 保留字段2 (for count or sizeof) */
    uint32_t    reserved3;    /* 保留字段3 */
};

部分的 Segment (主要指的 __TEXT__DATA)可以进一步分解为 Section。之所以按照 Segment -> Section 的结构组织方式,是因为在同一个 Segment 下的 Section,可以控制相同的权限,也可以不完全按照 Page 的大小进行内存对其,节省内存的空间。而 Segment 对外整体暴露,在程序载入阶段映射成一个完整的虚拟内存,更好的做到内存对齐。

下面列举一些常见的 Section。

Section 用途
__TEXT.__text 主程序代码
__TEXT.__cstring C 语言字符串
__TEXT.__const const 关键字修饰的常量
__TEXT.__stubs 用于 Stub 的占位代码,很多地方称之为桩代码
__TEXT.__stubs_helper 当 Stub 无法找到真正的符号地址后的最终指向
__TEXT.__objc_methname Objective-C 方法名称
__TEXT.__objc_methtype Objective-C 方法类型
__TEXT.__objc_classname Objective-C 类名称
__DATA.__data 初始化过的可变数据
__DATA.__la_symbol_ptr lazy binding 的指针表,表中的指针一开始都指向 __stub_helper
__DATA.nl_symbol_ptr 非 lazy binding 的指针表,每个表项中的指针都指向一个在装载过程中,被动态链机器搜索完成的符号
__DATA.__const 没有初始化过的常量
__DATA.__cfstring 程序中使用的 Core Foundation 字符串(CFStringRefs
__DATA.__bss BSS,存放为初始化的全局变量,即常说的静态内存分配
__DATA.__common 没有初始化过的符号声明
__DATA.__objc_classlist Objective-C 类列表
__DATA.__objc_protolist Objective-C 原型
__DATA.__objc_imginfo Objective-C 镜像信息
__DATA.__objc_selfrefs Objective-C self 引用
__DATA.__objc_protorefs Objective-C 原型引用
__DATA.__objc_superrefs Objective-C 超类引用




ASLR

Bitcode的工作流程及安全性评估

你可能感兴趣的:(从Mach-O获取Bitcode)