现在做iOS开发的挺多,了解一下在苹果平台上程序运行的原理

解析 MACH_O 文件

这篇文章描述了如何解析 Mach-O 文件并稍微解释了一下它的格式。这不是一份权威指南,不过当你不知从何开始时,它可能有些帮助。想了解更多信息,请考虑阅读官方文档和操作系统提供的头文件。

Macho-O 是什么

维基百科 的简单描述:

Mach-O 是 Mach object 文件格式的缩写,它是一种用于记录可执行文件、对象代码、共享库、动态加载代码和内存转储的文件格式。作为 a.out 格式的替代品,Mach-O 提供了更好的扩展性,并提升了符号表中信息的访问速度。

大多数基于 Mach 内核的操作系统都使用 Mach-O。NeXTSTEP、OS X 和 iOS 是使用这种格式作为本地可执行文件、库和对象代码的例子。

Mach-O 格式

Mach-O 没有类似于 XML、YAML、JSON 等诸如此类的特殊格式,它只是一个二进制字节流,被划分为了有意义的数据块。这些块包含元信息,比如,字节顺序、cpu 类型、块的大小,等等。

典型的 Mach-O 文件(对应的 官方文档 )包含三个区域:

  1. 头-包含该二进制文件的一般信息:字节顺序、(魔数)、cpu 类型、加载指令的数量等等。
  2. 加载指令-它是一张包含很多内容的表,内容包括区域的位置、符号表、动态符号表等。每个加载指令都包含一个元信息,比如指令类型、名称、在二进制文件中的位置等等。
  3. 数据-通常是对象文件中最大的部分。主要包含代码、数据,例如符号表,动态符号表等等。

这里是一个简化的图形表示︰

OS X 有两种类型的目标文件:Mach-O 文件和通用二进制文件,也叫作胖文件。它们之间的区别是:Mach-O 文件包含一种架构(i386、x86_64、arm64 等等)的对象代码,而胖文件可能包含若干包含不同架构(i386、x86_64、arm、arm64 等等)对象代码的对象文件。

胖文件的结构相当简单︰ 胖文件头以及后面的 Mach-O 文件:

解析 Mach-O 文件

