Продолжаем серию про чтение бинарных файлов iOS-приложений. Для понимания технических деталей рекомендуется почитать первую часть здесь. В этой статье посмотрим, как укладывается в бинарный файл код на Swift.
Итак, создаем 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-класса, но не совсем:
- Два 64-битных слова до начала структуры также относятся к описанию класса.
- Последний бит указателя на raw_data равен 1. Этот бит служит идентификатором того, что класс написан на Swift.
- После некоторого набора фиксированных полей идет часть переменного размера, виртуальная таблица методов и других членов класса.
- Структура 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-битных слов. Они не распарсены хоппером, и поэтому выглядят в нем как-то так (здесь подряд записаны 0x100008144 и 0x100008158):
Представим это в более удобоваримом виде. 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 не генерирует некоторые сеттеры и геттеры, причем не руководствуется, казалось бы, логичным правилом "для переменных иваров генерировать, для константных — нет".
В третьих, отметим, что названия и интерфейсы методов предоставил хоппер, и достал он их из таблицы символов. Однако соответствующие символы не нужны для функционирования программы, так что на практике их вырезают из бинарного файла. Поэтому обычно информацию о сигнатурах свифтовых методов нельзя получить из бинарного файла, за исключением случая, который мы обсудим позже.
Остановимся теперь на дескрипторе класса. Указатель на дескриптор знаковый. Например, в нашем бинарном файле этот указатель лежит по адресу 0x1000094a0 и записывается 0xffffffffffffd9e8. 0xffffffffffffd9e8 — это шестнадцатеричная запись отрицательного числа -0x2618. Получаем: 0x1000094a0 — 0x2618 = 0x100006e88 — адрес, по которому лежит дескриптор. В дескрипторе хранятся следующие данные:
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 ассемблер, представление о нем можно получить здесь):
Здесь на стек сохраняются типы, ссылки на которые лежат по адресам 0x100008000 и 0x100008008. Смотрим, что там лежит:
Видим распарсенные хоппером __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:”. Посмотрим код этого метода:
Это снова код на arm64 ассемблере, и все, что нам надо про него знать, — это то, что вызовам из него других методов соответствуют инструкции bl. Видим, что вызывается соответствующий свифтовый метод. Даже если у нас нет таблицы символов, этот метод можно вычислить, так как все остальные вызовы (инструкции bl) — это retain и release, их символы не вырезаются.
ClassMethod находится таким же способом в метаклассе. Теперь интерфейс восстанавливается гораздо лучше:
class InspectedObject {
var intVar : Int
let stringConst : String
func instanceMethodWithArg(Int) -> Int
func toBeOverriden()
static func classMethod()
}