Читаем бинарные файлы iOS-приложений. Часть 2: Swift

Продолжаем серию про чтение бинарных файлов iOS-приложений. Для понимания технических деталей рекомендуется почитать первую часть здесь. В этой статье посмотрим, как укладывается в бинарный файл код на Swift.


ba61a90a69ad416089cc44fb0c45c44d.jpg

Итак, создаем Single View Application на Swift и добавляем следующий Inspected.swift:


import Foundation

class InspectedObject {
    var intVar : Int = 57
    let stringConst = "const string"

    func instanceMethod(arg:Int) -> Int {
        return arg + 57
    }

    func toBeOverriden() {}

    static func classMethod() {}
}

class SubInspectedObject: InspectedObject {
    var subConstInt = 1543;
    let subStringVar = "sub const string"

    func subInstanceMethod() {}

    override func toBeOverriden() {}
}

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


Снова находим наш класс через objc_classlist. Вместо имени видим замангленную (mangled) строку: __TMC12InspectedApp15InspectedObject. Я не буду здесь подробно обсуждать алгоритм манглинга свифта, но это и не особо нужно, потому что вместе с достаточно новым Xcode поставляется утилита swift-demangle, которая лежит по примерно такому пути:


/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift-demangle


Прогоняя через swift-demangle, получаем:


_TMC12InspectedApp15InspectedObject ---> type metadata for InspectedApp.InspectedObject

То есть по этому адресу лежит описание класса InspectedObject, логично. Смотрим на описание, видим такую же структуру, что и у Objective-C-класса, но не совсем:


  1. Два 64-битных слова до начала структуры также относятся к описанию класса.
  2. Последний бит указателя на raw_data равен 1. Этот бит служит идентификатором того, что класс написан на Swift.
  3. После некоторого набора фиксированных полей идет часть переменного размера, виртуальная таблица методов и других членов класса.
  4. Структура raw_data также присутствует, но вся информация, которая в ней есть, также есть и в дескрипторе класса.

Устройство Swift-класса в бинарном файле можно поизучать в исходниках. Запись класса собирается из полей следующих классов из этого файла:


HeapMetadataHeaderPrefix (destructor),
TypeMetadataHeader (value witness table),
TypeMetadataHeader (kind=isa),
TargetClassMetadata (все остальное).


Собираем вместе:


struct swift_class
{                   
    uint64 destructor_addr;     // адрес деструктора
    uint64 witness_table_addr; // адрес таблицы служебных методов класса, позволяющих раскладывать объекты в памяти и манипулировать ими
    uint64 metaclass_addr;  // как в Objective-C
    uint64 superclass_addr; // как в Objective-C
    uint64 cache_addr;      // как в Objective-C
    uint64 vtable_addr;     // как в Objective-C
    uint64 data_addr;       // как в Objective-C + 1 младшем бите
    uint32 class_flags;     // свифтовые флаги (см ниже)
    uint32 inst_addr_point; // куда, относительно начала экземпляра класса, указывают указатели на экземпляр
    uint32 inst_size;       // размер экземпляра класса
    uint16 inst_align_mask; // маска выравнивания 
    uint16 reserved;        // зарезервировано для использования в рантайме
    uint32 class_size;      // размер объекта-класса
    uint32 class_addr_point;    //  куда, относительно начала класса, указывают указатели на класс
    int64 descriptor_rel_addr;  // относительный указатель на дескриптор класса (см. ниже)
    int64 ivar_destroyer;       // метод для деаллокации иваров при преждевременном возвращении из конструктора (при возникновении исключения)
}

Свифтовые флаги — это объект типа ClassFlags отсюда.


После этой фиксированной структуры идут члены класса, разложенные следующим образом:


  • Члены суперкласса (рекурсивно).
  • Должна быть некоторая ссылка на данные родителя, но в текущей реализации всегда нулевое 64-битное слово.
  • Параметры шаблона для этого класса.
  • Переменные класса (если когда-нибудь Swift будет поддерживать их в таком виде).
  • Виртуальные методы.

Посмотрим на классы InspectedObject и SubInspectedObject в нашем сгенерированном бинарном файле. Обратим внимание на переменную часть после деструктора переменных. Это несколько 64-битных слов. Они не распарсены хоппером, и поэтому выглядят в нем как-то так (здесь подряд записаны 0×100008144 и 0×100008158):


2cc9e80ad4fb49ecb2c22adc63387199.png

Представим это в более удобоваримом виде. InspectedObject:


0x1000041b4 // intVar getter
0x1000041c8 // intVar setter
0x1000041e0 // intVar.materializeForSet() -- метод, возвращающий указатель на место в памяти, где лежит intVar (подробнее -- [здесь](https://github.com/apple/swift/blob/swift-3.0.1-preview-2-branch/docs/proposals/Accessors.rst))
0x100004108 // instanceMethod (arg : Int) -> Int
0x100004138 // InspectedObject.toBeOverriden() -- этот метод переопределяется в сабклассе
0x1000081d8 // InspectedObject.init () ->InspectedObject
0x10        // отступ для intVar
0x18        // отступ stringConst

