R0 CREW

Разбор формата файлов из Instruments

Оригинал: jamie-wong.com

Вы когда-нибудь задумывались над тем, как приложения хранят свои данные? Множество форматов файлов, таких как MP3 и JPG, стандартизированы и хорошо документированы, но как насчет пользовательских, проприетарных форматов файлов? Что вы делаете, когда хотите извлечь данные, которые, как вы предполагаете, где-то в файле, и нет API для его извлечения?

Последние несколько месяцев я создавал инструмент визуализации производительности, называемый speedscope. Инструмент может импортировать форматы профайлера из разных источников, таких как Chrome, Firefox и формат stackcollapse Брендана Грегга.

В Figma я работаю на C++, который отлично кросс-компилируется с asm.js и WebAssembly для запуска в браузере. Однако, иногда полезно иметь возможность профилировать встроенную сборку, которую мы используем для разработки и отладки. Выбранным инструментом для OS X является Instruments.

Если мы сможем извлечь нужную информацию из файлов «Instruments», мы сможем загрузить её в flamecharts, что поможет нам получить интуитивно понятную информацию о том, что происходит с нашим кодом, когда он выполняется.

До этого момента все форматы, которые я импортировал в speedscope, были либо открытым текстом, либо JSON, что облегчало их анализ. Формат файлов .trace, напротив, является сложным форматом, с несколькими кодировками, который, похоже, использует несколько бинарных форматов.

Это был мой первый опыт разбора сложного бинарного файла, и я хотел бы поделиться им, надеюсь, вы научитесь некоторым инструментам.

  • Краткое введение в сэмплирующий профайлер
  • Исследование с помощью file и tree
  • Поиск строк с помощью grep
  • Интерпретация plist с помощью plutil
  • О парсинге бинарных plist’ов
  • Восстановление графа объекта
  • Заголовки кастомных типов данных
  • Поиск списка экземпляров с помощью find и du
  • Исследование содержимого бинарных файлов с помощью xxd
  • Исследование бинарного формата с помощью Synalyze It!
  • Поиск двоичных последовательностей с помощью python
  • Объединим все вместе

Дисклеймер: я много раз тормозил, пытаясь понять формат файла. Для краткости то, что представлено здесь, является гораздо более “гладким” процессом, чем то, что мне пришлось пройти. Если вы не можете разобраться, пытаясь сделать что-то подобное, не отчаивайтесь!

Краткое введение в сэмплирующий профайлер

Прежде чем мы перейдем к форматам файлов, необходимо понять, какие данные нужно извлекать. Мы попытается импортировать процессорное время из профайлера, которое поможет ответить на вопрос “Какое время затрачивает моя программа?” Существуют различные способы анализа производительности исполнения программы, но самым распространенным способом является использование сэмплирующего профайлера.

Пока программа запущена, сэмплирующий профайлер периодически спрашивает запущенную программу: “Хей! Что ты СЕЙЧАС делаешь?” Программа отвечает текущим стеком вызова (или стеком вызовов, в случае многопоточной программы), а профайлер записывает текущий стек вызова вместе с текущим временем. Ручной способ сделать это, если у вас нет профайлера - просто несколько раз остановите программу в отладчике и посмотрите стек вызова.

Time Profiler в Instruments - это сэмплирующий профайлер.

После записи времени профиля в Instruments, вы можете видеть лист сэмплов с временными метками и связанным стеком вызовов.

Это именно та информация, которую мы хотим извлечь: временные метки и стек вызовов.

Исследование с помощью file и tree

Если вы хотите следовать представленным шагам, вы можете найти тестовый файл здесь: simple-time-profile.trace, который является профилем из Instruments 8.3.3. Это профиль времени простой программы, я сделал его специально для анализа без сложных потоков и мультипроцессинга: simple.cpp.

Отличный первый шаг при анализе какого-либо файла - использовать unix программу file.

File определяет тип файла по байтам. Вот пример:

