Найти тему
Nuances of programming

Скрытые нововведения в iOS 15

Оглавление

Источник: Nuances of Programming

Самая интригующая часть нового функционала, появившегося по итогам проведения конференции WWDC21, была запрятана в замечаниях к выпуску Xcode 13.

Все программы и библиотеки dylib, имеющие в качестве целевой платформы развертывания macOS 12 или iOS 15 и более поздние их версии, теперь используют формат объединенных в цепочки адресных привязок. При этом задействуются различные команды загрузки и данные LINKEDIT, а сами эти программы и библиотеки не будут запускаться или загружаться на более ранних версиях ОС.

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

dyld

Динамический компоновщик (dyld) представляет собой точку входа каждого приложения и обеспечивает подготовку кода к запуску. Поэтому разумно ожидать, что любое улучшение dyld приведет к уменьшению времени запуска приложения. Прежде чем вызывается main, запускается блок статической инициализации или настраивается среда выполнения Objective-C, dyld выполняет адресные привязки. Они состоят из операций перемещения и привязки, которые изменяют указатели в двоичном файле приложения. Причем делают это так, чтобы они содержали адреса, которые будут действительны во время выполнения. Посмотрим, как эти операции выглядят. Для этого воспользуемся инструментом командной строки dyldinfo.

% xcrun dyldinfo -rebase -bind Snapchat.app/Snapchat
rebase information (from compressed dyld info):
segment section address type
__DATA __got 0x10748C0C8 pointer
...
bind information:
segment section address type addend dylib symbol
__DATA __const 0x107595A70 pointer 0 libswiftCore _$sSHMp

И вот что здесь происходит. Адрес 0x10748C0C8 находится в __DATA/__got. Его надо переместить, т. е. сдвинуть на некое постоянное значение, которое так и называется «сдвиг». А когда адрес 0x107595A70 находится уже в __DATA/__const и должен указывать на дескриптор протокола для Hashable из libswiftCore.dylib, dyld использует команду загрузки LC_DYLD_INFO и структуру dyld_info_command для определения в двоичном файле местоположения и размера перемещений, привязок и экспортируемых символов. На этом ресурсе происходит парсинг этих данных для визуальной оценки влияния перемещений и привязок на размер двоичного кода, а также предлагаются флаги компоновщика для уменьшения этого размера:

-2

Новый формат

Когда я впервые загрузил туда приложение для iOS 15, визуализации адресных привязок dyld еще не было. Отсутствовавшая ​команда загрузки LC_DYLD_INFO_ONLY была заменена на LC_DYLD_CHAINED_FIXUPS и LC_DYLD_EXPORTS_TRIE.

% otool -l iOS14Example.app/iOS14Example | grep LC_DYLD
cmd LC_DYLD_INFO_ONLY% otool -l iOS15Example.app/iOS15Example | grep LC_DYLD
cmd LC_DYLD_CHAINED_FIXUPS
cmd LC_DYLD_EXPORTS_TRIE

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

Часть префиксного дерева экспортируемых символов для «Википедии»
Часть префиксного дерева экспортируемых символов для «Википедии»

Единственное изменение в iOS 15: на данные теперь ссылаются с помощью linkedit_data_command, в котором содержится смещение первого узла. Чтобы удостовериться в этом, пришлось написать короткое приложение на Swift для выполнения парсинга двоичного кода iOS 15 и вывода каждого символа:

let bytes = (try! Data(contentsOf: url) as NSData).bytes
bytes.processLoadComands { load_command, pointer in
if load_command.cmd == LC_DYLD_EXPORTS_TRIE {
let dataCommand = pointer.load(as: linkedit_data_command.self)
bytes.advanced(by: Int(dataCommand.dataoff)).readExportTrie()
}
}

extension UnsafeRawPointer {
func readExportTrie() {
var frontier = readNode(name: "")
guard !frontier.isEmpty else { return }

repeat {
let (prefix, offset) = frontier.removeFirst()
let children = advanced(by: Int(offset)).readNode(name: prefix)
for (suffix, offset) in children {
frontier.append((prefix + suffix, offset))
}
} while !frontier.isEmpty
}

// Возвращает массив дочерних узлов и их смещение
func readNode(name: String) -> [(String, UInt)] {
guard load(as: UInt8.self) == 0 else {
// Это конечный узел
print("symbol name \(name)")
return []
}
let numberOfBranches = UInt(advanced(by: 1).load(as: UInt8.self))
var mutablePointer = self.advanced(by: 2)
var result = [(String, UInt)]()
for _ in 0..<numberOfBranches {
result.append(
(mutablePointer.readNullTerminatedString(),
mutablePointer.readULEB()))
}
return result
}
}

Цепочки привязок

Реальное изменение произошло в LC_DYLD_CHAINED_FIXUPS. До iOS 15 перемещения, привязки и «ленивые» привязки хранились каждая в отдельной таблице. Теперь они объединены в цепочки, причем имеются указатели на начало цепочек, содержащихся в этой новой команде загрузки.

Читайте также:

Читайте нас в Telegram, VK

Перевод статьи Noah Martin: How iOS 15 makes your app launch faster