SubInspectedObject:
0x1000041b4 // intVar getter
0x1000041c8 // intVar setter
0x1000041e0 // intVar.materializeForSet()
0x100004108 // instanceMethod (arg : Int) -> Int
0x100004344 // SubInspectedObject.toBeOverriden() -- переопределенный метод на месте исходного метода
0x10000447c // SubInspectedObject.init() -> SubInspectedObject -- init также на месте init суперкласса
0x10        // отступ для intVar
0x18        // отступ stringConst

Здесь заканчиваются члены суперкласса. Далее:


0x1000043e8 // subConstInt getter
0x1000043fc  // subConstInt setter
0x100004414 // subConstInt.materializeForSet()
0x100004334 // SubInspectedObject.subInstanceMethod ()
0x30               // отступ для subConstInt
0x38               // отступ для subStringVar   

Отметим пару моментов.


Во-первых, ссылка на метод toBeOverriden () располагается на одном и том же месте в InspectedObject и SubInspectedObject. Это позволяет Swift вызывать виртуальные методы по отступу от начала класса.


Во-вторых, Swift не генерирует некоторые сеттеры и геттеры, причем не руководствуется, казалось бы, логичным правилом «для переменных иваров генерировать, для константных — нет».
В третьих, отметим, что названия и интерфейсы методов предоставил хоппер, и достал он их из таблицы символов. Однако соответствующие символы не нужны для функционирования программы, так что на практике их вырезают из бинарного файла. Поэтому обычно информацию о сигнатурах свифтовых методов нельзя получить из бинарного файла, за исключением случая, который мы обсудим позже.


Остановимся теперь на дескрипторе класса. Указатель на дескриптор знаковый. Например, в нашем бинарном файле этот указатель лежит по адресу 0×1000094a0 и записывается 0xffffffffffffd9e8. 0xffffffffffffd9e8 — это шестнадцатеричная запись отрицательного числа -0×2618. Получаем: 0×1000094a0 — 0×2618 = 0×100006e88 — адрес, по которому лежит дескриптор. В дескрипторе хранятся следующие данные:


struct {
    int32 name_addr;  // относительный адрес имени
    uint32 num_fields;  // количество иваров
    uint32 fields_offsets_vector_offset;     // отступ от начала класса до вектора отступов иваров
    int32 fields_names_addr;  // по этому адресу подряд выписаны названия иваров
    int32 fields_types_accessor_addr;  // относительный адрес метода, возвращающего вектор типов иваров
    uint32 generic_pattern_and_kind;  // информация для шаблонных классов
    int32 metadata_accessor_addr;  // относительный указатель на метод, возвращающий указатель на данные класса, используется при конструировании объектов класса
}

Получается, что информация о типах иваров не хранится в явном виде. Однако ее можно извлечь из кода метода fields types accessor. Например, fields types accessor для InspectedObject имеет следующие строки (arm64 ассемблер, представление о нем можно получить здесь):


f6b4181c184c4080a76951cd957f4c32.png

Здесь на стек сохраняются типы, ссылки на которые лежат по адресам 0×100008000 и 0×100008008. Смотрим, что там лежит:


102fa72e582443979810ec807fe2fe07.png

Видим распарсенные хоппером __TMSS и __TMSi, которые размангливаются в Swift.String и Swift.Int. Соответствующие символы нелокальные и не вырезаются из таблицы символов.


Итак, собирая все вместе и предполагая отсутствие символов, соответствующих внутренним методам, получаем следующий восстановленный интерфейс класса InspectedObject:


class InspectedObject {
    var intVar : Int;
    var stringConst : String;

    func sub_100004108()
    func    sub_100004138()

}

Заметим, что метод класса classMethod () генерируется как независимая функция, и восстановить его наличие по одному только бинарному коду невозможно.


В целом восстановленный по Swift-классу интерфейс довольно скуден. Однако если класс имеет Objective-C-класс в качестве предка, то он поддерживает режим совместимости с Objective-C, и все свифтовые методы заворачиваются в Objective-C-методы, что позволяет восстановить имена.


Итак, добавляем в объявление InspectedObject наследование от NSobject:


class InspectedObject : NSObject {
...
}

Смотрим в бинарный файл. Теперь, raw_data заполнена, видим все методы, объявленные, включая сеттеры, геттеры, а также ClassMethod () в метаклассе. Имена методов немного изменены, например, вместо «InstanceMethod» видим «instanceMethodWithArg:». Посмотрим код этого метода:


5eb36e2260e44315b76323816c312bf6.png

Это снова код на arm64 ассемблере, и все, что нам надо про него знать, — это то, что вызовам из него других методов соответствуют инструкции bl. Видим, что вызывается соответствующий свифтовый метод. Даже если у нас нет таблицы символов, этот метод можно вычислить, так как все остальные вызовы (инструкции bl) — это retain и release, их символы не вырезаются.


ClassMethod находится таким же способом в метаклассе. Теперь интерфейс восстанавливается гораздо лучше:


class InspectedObject {
    var intVar : Int
    let stringConst : String

    func instanceMethodWithArg(Int) -> Int
    func toBeOverriden()
    static func classMethod()
}

Комментарии (1)

  • 6 апреля 2017 в 14:18

    0

    Статьи агонь, спасибо большое!

© Habrahabr.ru