$ file favicon-16x16.png
favicon-16x16.png: PNG image data, 16 x 16, 8-bit colormap, non-interlaced
$ file favicon.ico
favicon.ico: MS Windows icon resource - 3 icons, 48x48, 256-colors
$ file README.md
README.md: UTF-8 Unicode English text, with very long lines
$ file /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome
/Applications/Google Chrome.app/Contents/MacOS/Google Chrome: Mach-O 64-bit executable x86_64

Для начала посмотрим, как File определит файл с расширением .trace.

$ file simple-time-profile.trace
simple-time-profile.trace: directory

Интересно! .trace это не один файл, а каталог файлов программы Instruments.

macOS строится на концепции бандлов (пакетов), которые представляют из себя каталоги, но выглядят как файлы. Такая концепция позволяет упаковывать разные форматы файлов в единый объект. Другие форматы файлов, например .jar (Java) и .docx (Microsoft Office) достигают аналогичных целей группируя разные форматы файлов вместе в zip архив (они буквально создают .zip архивы для разных расширений файлов).

Понимая данную концепцию, давайте посмотрим на структуру каталога используя программу tree, установленную на Mac с помощью команды brew install tree.

$ tree -L 4 simple-time-profile.trace
simple-time-profile.trace
├── Trace1.run
│   ├── RunIssues.storedata
│   ├── RunIssues.storedata-shm
│   └── RunIssues.storedata-wal
├── corespace
│   ├── MANIFEST.plist
│   ├── currentRun
│   │   └── core
│   │       ├── extensions
│   │       ├── stores
│   │       └── uniquing
│   └── run1
│       └── core
│           ├── core-config
│           ├── extensions
│           ├── stores
│           ├── table-manager
│           └── uniquing
├── form.template
├── instrument_data
│   └── 20202640-0B46-4698-ADAD-DF54B3ABE816
│       └── run_data
│           └── 1.run.zip
├── open.creq
└── shared_data
    └── 1.run

…ладно! Тут мы видим слишком много данных, становится непонятно где искать интересующую нас информацию.

Поиск строк с помощью grep

Строки - один из самых простых форматов данных для поиска. В представленном случае, мы пытаемся найти имена функций программы где-нибудь в профиле. Основная функция программы, которую мы профилировали:

int main(int argc, char* argv[]) {
  while (true) {
    alpha();
    beta();
    gamma();
    delta();
  }
  return 0;
}

Если нам повезет, мы найдем строку gamma где-нибудь в открытом тексте в бандле .trace. Если данные были бы сжаты, нам бы не повезло.

$ grep -r gamma simple-time-profile.trace
Binary file simple-time-profile.trace/form.template matches

Прикольно, итак form.template содержит в себе строку gamma. Посмотрим что это за файл.

$ file simple-time-profile.trace/form.template
simple-time-profile.trace/form.template: Apple binary property list

Итак, что собой являет “Apple binary property list”, что это за список такой?

Интерпретация plist с помощью plutil

Погуглив, я нашел статью о преобразовании бинарных plists, где упоминался инструмент под названием plutil для анализа и манипуляций с содержимым бинарных plists. plutil -p кажется отличным способом вывода plists в читаемый вариант.

$ plutil -p simple-time-profile.trace/form.template
{
  "$version" => 100000
  "$objects" => [
    0 => "$null"
    1 => "rsrc://Template - samplertemplate"
    2 => {
      "NSString" => <CFKeyedArchiverUID ...>{value = 3}
      "NSDelegate" => <CFKeyedArchiverUID ...>{value = 0}
      "NSAttributes" => <CFKeyedArchiverUID ...>{value = 5}
      "$class" => <CFKeyedArchiverUID ...>{value = 11}
    }
    ...(many more entries here, excluded for brvity)
  ]
  "$archiver" => "NSKeyedArchiver"
  "$top" => {
    "com.apple.xray.owner.template" => <CFKeyedArchiverUID ...>{value = 12}
    "com.apple.xray.instrument.command" => <CFKeyedArchiverUID ...>{value = 234}
    "$1" => <CFKeyedArchiverUID ...>{value = 163}
    "cliTargetDevice" => <CFKeyedArchiverUID ...>{value = 0}
    "com.apple.xray.owner.template.description" => <CFKeyedArchiverUID ...>{value = 2}
    "$2" => <CFKeyedArchiverUID ...>{value = 164}
    "com.apple.xray.owner.template.version" => 2.1
    "com.apple.xray.owner.template.iconURL" => <CFKeyedArchiverUID ...>{value = 1}
    "$0" => <CFKeyedArchiverUID ...>{value = 141}
    "com.apple.xray.run.data" => <CFKeyedArchiverUID ...>{value = 247}
  }
}

