четверг, 26 марта 2020 г.

Почему оптимизация memset накрылась...

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

Такую же причуду удалось получить на функции memcpy. GCC чудит и внутрь вставляет рекурсию, функция вызывает себя. Это происходит при использовании оптимизации -O3.

Отключить оптимизацию можно ключом -fno-tree-loop-distribute-patterns.

понедельник, 23 марта 2020 г.

ELF32 для ARM Cortex-M

Мы изучили идеи, которые вложены в загрузчик GNU/Linux. Поигрались с возможностями компилятора (gcc, clang) и линковщика (ld). Готовы описать, как выглядит сборка проекта и как ее упростить. Наша цель встроить в контроллер ARM Cortex-M4 функцию динамической загрузки приложений и загрузку динамических библиотек. Зачем? Чтобы менять программу в процессе работы. Я вынашиваю идеи как делегировать на контроллер обработку данных.

[IHI0044] ELF for the Arm Architecture
ELF format
ELF32 -- бинарный формат для представления объектных и перемещаемых файлов, исполняемых файлов, разделяемых данных и динамических библиотек. Формат состоит из разделов (сегментов). Исполняемый файл может содержать загружаемые сегменты (программы). Сегменты содержат: таблицы символов, строки с именами объектов и функций, а также необходимые данные для сборки кода (таблицы перемещений).
Таблица перемещения - описывает, как нужно модифицировать переменную или вызов функции, с учетом того, что на этапе компиляции не определены абсолютные адреса переменных и адреса функций. Т.е адреса должны подставляться в скомпилированный код на этапе сборки. Фактически код после компилятора представляет из себя шаблон, в который нужно вписать ссылки. Для каждой целевой платформы определяется, как именно вписывается ссылка в сегмент кода и сегмент данных - правила модификации кода. Наша целевая платформа - ARM с системой команд Thumb32. Арм определил ряд правил сборки кода [IHI0044]. Этот список содержит около сотни разных правил. Правила могут быть сложные, сложнее, чем просто записать адрес по указанному смещению. Обычно правила выглядят примерно так: Взять содержимое инструкции, декодировать инструкцию, к аргументу инструкции добавить смещение - адрес сегмента, вычесть адрес самой инструкции, и закодировать инструкцию обратно. Переменные (данные, data) и функции (код, text) могут размещаться в разных сегментах, поэтому при сборке возникают дополнительные операции.
Мы действуем методом обратной разработки. На входе уже есть готовый ELF формат с таблицей перемещений. В формате используются всего четыре разные правила. Заметим, что загрузчик исполняется в составе операционной системы под совершенно определенную архитектуру. Мы будем обрабатывать только те правила, которые возникают на данной архитектуре. Таким образом, число правил существенно уменьшается. Получаем всего 2 правила сборки:
R_ARM_ABS32 -- это правило абсолютного перемещения. Data: (S + A) | T. Сложить содержимое ячейки и адрес сегмента. Для адреса функции не забыть правильно указать флаг системы команд.

R_ARM_THM_CALL -- это правило кодирования инструкции вызова функции Thumb32: ((S + A) | T) - P (маска=0x01FFFFFE) \sa R_ARM_THM_PC22. Сложить содержимое ячейки и адрес сегмента и вычесть адрес самой ячейки. Для ссылки на функцию добавить флаг системы команд Thumb - единичку в младшем разряде. Такое же правило форматирования используется для вызовов R_ARM_THM_JUMP24. Тут мы используем некоторое упрощение- длина вызова - 22 бита вместо 24, два бита не обрабатываем. На вопрос почему, ответ: потом что так обрабатывает линковщик GNU binutils, такое упрощение рекомендует ARM.

Компилятор GCC использует правило R_ARM_TARGET1 для формирования таблицы инициализации - таблицы конструкторов и деструкторов. Это правило может быть таким же, как и R_ARM_ABS32, мы определяем как выглядит это правило, потому что инициализация - это дело разработчика операционной системы, наше дело. Мы используем тот же метод, R_ARM_ABS32.

Компилятор Clang использует еще пару правил. Эти правила возникают из идеи грузить константы 32 бита в два приема - отдельно младшую (lower16), отдельно старшую часть (upper16).

Сделал утилиту, которая работает в точности как readelf (GNU binutils), разбирает таблицы символов и перемещений. Зачем сделал, не очень понимаю. Результатом является возможность реализации динамической загрузки и динамической линковки кода в контроллере, разбор elf формата. Практического приложения пока нет. Пока разбирался с возможностями линковки увидел возможность собирать операционку с использованием статических библиотек. Статические библиотеки можно прикомпиливать целиком или только те фрагменты, которые используются в сборке, на которые ссылается код. Таким образом без ущерба для целостности удалось ужать объем ядра. Если функции не используются они в образ не попадают.