OS X 没有提供 libmacho 或任何类似的工具,我们唯一拥有的是一组定义在 /usr/include/mach-o/* 中的 C 结构体,因此我们需要自己实现解析。它可能非常棘手,但也并不是非常困难。

内存描述

在我们开始解析前,让我们看看一个 Mach-O 文件的详细描述。简单起见,下面的对象文件是单个 i386 Mach-O 文件(而不是胖文件),它只包含两个段类型的数据条目。

仅需下面的结构体我们就可以描述该文件:

 
 
 
 
 

Objective-C

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct mach_header {
  uint32_t      magic;
  cpu_type_t    cputype;
  cpu_subtype_t cpusubtype;
  uint32_t      filetype;
  uint32_t      ncmds;
  uint32_t      sizeofcmds;
  uint32_t      flags;
};
 
struct segment_command {
  uint32_t  cmd;
  uint32_t  cmdsize;
  char      segname[16];
  uint32_t  vmaddr;
  uint32_t  vmsize;
  uint32_t  fileoff;
  uint32_t  filesize;
  vm_prot_t maxprot;
  vm_prot_t initprot;
  uint32_t  nsects;
  uint32_t  flags;
};
 

下面是内存映射的情况:

如果你想要从文件中读取特定的信息,你只需要一个正确的数据结构和偏移量。

解析

让我们来编写一个程序,它能读取 Mach-O 或 胖文件 并打印每个段的名称以及它编译的目标架构。

结束时,我们可能会有类似这样的东西︰

 
 
 
 
 

Objective-C

 
1
2
3
4
5
6
$ ./segname_dumper some_binary
i386
segname __PAGEZERO
segname __TEXT
segname __LINKEDIT
 

驱动

让我们从一个简单的“驱动”开始。

至少有两种可用的方式来解析此类文件︰ 加载文件内容到内存中并直接处理缓冲区 或打开一个文件在其中来回跳转。两种方法都有自己的优点和缺点,但这里我会选用第二种。此外,我假定没有人会用错误的方式使用该程序,因此我没有添加错误处理。

 
 
 
 
 

Objective-C

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <stdlib.h>
#include <mach-o/loader.h>
#include <mach-o/swap.h>
 
void dump_segments(FILE *obj_file);
 
int main(int argc, char *argv[]) {
  const char *filename = argv[1];
  FILE *obj_file = fopen(filename, "rb");
  dump_segments(obj_file);
  fclose(obj_file);
 
  return 0;
}
 
void dump_segments(FILE *obj_file) {
  // Driver
}
 

魔数、CPU、字节序

为了至少阅读对象文件的头,我们需要得到我们需要的所有信息︰ CPU 架构(32 位或 64 位) 和字节顺序。但首先我们需要取出一个魔数︰

 
 
 
 
 

Objective-C

 
1
2
3
4
5
6
7
8
9
10
uint32_t read_magic(FILE *obj_file, int offset) {
  uint32_t magic;
  fseek(obj_file, offset, SEEK_SET);
  fread(&magic, sizeof(uint32_t), 1, obj_file);
  return magic;
}
 
void dump_segments(FILE *obj_file) {
  uint32_t magic = read_magic(obj_file, 0);
}

函数 read_magic 是非常直截了当的,但有一件事可能看起来很怪︰ fseek。问题是,每当有人读取文件,文件内部的偏移量都会发生改变。最好显式指定偏移量,以确保我们阅读到我们实际上想要读取的内容。此外,这个小技巧稍后也会有用。

描述 32 位和 64 位对象文件的结构体是不同的(例如︰ mach_header 和 mach_header_64),我们需要检查文件的体系结构来选择需要的结构体:

 
 
 
 
 

Objective-C

 
1
2
3
4
5
6
7
8
9
int is_magic_64(uint32_t magic) {
  return magic == MH_MAGIC_64 || magic == MH_CIGAM_64;
}
 
void dump_segments(FILE *obj_file) {
  uint32_t magic = read_magic(obj_file, 0);
  int is_64 = is_magic_64(magic);
}
 

MH_MAGIC_64 和 MH_CIGAM_64 是系统提供的魔数。第二个看起来比第一个更多 magicly(译者注:原文如此,magic 对应的形容词是 magical,作者使用了magicly。)。解释如下。

由于历史的原因,不同的计算机可能使用不同的 字节顺序︰ 大端字节序 (从左到右) 和小端字节序 (从右至左)。魔数同样存储了这一信息︰ MH_CIGAM 和 MH_CIGAM_64 表示字节顺序不同于主机操作系统,因此所有字节都应颠倒︰

 
 
 
 
 

Objective-C

 
1
2
3
4
5
6
7
8
9
10
int should_swap_bytes(uint32_t magic) {
  return magic == MH_CIGAM || magic == MH_CIGAM_64;
}
 
void dump_segments(FILE *obj_file) {
  uint32_t magic = read_magic(obj_file, 0);
  int is_64 = is_magic_64(magic);
  int is_swap = should_swap_bytes(magic);
}
 

Mach-O 头

终于我们能够读取 mach_header 了。我们先来介绍几个用于从文件中读取数据的常用函数。

 
 
 
 
 

Objective-C

 
1
2
3
4
5
6
7
void *load_bytes(FILE *obj_file, int offset, int size) {
  void *buf = calloc(1, size);
  fseek(obj_file, offset, SEEK_SET);
  fread(buf, size, 1, obj_file);
  return buf;
}
 

注意︰ 数据应当在使用后释放 !

 
 
 
 
 
 

Objective-C

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void dump_mach_header(FILE *obj_file, int offset, int is_64, int is_swap) {
  if (is_64) {
    int header_size = sizeof(struct mach_header_64);
    struct mach_header_64 *header = load_bytes(obj_file, offset, header_size);
    if (is_swap) {
      swap_mach_header_64(header, 0);
    }
 
    free(header);
  } else {
    int header_size = sizeof(struct mach_header);
    struct mach_header *header = load_bytes(obj_file, offset, header_size);
    if (is_swap) {
      swap_mach_header(header, 0);
    }
 
    free(header);
  }
  free(buffer);
}
 
void dump_segments(FILE *obj_file) {
  uint32_t magic = read_magic(obj_file, 0);
  int is_64 = is_magic_64(magic);
  int is_swap = should_swap_bytes(magic);
  dump_mach_header(obj_file, 0, is_64, is_swap);
}
 

为了不搞砸驱动函数,我们在这里引入另一个函数 dump_mach_header。下一步是读取所有段指令并打印它们的名字。问题是,mach-o 文件通常也包含其他指令。如果你还记得 segment_command 结构的第一个字段的是 uint32_t cmd;,此字段表示指令的类型。下面是我们将使用的由系统提供的另一种结构体︰

 
 
 
 
 

Objective-C

 
1
2
3
4
5
struct load_command {
  uint32_t cmd;
  uint32_t cmdsize;
};
 

除了以上的所有信息 mach_header 还有许许多多加载命令,所以我们可以只是遍历并跳过我们不感兴趣的指令。此外,我们需要计算标头结束位置的偏移量。这里是 dump_mach_header 的最终版本︰

 
 
 
 
 
 

Objective-C

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void dump_mach_header(FILE *obj_file, int offset, int is_64, int is_swap) {
  uint32_t ncmds;
  int load_commands_offset = offset;
 
  if (is_64) {
    int header_size = sizeof(struct mach_header_64);
    struct mach_header_64 *header = load_bytes(obj_file, offset, header_size);
    if (is_swap) {
      swap_mach_header_64(header, 0);
    }
    ncmds = header->ncmds;
    load_commands_offset += header_size;
 
    free(header);
  } else {
    int header_size = sizeof(struct mach_header);
    struct mach_header *header = load_bytes(obj_file, offset, header_size);
    if (is_swap) {
      swap_mach_header(header, 0);
    }
 
    ncmds = header->ncmds;
    load_commands_offset += header_size;
 
    free(header);
  }
 
  dump_segment_commands(obj_file, load_commands_offset, is_swap, ncmds);
}
 

段指令

是时候去打印所有的段名了:

 
 
 
 
 
 

Objective-C

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void dump_segment_commands(FILE *obj_file, int offset, int is_swap, uint32_t ncmds) {
  int actual_offset = offset;
  for (int  i = 0; i < ncmds; i++) {
    struct load_command *cmd = load_bytes(obj_file, actual_offset, sizeof(struct load_command));
    if (is_swap) {
      swap_load_command(cmd, 0);
    }
 
    if (cmd->cmd == LC_SEGMENT_64) {
      struct segment_command_64 *segment = load_bytes(obj_file, actual_offset, sizeof(struct segment_command_64));
      if (is_swap) {
        swap_segment_command_64(segment, 0);
      }
 
      printf("segname: %sn", segment->segname);
 
      free(segment);
    } else if (cmd->cmd == LC_SEGMENT) {
      struct segment_command *segment = load_bytes(obj_file, actual_offset, sizeof(struct segment_command));
      if (is_swap) {
        swap_segment_command(segment, 0);
      }
 
      printf("segname: %sn", segment->segname);
 
      free(segment);
    }
 
    actual_offset += cmd->cmdsize;
 
    free(cmd);
  }
}
 

这个函数不需要 is_64 参数,因为我们可以从 cmd 类型本身 (LC_SEGMENT/LC_SEGMENT_64)推断它。如果它不是段,我们只需跳过该命令并向前移动到下一个。

CPU 名

我想要展示的最后一件事是如何基于 mach_header 的 cputype 获得处理器的名称。我相信这不是最好的选择,但对于本文的示例来说,它是可以接受的︰

 
 
 
 
 
 

Objective-C

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct _cpu_type_names {
  cpu_type_t cputype;
  const char *cpu_name;
};
 
static struct _cpu_type_names cpu_type_names[] = {
  { CPU_TYPE_I386, "i386" },
  { CPU_TYPE_X86_64, "x86_64" },
  { CPU_TYPE_ARM, "arm" },
  { CPU_TYPE_ARM64, "arm64" }
};
 
static const char *cpu_type_name(cpu_type_t cpu_type) {
  static int cpu_type_names_size = sizeof(cpu_type_names) / sizeof(struct _cpu_type_names);
  for (int i = 0; i < cpu_type_names_size; i++ ) {
    if (cpu_type == cpu_type_names[i].cputype) {
      return cpu_type_names[i].cpu_name;
    }
  }
 
  return "unknown";
}
 

OS X 为大量的 CPU 提供了 CPU_TYPE_ *,所以我们可以 “容易”地为特定的魔数关联一个字符串。为了打印 CPU 的名称,我们需要稍微修改 dump_mach_header

 
 
 
 
 
 

Objective-C

 
1
2
3
4
5
6
7
8
9
10
11
12
int header_size = sizeof(struct mach_header_64);
struct mach_header_64 *header = load_bytes(obj_file, offset, header_size);
if (is_swap) {
  swap_mach_header_64(header, 0);
}
ncmds = header->ncmds;
load_commands_offset += header_size;
 
printf("%sn", cpu_type_name(header->cputype)); // <-
 
free(header);
 

胖对象

这篇文章已经包含大量的内容,所以我不会描述如何处理胖对象,但你可以在这里找到如何实现它︰ segment_dumper

接下来是什么

大概就是以上这些。

这里是一组可能有用的链接,如果你想要更深入地挖掘和了解更多关于 mach-o 的内容:

转载:http://ios.jobbole.com/89498/

解析 MACH_O 文件的更多相关文章

  1. Android 解析XML文件和生成XML文件

    解析XML文件 public static void initXML(Context context) { //can't create in /data/media/0 because permis ...

  2. CSharpGL(9)解析OBJ文件并用CSharpGL渲染

    CSharpGL(9)解析OBJ文件并用CSharpGL渲染 2016-08-13 由于CSharpGL一直在更新,现在这个教程已经不适用最新的代码了.CSharpGL源码中包含10多个独立的Demo ...

  3. Jsoup系列学习(2)-解析html文件

    解析html文件 1.当我们通过发送http请求时,有时候返回结果是一个html格式字符串,你需要从一个网站获取和解析一个HTML文档,并查找其中的相关数据.你可以使用下面解决方法: 使用 Jsoup ...

  4. JAVA使用SAX解析XML文件

    在我的另一篇文章(http://www.cnblogs.com/anivia/p/5849712.html)中,通过一个例子介绍了使用DOM来解析XML文件,那么本篇文章通过相同的XML文件介绍如何使 ...

  5. JAVA中使用DOM解析XML文件

    XML是一种方便快捷高效的数据保存传输的格式,在JSON广泛使用之前,XML是服务器和客户端之间数据传输的主要方式.因此,需要使用各种方式,解析服务器传送过来的信息,以供使用者查看. JAVA作为一种 ...

  6. CSharpGL(5)解析3DS文件并用CSharpGL渲染

    CSharpGL(5)解析3DS文件并用CSharpGL渲染 我曾经写过一个简单的*.3ds文件的解析器,但是只能解析最基本的顶点.索引信息,且此解析器是仿照别人的C++代码改写的,设计的也不好,不方 ...

  7. php解析.csv文件

    public function actionImport() { //post请求过来的 $fileName = $_FILES['file']['name']; $fileTmpName = $_F ...

  8. java中采用dom4j解析xml文件

    一.前言 在最近的开发中用到了dom4j来解析xml文件,以前听说过来解析xml文件的几种标准方式:但是从来的没有应用过来,所以可以在google中搜索dmo4j解析xml文件的方式,学习一下dom4 ...

  9. 使用XStream解析MXL文件用到的jar包---xpp3_min-1.1.3.4.O.jar和xstream-1.3.1.jar

    使用XStream解析MXL文件用到的jar包---xpp3_min-1.1.3.4.O.jar和xstream-1.3.1.jar

随机推荐

  1. 【mark】linux 终端命令行下的快捷键(自己已验证所有)

    说明: \c + a:表示ctrl+a \a + a:表示alt+a 命令列表: 1 移动: \c + a:将光标移到行首 \c + e:将光标移到行尾 \c + f:将光标向后(右)移动一个字符 \ ...

  2. 随机内容生成(random模块)

    摘抄于: 低调的python小子 当梦想照进现实  幸福近在咫尺 [jpg]http://ip.ipwind.cn/msn.png[/jpg] Python中的random模块用于生成随机数.下面介绍 ...

  3. 基于jQuery的H5调试条

    <!DOCTYPE html> <html> <head> <meta name="viewport" content="wid ...

  4. Maven常用命令(转)

    Maven库: http://repo2.maven.org/maven2/ Maven依赖查询: http://mvnrepository.com/ Maven常用命令: 1. 创建Maven的普通 ...

  5. Java Abstract class and Interface

    Abstract Class 在定义class的时候必须有abstract 关键字 抽象方法必须有abstract关键字. 可以有已经实现的方法. 可以定义static final 的常量. 可以实现 ...

  6. Movie

    情书   av1300192最完美的离婚  av1304160梦旅人 av1126580NANA真人版电影  av1608482为了N  av1815256恋爱写真  av710199笃姬  搜狐天皇 ...

  7. 关于css3的自定义字体

    css3的@font-face属性打破了中文页面字体一成不变的格局,但今天本菜在用的时候并不那么爽.开始各种引用外部ttf文件失败.下了300M+的字体文件,苦逼的试了一下午.终于有一个ttf引用成功 ...

  8. 这个算asp.net的一个bug吗?

    asp.net设置按钮Enabled="false"后OnClientClick中添加的验证脚本消失了 下面的确可以 <asp:Button ID="btnRegi ...

  9. Convention插件 struts零配置

    http://blog.csdn.net/spyjava/article/details/13631961系列课程使用 注解:http://www.yiibai.com/struts_2/struts ...

  10. easyui-datebox 和easyui-datetimebox 设置默认时间当前时间

    //显示当前日期 formatterDate = function (date) { var day = date.getDate() > 9 ? date.getDate() : " ...