Я не был знаком со многими Mac API интерфейсами, и лучшим вариантом было погуглить интересующую меня информацию. Информация о значении CFKeyedArchiverUID появлялась часто, и показалась взаимосвязанной со значением NSKeyedArchiver.

Еще немного погуглив, нашлось, что NSKeyedArchiver является предоставляемым Apple API для сериализации и десериализации графов объектов в файлы. Если мы сможем понять, как восстановить сериализованные графы объектов, мы сможем приблизиться к пониманию процесса извлечения необходимых нам данных.

Лучший способ изучить работу Cocoa API - использовать XCode Playground. Внутри обучающей площадки, я смог быстро сконструировать NSKeyedUnarchive и начать извлекать данные из него, но я столкнулся с рядом проблем:

В частности, встречается ошибка:

cannot decode object of class (XRInstrumentControlState) for key (NS.objects);
the class may be defined in source code or a library that is not linked

Неудивительно, что для декодирования объектов, хранящихся в архиве с ключами, вам необходимо иметь доступ к классам, которые были использованы для их кодирования. В этом случае у нас нет класса XRInstrumentControlState, поэтому архиватор не имеет понятия, как его декодировать!

Вероятно, мы могли бы обойти это ограничение путем подкласса NSKeyedUnarchiver и переопределения метода, который решает, какой класс декодировать на основе имени класса, но в конечном итоге я хочу иметь возможность читать эти файлы через JavaScript в браузере, где у меня не будет доступа к API Cocoa. Учитывая это, было бы хорошо понять, как сериализация работает напрямую.

Для извлечения данных из файла, нам необходимо делать то же самое, что и putil -p, а также, повторить действия NSKeyedUnarchiver при восстановлении графа объектов из файла plist.

О парсинге бинарных plist’ов

К счастью, парсинг бинарных plist’ов - распространенная задача с которой сталкивались многие. Вот некоторые парсеры бинарных plist’ов на разных языках программирования:

В конечном счете, я закончил внесение незначительных изменений в парсер бинарных plist’ов, который мы используем в Figma, для импорта Sketch, который вы теперь можете найти в репозитории speedscope в instruments.ts.

Восстановление графа объекта

Также может помочь, что другие люди сделали анализ того, как NSKeyedArchiver сериализует данные в список свойств. Например, пост от mac4n6 описывает пример того, как граф объектов может быть восстановлен из списка свойств. В результате это становится относительно простым процессом замены числовых идентификаторов (ID) соответствующими элементами $object из таблицы поиска.

После замены объекты будут иметь свойство, указывающее из какого класса они были сериализованы. Многие часто используемые типы данных имеют последовательные форматы сериализации, которые мы можем использовать для построения функционального представления исходного объекта.

Стоит отметить, что на удивление многие люди были заинтересованы в решении, а также любезно предоставили исходный код для решения:

Вы можете увидеть эти примеры в patternMatchObjectiveC (в исходном коде speedscope).

Заголовки кастомных типов данных.

Однако существуют типы данных в файле simple-time-profile.trace / form.template, которые относятся к Instruments. Когда мы пытаемся восстановить объект из NSKeyedArchive, нам присваивается переменная $classname. Если мы соберем все имена классов, относящиеся к Instruments, и выведем их, у нас останутся эти данные:

