9

深入剖析Macho (1)

 3 years ago
source link: http://satanwoo.github.io/2017/06/13/Macho-1/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

最近在公司里和一些同事搞了一些东西,略微底层。于是希望借这个机会好好把Macho相关的知识点梳理下。

虽然网上关于Macho的文章介绍一大堆,但是我希望能够从Macho的构成,加载过程以及需要了解的相关背景角度去进行分析,每一个点都力图深入。也会在这篇文章最后打造一个类似class-dump的小型工具。

程序启动加载的过程

当你点击一个icon启动应用程序的时候,系统在内部大致做了如下几件事:

  • 内核(OS Kernel)创建一个进程,分配虚拟的进程空间等等,加载动态链接器。
  • 通过动态链接器加载主二进制程序引用的库、绑定符号。

虽然简要概述很简单,但是有几个需要特别主要的地方:

  1. 二进制程序的格式是怎么样的?内核是如何加载它的?
  2. 内核是如何得知要使用哪种动态链接器的?
  3. 动态链接器和静态链接器的区别是啥?
  4. 程序在运行前究竟要做哪些步骤?顺序是怎么样的?

带着这些问题,我将一步步来剖析整个过程

二进制程序格式

在MacOS或者iOS上可执行的程序格式叫做Macho-O,它的主要成分如下图所示:

1421892661838860.gif
  • 一个mach_header标记一些元信息,比如架构、CPU、大小端等等
  • 多个Load Command告诉你究竟如何加载每个段的信息。
  • 多个SegementSection,包含了每个段自身的信息。包括一些数据、代码以及段的执行权限等等。
需要注意的是,不仅仅是可执行文件是Macho-O,目标文件(.o)以及动态库,静态库都是Mach-O格式。

所以,下面我们就用64位的定义从每个部分来介绍一下具体的数据结构:

mach_header_64

这个结构体代表的都是Mach-O文件的一些元信息,它的作用是让内核在读取该文件创建虚拟进程空间的时候,检查文件的合法性以及当前硬件的特性是否能支持程序的运行。

从源码中可以看出,整个结构题定义如下:

struct mach_header_64 {
    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 */
    uint32_t    reserved;    /* reserved */
};
  • magic 用于标识当前设备的是大端序还是小端序。如果是0xfeedfacf(MH_MAGIC_64)就是大端序,而0xcffaedfe(MH_CIGAM_64)是小端序,iOS系统上是小端序。
  • cputype 标识CPU的架构,比如ARM,X86,i386等等,进行了宏观划分。
  • cpusubtype 具体的CPU类型,区分不同版本的处理器。
  • filetype 划分之前我们提到的文件类型,比如是可执行文件还是目标文件。
  • ncmds 有几个LoadCommands,每个LoadCommands代表了一种Segment的加载方式。
  • sizeofcmds LoadCommand的大小,主要用于划分Mach-O文件的‘区域’。
  • flags 标记了一些dyld过程中的参数。
  • reversed 没用。

这里有个比较有意思的问题是,我为了验证大端序小端序的问题的时候,用了MacOS上的计算器进行
验证,本质上这应该是个小端序的应用程序,其二进制如下:

屏幕快照 2017-06-11 下午3.12.33.png

但是在otoolMachoView上看出来都是MH_MAGIC_64,如下所示:

屏幕快照 2017-06-11 下午3.13.47.png

我擦,这下看了懵逼,难道是我理解错了?于是赶紧翻了下class-dump代码,其解析header部分代码如下:

// 解析部分代码
_byteOrder = CDByteOrder_LittleEndian;

CDDataCursor *cursor = [[CDDataCursor alloc] initWithData:data];
_magic = [cursor readBigInt32];
if (_magic == MH_MAGIC || _magic == MH_MAGIC_64) {
    _byteOrder = CDByteOrder_BigEndian;
} else if (_magic == MH_CIGAM || _magic == MH_CIGAM_64) {
    _byteOrder = CDByteOrder_LittleEndian;
} else {
    return nil;
}

// readBigInt32的代码
- (uint32_t)readBigInt32;
{
    uint32_t result;

    if (_offset + sizeof(result) <= [_data length]) {
        result = OSReadBigInt32([_data bytes], _offset);
        _offset += sizeof(result);
    } else {
        [NSException raise:NSRangeException format:@"Trying to read past end in %s", __cmd];
        result = 0;
    }

    return result;
}

我们在用LLDB看下_data里面的内容指向的内存地址:

(lldb) po _data
<OS_dispatch_data: data[0x100600b40] = { leaf, size = 199520, buf = 0x100281000 }>

Xcode Memory看下:

屏幕快照 2017-06-11 下午3.25.06.png

看起来是没错的。然后由于MacOSX本身是小端序的,CFFAEDFE这样的数据会被自动解析成FE ED FA CF。所以这样是有问题的。因此,class-dump采用了OSReadBigInt32的方式去解析:

