Как читать заголовок Mach-O из объектного файла?

Я провел последние несколько дней, экспериментируя с сборкой, и теперь понимаю взаимосвязь между сборочным и машинным кодом (используя x86 через NASM на OSX, читая документы Intel).

Теперь я пытаюсь понять детали работы компоновщика и особенно хочу понять структуру объектных файлов Mach-O, начиная с заголовков Mach-O.

Мой вопрос: можете ли вы определить, как заголовки Mach-O ниже отображаться на выходе команды otool (который отображает заголовки, но они находятся в другом формате)?

Некоторые причины этого вопроса включают:

  • Это поможет мне увидеть, как документы в "структуре заголовков Mach-O" выглядят в реальных объектных файлах.
  • Это упростит путь к пониманию, поэтому мне и другим новичкам не придется тратить много часов или дней на размышления о том, "что они означают это или это". Трудно без предыдущего опыта мысленно перевести общую документацию Mach-O в фактический объектный файл в реальном мире.

Ниже я покажу пример и процесс, которые я прошел, чтобы попытаться декодировать заголовок Mach-O из реального объектного файла. В приведенных ниже описаниях я стараюсь показать намеки на все маленькие/тонкие вопросы, которые возникают. Надеюсь, это обеспечит понимание того, как это может быть очень запутанным для новичков.


Пример

Начиная с базового файла C с именем example.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, я нашел эти ресурсы полезными:

Те последние 3 из opensource.apple.com содержат все константы, такие как:

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

Учитывая эту информацию, целью было найти каждую из этих частей заголовка Mach-O в объектном файле example.out.


Сначала: поиск "волшебного" номера

Учитывая этот пример и исследование, я смог идентифицировать первую часть заголовка Mach-O, "волшебное число". Это было круто.

Но это был не простой процесс. Вот фрагменты информации, которые нужно было собрать, чтобы понять это.

  • В первом столбце вывода otool отображается "magic" как 0xfeedfacf.
  • Apple Mach-O docs говорят, что заголовок должен быть либо MH_MAGIC, либо MH_CIGAM ( "magic" в обратном порядке), Таким образом, найти их через google в mach-o/loader.h. Так как я использую 64-битную архитектуру, а не 32-разрядную, то она идет с MH_MAGIC_64 (0xfeedfacf) и MH_CIGAM_64 (0xcffaedfe).
  • Посмотрел файл example.out, а первые 8 шестнадцатеричных кодов были cffa edfe, что соответствует MH_CIGAM_64! Это в другом формате, который немного отбрасывает вас, но они представляют собой два разных гексагональных формата, которые достаточно близки, чтобы увидеть соединение. Они также отменены.

Вот 3 числа, которых было достаточно, чтобы выяснить, что такое магическое число:

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

Так что интересно! Все еще не совсем уверен, что я прихожу к правильному выводу об этих цифрах, но надеюсь на это.


Далее: поиск cputype

Теперь он начинает запутываться. Вот фрагменты, которые нужно было собрать, чтобы почти понять это, но это то место, где я застрял до сих пор:

  • otool показывает 16777223. Этот вопрос о стеке apple дал несколько советов о том, как это понять.
  • Найден CPU_TYPE_X86_64 в mach/machine.h и должен был выполнить несколько вычислений, чтобы выяснить его значение.

Ниже приведены соответствующие константы для вычисления значения 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, nice!

Затем попытался найти это число в example.out, но он не существует, потому что это десятичное число. Я просто преобразовал это в hex в JavaScript, где

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

Не уверен, что это правильный способ генерации шестнадцатеричного числа, особенно тот, который будет соответствовать шестнадцатеричным числам в объектном файле Mach-O. 1000007 - это только 7 чисел, поэтому не знаю, хотите ли вы "набить" его или что-то еще.

В любом случае, вы видите это число example.out, сразу после магического числа:

0700 0001

Хм, они кажутся несколько связанными:

0700 0001
1000007

Похоже, что в конец 1000007 добавлен 0 и что он был отменен.


Вопрос

На этом этапе я хотел задать вопрос, уже потратив несколько часов, чтобы добраться до этого момента. Как структура заголовка Mach-O сопоставляется с фактическим файлом объекта Mach-O? Можете ли вы показать, как каждая часть заголовка отображается в файле example.out выше, с кратким объяснением, почему?

Ответ 1

В чем вас смущает endianness. В этом случае заголовок сохраняется в собственном формате для платформы. Платформы, совместимые с Intel, представляют собой малоинтенсивные системы, что означает, что младший байт многобайтового значения является первым в последовательности байтов.

Таким образом, последовательность байтов 07 00 00 01, интерпретируемая как 32-разрядное значение малознакомого, соответствует 0x01000007.

Другая вещь, которую вам нужно знать для интерпретации структуры, - это размер каждого поля. Все поля uint32_t довольно просты. Это 32-разрядные целые числа без знака.

Оба cpu_type_t и cpu_subtype_t определены в machine.h, которые вы связывали с эквивалентом integer_t. integer_t определяется как эквивалентный int в /usr/include/mach/i 386/vm_types.h. OS X - платформа LP64, что означает, что long и указатели чувствительны к архитектуре (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

MAGIC или CIGAM дает подсказки по порядку байтов, используемому в файле. Когда вы читаете первые четыре байта как cffaedfe, это означает, что вы должны интерпретировать любые 4 байта в маленьком конце. Означает, что вы пишете числа с единицами первой, затем десятой и т.д. Итак, когда вы читаете 07000001, он представляет номер 01000007, который именно вы ожидали (1000007), кроме ведущего 0. Могу ли я предложить вам прочитать порядок байтов?