[
  "XRRecordingOptions",
  "XRContext",
  "XRAnalysisCoreDetailNode",
  "XRAnalysisCoreTableQuery",
  "XRMainWindowUIState",
  "XRInstrumentControlState",
  "XRRunListData",
  "XRIntKeyedDictionary",
  "PFTPersistentSymbols",
  "XRArchitecture",
  "PFTSymbolData",
  "PFTOwnerData",
  "XRCore",
  "XRThread",
  "XRBacktraceTypeAdapter"
]

Отступив назад, мы попытаемся понять, где находятся имена функций и местоположения файлов внутри файла (bundle/бандла). Из перечисленных выше классов Instruments, PFTSymbolData похож на кандидатом для хранения этой информации.

Погуглив о PFTSymbolData, можно найти страницу на github’e, демонстрирующую реверс подключаемого файла (заголовочного файла) из XCode!

Эти заголовки извлекаются с помощью class-dump. Это была удачная находка, кто-то извлек все заголовки Xcode’а и скинул в репозиторий на GitHub.

Используя заголовок в качестве ссылки и проверки данных, я смог восстановить семантически функциональное представление о PFTSymbolData.

Вы можете посмотреть релевантный код здесь - readInstrumentsKeyedArchive.

Теперь у нас есть таблица символов, но нам до сих пор нужен список экземпляров.

Поиск списка экземпляров с помощью find и du

Я надеялся, что вся необходимая информация будет собрана внутри бандла .trace, но нам не повезло.

Следующее, что стоит найти - список экземпляров собранных во время измерений. Каждый экземпляр содержит временную метку, поэтому я ожидаю, что они будут сохранены в числовой таблице. Но я не имел понимания, какие числа надо искать, потому что я не знал, как хранятся временные метки. Временные метки могут хранится, как абсолютные значения от Unix-эпохи, или могут храниться относительного предыдущего экземпляра, или относительно начала создания профиля, и могут хранится как значения с плавающей точкой или целые числа, а эти целые числа могут быть big-endian или small-endian.

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

Чтобы найти потенциальные файлы, представляющие интерес, я запустил unix pipeline:

$ find . -type f | xargs du | sort -n | tail -n 10
152     ./corespace/run1/core/stores/indexed-store-9/spindex.0
168     ./corespace/run1/core/stores/indexed-store-12/spindex.0
296     ./corespace/run1/core/stores/indexed-store-3/bulkstore
600     ./form.template
808     ./corespace/run1/core/stores/indexed-store-9/bulkstore
1064    ./corespace/run1/core/stores/indexed-store-12/bulkstore
2048    ./corespace/run1/core/uniquing/arrayUniquer/integeruniquer.data
2048    ./corespace/run1/core/uniquing/typedArrayUniquer/integeruniquer.data
20480   ./corespace/currentRun/core/uniquing/arrayUniquer/integeruniquer.data
20480   ./corespace/currentRun/core/uniquing/typedArrayUniquer/integeruniquer.data

Давайте рассмотрим pipeline.

  • find . -type f находит все файлы в текущем каталоге, выводя построчно (find man)
  • xargs du запускает du для поиска размеров файлов, используя список, связанный с ним в качестве аргументов с использованием xargs. В качестве альтернативы мы могли бы использовать du $(find . -type f). (xargs man, du man)
  • sort -n сортирует в порядке возрастания номеров (sort man)
  • tail -n 10 выводит последние 10 строк (tail man)

Мы можем расширить эту команду, чтобы узнать тип каждого файла:

$ find . -type f | xargs du | sort -n | tail -n 10 | cut -f2 | xargs file
./corespace/run1/core/stores/indexed-store-9/spindex.0:                     data
./corespace/run1/core/stores/indexed-store-12/spindex.0:                    data
./corespace/run1/core/stores/indexed-store-3/bulkstore:                     data
./form.template:                                                            Apple binary property list
./corespace/run1/core/stores/indexed-store-9/bulkstore:                     data
./corespace/run1/core/stores/indexed-store-12/bulkstore:                    data
./corespace/run1/core/uniquing/arrayUniquer/integeruniquer.data:            data
./corespace/run1/core/uniquing/typedArrayUniquer/integeruniquer.data:       data
./corespace/currentRun/core/uniquing/arrayUniquer/integeruniquer.data:      data
./corespace/currentRun/core/uniquing/typedArrayUniquer/integeruniquer.data: data

С помощью команды cut можно извлечь столбцы данных из простой текстовой таблицы. В представленном кейсе cut -f2 выбирает только второй столбец с данными. Затем мы запускаем команду file для каждого результата. (cut man)

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

Исследование содержимого бинарных файлов с помощью xxd

xxd это инструмент для получения hex-дампа из бинарного файла (см. xxd man).

$ echo "hello" | xxd
00000000: 6865 6c6c 6f0a                           hello.

Здесь мы видим вывод смещения (00000000:), шестнадцатеричного представления байтов в файле (6865 6c6c 6f0a) и соответствующую интерпретацию ASCII этих байтов (hello.), причем вместо непечатаемых символов используется точка. Точка в этом случае соответствует байту 0a, который в свою очередь соответствует символу ASCII \n, сгенерированным echo.

Вот пример использования printf для генерации 3 байт непечатаемых символов:

$ printf "\1\2\3" | xxd
00000000: 0102 03                                  ...

Давайте используем это для изучения самого большого найденного файла.

$ xxd corespace/currentRun/core/uniquing/typedArrayUniquer/integeruniquer.data | head -n 10
00000000: 6745 2301 7e33 0a00 0100 0000 0000 0000  gE#.~3..........
00000010: 0100 0000 0000 0000 0000 0000 0000 0000  ................
00000020: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000030: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000040: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000050: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000060: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000070: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000080: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000090: 0000 0000 0000 0000 0000 0000 0000 0000  ................

Похоже, что в этом файле много данных, которые имеют нулевой вывод. Список экземпляров не может быть заполнен нулями, поэтому стоит посмотреть на байты не равные нулю.
Флаг -a в xxd может быть полезен в данной ситуации.

 -a | -autoskip
              toggle autoskip: A single '*' replaces nul-lines.  Default off.

$ xxd -a ./corespace/currentRun/core/uniquing/typedArrayUniquer/integeruniquer.data
00000000: 6745 2301 7e33 0a00 0100 0000 0000 0000  gE#.~3..........
00000010: 0100 0000 0000 0000 0000 0000 0000 0000  ................
00000020: 0000 0000 0000 0000 0000 0000 0000 0000  ................
*
009ffff0: 0000 0000 0000 0000 0000 0000 0000 0000  ................

Ну, кажется этот файл не содержит много полезных данных. Давайте отсортируем наш список файлов, на этот раз сортируя по количеству ненулевых строк.

$ for f in $(find . -type f); do echo "$(xxd -a $f | wc -l) $f"; done | sort -n | tail -n 10
    1337 ./corespace/currentRun/core/extensions/com.apple.dt.instruments.ktrace.dtac/knowledge-rules-0.clp
    1337 ./corespace/run1/core/extensions/com.apple.dt.instruments.ktrace.dtac/knowledge-rules-0.clp
    2232 ./corespace/currentRun/core/extensions/com.apple.dt.instruments.poi.dtac/binding-rules.clp
    2232 ./corespace/run1/core/extensions/com.apple.dt.instruments.poi.dtac/binding-rules.clp
    2391 ./corespace/run1/core/uniquing/arrayUniquer/integeruniquer.data
    2524 ./corespace/run1/core/stores/indexed-store-12/spindex.0
    2736 ./corespace/run1/core/stores/indexed-store-9/spindex.0
    5148 ./corespace/run1/core/stores/indexed-store-9/bulkstore
    6793 ./corespace/run1/core/stores/indexed-store-12/bulkstore
   18091 ./form.template

Мы уже знаем, что такое form.template, поэтому мы начнем со второго по величине. Если мы посмотрим на indexed-store-12/bulkestore, можно предположить, что там будут какие-то полезные данные, начиная со смещения 0x1000.

$ xxd -a ./corespace/run1/core/stores/indexed-store-12/bulkstore | head -n 20
00000000: 0a0a 3412 0300 0000 2800 0000 0010 0000  ..4.....(.......
00000010: 2100 0000 0040 0800 0040 0000 0000 0000  !....@...@......
00000020: 0000 0000 0000 0000 0000 0000 0000 0000  ................
*
00001000: 796c 8f2b 0000 0400 0000 0000 0006 0000  yl.+............
00001010: 0004 0000 0040 420f 0000 0000 00fe 0000  .....@B.........
00001020: 00a4 9ddc 2b00 0004 0000 0000 0000 0200  ....+...........
00001030: 0000 0400 0000 4042 0f00 0000 0000 0001  ......@B........
00001040: 0000 5d7f 0a2c 0000 0400 0000 0000 0002  ..]..,..........
00001050: 0000 0004 0000 0040 420f 0000 0000 0002  .......@B.......
00001060: 0100 0039 fb19 2c00 0004 0000 0000 0000  ...9..,.........
00001070: 0000 0000 0400 0000 4042 0f00 0000 0000  ........@B......
00001080: 0401 0000 336b 292c 0000 0400 0000 0000  ....3k),........
00001090: 0000 0000 0004 0000 0040 420f 0000 0000  .........@B.....
000010a0: 0005 0100 0026 e538 2c00 0004 0000 0000  .....&.8,.......
000010b0: 0000 0000 0000 0400 0000 4042 0f00 0000  ..........@B....
000010c0: 0000 0701 0000 4c5a 482c 0000 0400 0000  ......LZH,......
000010d0: 0000 0000 0000 0004 0000 0040 420f 0000  ...........@B...
000010e0: 0000 0009 0100 0040 ce57 2c00 0004 0000  .......@.W,.....
000010f0: 0000 0000 0000 0000 0400 0000 4042 0f00  ............@B..