OS_INLINE
UInt32
OSReadSwapInt32(
    volatile void               * base,
    volatile UInt                 offset
)
{
    union lconv {
    UInt32 ul;
    UInt8  uc[4];
    } *inp, outv;

    // 步骤1
    inp = (union lconv *)((UInt8 *)base + offset);

    // 步骤2
    outv.uc[0] = inp->uc[3];
    outv.uc[1] = inp->uc[2];
    outv.uc[2] = inp->uc[1];
    outv.uc[3] = inp->uc[0];

    // 步骤3
    return (outv.ul);
}

这个方法会利用union的特性,进行数据交换。我们还是用刚刚的例子来验证:

  • 步骤1按照默认方式读出数据:FE ED FA CF
  • 步骤2进行交换,地址从低到高,分别是FE ED FA CF
  • 步骤3利用union的特性,当成一个32的数输出,按照默认小端序解析,会成为CF FA ED FE。也即是MH_CIGAM_64,是小端序。

其实按照MachoView的解析方式,将MH_CIGAM_64MH_MAGIC_64理解成MACHO文件和当前平台的编码顺序是否一致更好,如果解析出来是MH_CIGAM_64则表示不一致;否则一致。

Segment(段)

讲完了Mach-O文件的header部分,我们需要进行Load Commands部分。但是在这之前,我想先大致介绍下Mach-O中的Segment及其下属的Section(节),让大家能更好的理解Load Commands。

从整体上来说,Mach-O里面包含的段有以下这些:

  • __TEXT 代码段/只读数据段
  • __PAGEZERO Catch访问NULL指针的非法操作的段
  • __DATA 数据段
  • __LINKEDIT 包含需要被动态链接器使用的信息,包括符号表、字符串表、重定位项表等。
  • __OBJC 包含会被Objective Runtime使用到的一些数据。

关于__OBJC这个段,我是一脸懵逼的,从Macho文档上看,他包含了一些编译器私有的节。没有任何公开的资料描述,具体让我研究研究再说。

Section(节)

刚刚我们提到的__TEXT__DATA段都分别有下属的节。

之所以按照段->节的方式组织,是因为同一个段下的节,在内存的权限相同,可以不完全按照页大小进行对齐,节省内存空间。而对外整体暴露段,在装载程序的时候完整映射成一个vma,可以更好的做内存对齐。
名称 作用 TEXT.text 只有可执行的机器码 TEXT.cstring 去重后的C字符串 TEXT.const 初始化过的常量 TEXT.stubs 符号桩。本质上是一小段会直接跳入lazybinding的表对应项指针指向的地址的代码。 TEXT.stub_helper 辅助函数。上述提到的lazybinding的表中对应项的指针在没有找到真正的符号地址的时候,都指向这。 TEXT.unwind_info 用于存储处理异常情况信息 TEXT.eh_frame 调试辅助信息 DATA.data 初始化过的可变的数据 DATA.nl_symbol_ptr 非lazy-binding的指针表,每个表项中的指针都指向一个在装载过程中,被动态链机器搜索完成的符号 DATA.la_symbol_ptr lazy-binding的指针表,每个表项中的指针一开始指向stub_helper DATA.const 没有初始化过的常量 DATA.mod_init_func 初始化函数,在main之前调用 DATA.mod_term_func 终止函数,在main返回之后调用 DATA.bss 没有初始化的静态变量 DATA.common 没有初始化过的符号声明

其中,比较难以理解的可能是__la_symbol_ptr,让我们还是来以计算器的例子来理解:

  • 我们先从MachoView上找一个stub,比如[xxxx -> _CFRelease]。
  • 其数据是FF256A7C0000,结合这个节是在__TEXT段中,我猜测是应该一段汇编代码的16进制表示。
屏幕快照 2017-06-12 上午10.48.21.png
  • 从Hopper中打开,查看对应偏移量的stub含义:
屏幕快照 2017-06-12 上午10.40.50.png

我们可以看到这段代码的16进制表达就是:

屏幕快照 2017-06-13 下午3.47.50.png

从上图不难看出,stub的含义就是跳转到以__la_symbol_ptr对应表项数据所指向地址的代码。

  • 跳入以后,我们可以看到如下代码:
屏幕快照 2017-06-12 上午10.41.02.png

可以看到,在还没加载程序的时候,对应表项的数据还是dq _CFRelease。双击点进去看一下:

屏幕快照 2017-06-13 下午3.51.34.png

这里显示的应该是有点问题,如果全0的话是不可能使用lazy binding的。

我们还是用MachOView来看一下:

屏幕快照 2017-06-13 下午4.04.04.png

跳转到这个地址看看,没错了,处于stub_helper节里了:

屏幕快照 2017-06-13 下午4.04.28.png
屏幕快照 2017-06-13 下午4.04.33.png

__la_symbol_ptr 里面所有表项的数据都会被bind成dyld_stub_helper


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK