【问题标题】:How to read Mach-O header from object file?如何从目标文件中读取 Mach-O 头文件?
【发布时间】:2015-02-24 12:16:54
【问题描述】:

这几天我一直在尝试汇编,现在了解了汇编和机器码之间的关系(在 OSX 上通过 NASM 使用 x86,阅读 Intel docs)。

现在我试图了解链接器如何工作的细节,特别想了解 Mach-O 目标文件的结构,从 Mach-O 标头开始。

我的问题是,您能否绘制出下面的 Mach-O 标头如何映射到 otool 命令输出(显示标头,但格式不同)?

这个问题的一些原因包括:

  • 它将帮助我了解有关“Mach-O 标头结构”的文档在实际目标文件中的外观。
  • 它将简化理解路径,因此我和其他新人不必花费数小时或数天来思考“他们的意思是 this 还是 this”类型的东西。没有以前的经验,很难将一般的 Mach-O 文档翻译成现实世界中的实际目标文件。

下面我展示了我尝试从真实目标文件中解码 Mach-O 标头的示例和过程。在下面的描述中,我试图显示所有出现的小/微妙问题的提示。希望这能让新手了解这对新手来说是多么的困惑。


示例

从一个名为 example.c 的基本 C 文件开始:

#include <stdio.h>

int
main() {
  printf("hello world");
  return 0;
}

gcc example.c -o example.out 编译它,得到:

cffa edfe 0700 0001 0300 0080 0200 0000
1000 0000 1005 0000 8500 2000 0000 0000
1900 0000 4800 0000 5f5f 5041 4745 5a45
524f 0000 0000 0000 0000 0000 0000 0000
0000 0000 0100 0000 0000 0000 0000 0000
0000 0000 0000 0000 0000 0000 0000 0000
0000 0000 0000 0000 1900 0000 2802 0000
5f5f 5445 5854 0000 0000 0000 0000 0000
0000 0000 0100 0000 0010 0000 0000 0000
0000 0000 0000 0000 0010 0000 0000 0000
0700 0000 0500 0000 0600 0000 0000 0000
5f5f 7465 7874 0000 0000 0000 0000 0000
5f5f 5445 5854 0000 0000 0000 0000 0000
400f 0000 0100 0000 2d00 0000 0000 0000
400f 0000 0400 0000 0000 0000 0000 0000
0004 0080 0000 0000 0000 0000 0000 0000
5f5f 7374 7562 7300 0000 0000 0000 0000
5f5f 5445 5854 0000 0000 0000 0000 0000
6e0f 0000 0100 0000 0600 0000 0000 0000
6e0f 0000 0100 0000 0000 0000 0000 0000
0804 0080 0000 0000 0600 0000 0000 0000
5f5f 7374 7562 5f68 656c 7065 7200 0000
... 531 total lines of this

运行otool -h example.out,它会打印:

example.out:
Mach header
      magic cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
 0xfeedfacf 16777223          3  0x80          2    16       1296 0x00200085

研究

为了了解 Mach-O 文件格式,我发现这些资源很有帮助:

来自 opensource.apple.com 的最后 3 个包含所有常量,例如:

#define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */
#define MH_CIGAM_64 0xcffaedfe /* NXSwapInt(MH_MAGIC_64) */
...
#define CPU_TYPE_MC680x0  ((cpu_type_t) 6)
#define CPU_TYPE_X86    ((cpu_type_t) 7)
#define CPU_TYPE_I386   CPU_TYPE_X86    /* compatibility */
#define CPU_TYPE_X86_64   (CPU_TYPE_X86 | CPU_ARCH_ABI64)

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

鉴于此信息,目标是在 example.out 目标文件中找到 Mach-O 标头的每一部分。


第一:找到“神奇”的数字

鉴于该示例和研究,我能够识别 Mach-O 标头的第一部分,即“幻数”。太酷了。

但这不是一个简单的过程。以下是为弄清楚这一点而必须收集的信息。

  • otool 输出的第一列显示“magic”为0xfeedfacf
  • Apple Mach-O docs 表示标题应为MH_MAGICMH_CIGAM(“魔术”相反)。所以通过谷歌在mach-o/loader.h找到了这些。由于我使用的是 64 位架构而不是 32 位架构,因此使用 MH_MAGIC_64 (0xfeedfacf) 和 MH_CIGAM_64 (0xcffaedfe)。
  • 查看example.out文件,前8个十六进制代码为cffa edfe,与MH_CIGAM_64匹配!它采用不同的格式,让你有点失望,但它们是 2 种不同的十六进制格式,它们足够接近以看到连接。它们也被颠倒了。

这里有 3 个数字,足以弄清楚神奇数字是什么:

0xcffaedfe // value from MH_CIGAM_64
0xfeedfacf // value from otool
cffa edfe  // value in example.out

这太令人兴奋了!仍然不能完全确定我是否对这些数字得出了正确的结论,但希望如此。


下一步:查找 cputype

现在它开始变得混乱。以下是需要拼凑起来的部分,几乎可以理解它,但这是我目前卡住的地方:

这里是计算CPU_TYPE_X86_64值的相关常量:

#define CPU_ARCH_ABI64  0x01000000      /* 64 bit ABI */
#define CPU_TYPE_X86        ((cpu_type_t) 7)
#define CPU_TYPE_I386       CPU_TYPE_X86        /* compatibility */
#define CPU_TYPE_X86_64     (CPU_TYPE_X86 | CPU_ARCH_ABI64)

所以基本上:

CPU_TYPE_X86_64 = 7 BITWISEOR 0x01000000 // 16777223

16777223 的数字与 otool 显示的一致,太好了!

接下来,尝试在example.out 中查找该数字,但它不存在,因为那是十进制数字。我刚刚在 JavaScript 中将其转换为十六进制,其中

> (16777223).toString(16)
'1000007'

所以不确定这是否是生成十六进制数的正确方法,尤其是与 Mach-O 目标文件中的十六进制数匹配的方法。 1000007 也只有 7 个数字,所以不知道你是否应该“填充”它或其他什么。

不管怎样,你会看到这个数字example.out,就在幻数之后:

0700 0001

嗯,它们似乎有点相关:

0700 0001
1000007

似乎在1000007 的末尾添加了一个0,并且它被颠倒了。


问题

此时我想问这个问题,已经花了几个小时才到达这一点。 Mach-O 标头的结构如何映射到实际的 Mach-O 目标文件?您能否说明标题的每个部分在上面的example.out 文件中是如何显示的,并简要说明原因?

【问题讨论】:

标签: c macos executable


【解决方案1】:

让您感到困惑的部分原因是endianness。在这种情况下,标头以平台的本机格式存储。与 Intel 兼容的平台是 little-endian 系统,这意味着多字节值的最低有效字节在字节序列中位于首位。

所以,字节序列07 00 00 01,当解释为一个小端序32位值时,对应于0x01000007

解释结构需要知道的另一件事是每个字段的大小。所有uint32_t 字段都非常简单。它们是 32 位无符号整数。

cpu_type_tcpu_subtype_t 都在您链接的 machine.h 中定义为等同于 integer_tinteger_t 被定义为等同于 /usr/include/mach/i386/vm_types.h 中的 int。 OS X 是一个 LP64 平台,这意味着 longs 和指针对架构敏感(32 位与 64 位),但 int 不是。它始终是 32 位的。

因此,所有字段的大小都是 32 位或 4 字节。因为有 8 个字段,所以总共 32 个字节。

从您原来的 hexdump 中,这里是对应于标头的部分:

cffa edfe 0700 0001 0300 0080 0200 0000
1000 0000 1005 0000 8500 2000 0000 0000

按字段划分:

struct mach_header_64 {
  uint32_t  magic;           cf fa ed fe -> 0xfeedfacf
  cpu_type_t  cputype;       07 00 00 01 -> 0x01000007
  cpu_subtype_t cpusubtype;  03 00 00 80 -> 0x80000003
  uint32_t  filetype;        02 00 00 00 -> 0x00000002
  uint32_t  ncmds;           10 00 00 00 -> 0x00000010
  uint32_t  sizeofcmds;      10 05 00 00 -> 0x00000510
  uint32_t  flags;           85 00 20 00 -> 0x00200085
  uint32_t  reserved;        00 00 00 00 -> 0x00000000
};

【讨论】:

    【解决方案2】:

    MAGICCIGAM 为您提供有关文件中使用的字节顺序的提示。当您将前四个字节读取为cffaedfe 时,这意味着您应该以小端方式解释任何 4 个字节。意味着你先用单位写数字,然后是第十个,等等。所以,当你阅读07000001时,它代表数字01000007,这正是你等待的数字(1000007),除了前导0。我建议你阅读一下字节顺序?

    【讨论】:

    • 太棒了,有帮助,有什么特别好的资源推荐来阅读字节顺序?
    • ...以及 Danny Cohen 的这篇著名文章(“ON HOLY WARS AND A PLEA FOR PACE”):ietf.org/rfc/ien/ien137.txt
    猜你喜欢
    • 2013-03-27
    • 1970-01-01
    • 1970-01-01
    • 2021-12-02
    • 1970-01-01
    • 2011-06-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多