@B в правом столбце, хотя и не явно, но семантически значимый, повторяется с регулярным интервалом. Если сможем понять этот регулярный интервал, можно догадаться, какова структура данных. Мы можем попытаться угадать разные интервалы, используя аргумент -c xxd и попробовать изменить группировку байтов с помощью аргумента -g.

     -c cols | -cols cols
              format <cols> octets per line. Default 16 (-i: 12, -ps: 30, -b: 6).

     -g bytes | -groupsize bytes
              separate the output of every <bytes> bytes (two hex characters or eight bit-digi

Кажется, мы получаем выравнивание между повторяющимися значениями, когда мы группируем данные в 33 символьную последовательность:

$ xxd -a -c 33 -g0 ./corespace/run1/core/stores/indexed-store-12/bulkstore | cut -d' ' -f2 | head -n 10
0a0a34120300000028000000001000002100000000400800004000000000000000
000000000000000000000000000000000000000000000000000000000000000000
*
00000000796c8f2b000004000000000000060000000400000040420f0000000000
fe000000a49ddc2b000004000000000000020000000400000040420f0000000000
000100005d7f0a2c000004000000000000020000000400000040420f0000000000
0201000039fb192c000004000000000000000000000400000040420f0000000000
04010000336b292c000004000000000000000000000400000040420f0000000000
0501000026e5382c000004000000000000000000000400000040420f0000000000
070100004c5a482c000004000000000000000000000400000040420f0000000000

Отлично! Это говорит о том, что этот формат файла использует 33 байта на запись, и, надеюсь, каждая из этих записей соответствует одному экземпляру в профиле.

Осмотрев каталог, который содержит этот bulkstore, мы находим файл с интересным названием schema.xml:

$ cat ./corespace/run1/core/stores/indexed-store-12/schema.xml
<schema name="time-profile" topology="XRT50_C22_TypeID">
    <column engineeringType="XRSampleTimestampTypeID" engineeringName="Sample Time" mnemonic="time" topologyField="XRTraceRelativeTimestampFieldID"></column>
    <column engineeringType="XRThreadTypeID" engineeringName="Thread" mnemonic="thread" topologyField="XRCategory1FieldID"></column>
    <column engineeringType="XRProcessTypeID" engineeringName="Process" mnemonic="process"></column>
    <column engineeringType="XRCPUCoreTypeID" engineeringName="Core" mnemonic="core"></column>
    <column engineeringType="XRThreadStateTypeID" engineeringName="State" mnemonic="thread-state"></column>
    <column engineeringType="XRTimeSampleWeightTypeID" engineeringName="Weight" mnemonic="weight"></column>
    <column engineeringType="XRBacktraceTypeID" engineeringName="Stack" mnemonic="stack"></column>
</schema>

Отлично! XRSampleTimestampTypeID и XRBacktraceTypeID кажутся чрезвычайно актуальными.

Следующий шаг - понять, как эти 33 байтовые записи отображаются в schema.xml.

Исследование бинарного формата с помощью Synalyze It!

В этом исследовании все инструменты, которые я использовал, были стандартными для большинства установок unix, все они бесплатны и с открытым исходным кодом. Хотя я, безусловно, мог использовать только стандартные инструменты, мой друг Питер Собот познакомил меня с инструментом, который сделал этот процесс намного проще.

Synalyze It! представляет собой шестнадцатеричный редактор и инструмент бинарного анализа для OS X. Версия для Windows и Linux называется Hexinator. Эти инструменты помогают угадывать структуру формата файла (например: “Я думаю, что файл представляет собой список структур, каждая из которых занимает 20 байт, где первые 4 байта представляют собой беззнаковый тип int, а следующие 16 байт являются строкой ASCII фиксированной длины”), а затем анализировать файл на основе этого предположения и представлять как подсвеченный HEX-дамп, так и в расширяемом древовидном представлении. Данный инструмент помогает мне проверять гипотезы о структуре файла.

В итоге я смогу определить длину и смещение полей, которые меня заинтересовали. Synalyze It! помогает вам визуализировать парсинг информации задавая цвета для разных полей. Здесь я установил временную метку экземпляра зеленым цветом, а идентификатор бэктрейса - красным.

Изучая значения времени выборки и сравнивая их с показами Instruments, я смог сделать вывод о том, что значения представляют собой число наносекунд, так как профиль начался как шестибайтное беззнаковое целое число. Я смог проверить это, отредактировав двоичный файл и затем снова открыв его в Instruments.

Мило! Таким образом, можно ответить на вопрос о том, где хранится информации о экземпляре, и мы знаем, как интерпретировать данные временных меток. Но мы все еще не совсем знаем, как превратить идентификатор бэктрейса в трассировку стека.

Чтобы попытаться найти стек, мы можем посмотреть, отображаются ли адреса памяти, идентифицированные как часть таблицы символов, где-нибудь вне бинарного plist form.template.

Поиск двоичных последовательностей с помощью python

Вот те же самые символьные данные, которые мы видели ранее:

Итак, давайте посмотрим, можем ли мы найти один из этих адресов, упомянутых где-то еще в бандле .trace. Мы рассмотрим третий адрес в этом списке, 4536213276.

В качестве первой попытки можно предположить, что число записывается как строка.

$ grep -a -R -l '4536213276' .

Нет результатов. Это был выстрел в воздух. Давайте попробуем более правдоподобную идею поиска бинарной записи этого числа.

Существует два стандартных способа кодирования многобайтовых целых чисел в поток байтов. Один называется “little endian”, другой “big endian”. В первом случае мы записываем байты от младшего к старшему, то есть самый маленький байт записывается первым. Во втором случае самый большой байт записывается первым.

Используя стандартную питоновскую библиотеку struct, мы можем увидеть, как выглядят каждое из этих представлений.

Число слишком велико, чтобы соответствовать 32-битовому целому числу, поэтому, вероятно, это 64-битное целое число, что имеет смысл, если это адрес памяти, который должен поддерживать 64-битные адреса.

$ python -c 'import struct, sys; sys.stdout.write(struct.pack(">Q", 4536213276));' | xxd
00000000: 0000 0001 0e61 1f1c                      .....a..
$ python -c 'import struct, sys; sys.stdout.write(struct.pack("<Q", 4536213276));' | xxd
00000000: 1c1f 610e 0100 0000                      ..a.....

Q предписывает struct.pack кодировать число в виде 64-битного беззнакового целого числа записанного big-endian, а <Q соответствует little-endian 64-битного беззнакового целого числа.

Если вы разделите байты, вы увидите, что в обеих кодировках одинаковые байты, в обратном порядке:

Теперь мы можем использовать небольшую программу python для поиска файлов со значением, которое мы ищем.

$ cat search.py
import os, glob, struct

addr = 4536213276
little = struct.pack('<Q', addr)
big = struct.pack('>Q', addr)

for (dirpath, dirnames, filenames) in os.walk('.'):
  for f in filenames:
    path = os.path.join(dirpath, f)
    contents = open(path).read()
    if little in contents:
      print 'Found little in %s' % path
    elif big in contents:
      print 'Found big in %s' % path
$ python search.py
Found big in ./form.template
Found little in ./corespace/run1/core/uniquing/arrayUniquer/integeruniquer.data

Отлично! Значение находится в двух местах: один little endian, один big endian. Первое место, где мы нашли нужные нам данные - form.template (о нем мы уже знаем), второе место - файл integeruniquer.data (его мы еще не исследовали). Он также был одним из файлов, которые мы находили при поиске файлов с большим количеством ненулевых данных.

После этого я просматривал данных файл в Synalyze It! и обнаружил, что файл содержит массивы целых чисел длиной 32 бита, за которыми следует список из 64-битных целых чисел.

Таким образом, integeruniquer.data содержит массив массивов из 64-битных целых чисел. Прекрасно! Кажется, что каждый 64-битный int является либо адресом памяти, либо индексом в массиве массивов. Это был последний фрагмент загадки, который нам нужен для анализа профилей.

Объединим все вместе

Таким образом, окончательный процесс выглядит следующим образом:

  1. Найдите список экземпляров найденных в bulkstore файлах рядом с schema.xml, который содержит строку <schema name=“time-profile”.
  2. Извлеките список (timestamp, backtraceID) кортежей из bulkstore
  3. Используйте backtrace ID в качестве индекса массива, представленного arrayUniquer/integeruniquer.data, преобразуйте список кортежей(timestamp, backtraceID) в список кортежей (timestamp, address[]).
  4. Распарсите бинарный plist form.template и извлеките символьные данные из PFTSymbolData из полученного NSKeyedArchive. Преобразуйте это в сопоставление адреса (address) к имени функции и пути к файлу (function name, file path).
  5. Используя address → (function name, file path) в сочетании с кортежем содержащим временную метку и адрес (timestamp address[]), создайте список кортежей (timestamp, (function name, file path)[]). Это окончательная информация необходимая для создания flamegraph’а!

Мною была проделана большая работа, в конечном итоге ее результатом стал относительно простым алгоритмом для извлечения данных. Вы можете найти реализацию в importFromInstrumentsTrace в качестве источника для speedscope на GitHub.

Если вы захотите попробовать speedscope, пожалуйста твитните мне @jlfwong свое мнение об этом инструменте!

Спасибо, Питеру Соботу, Райану Каплану и Руди Чен за предоставленную обратную связь по данному посту.

© Translated by turbobarsuchiha special for r0 Crew