Здравствуйте. Я хочу рассказать о своей курсовой или к чему приводит любопытство.
Давно от нечего делать пишу программки под симбиан. И время от времени сталкивался со странностями при сборке. Все указывало на утилиту elf2e32. Ее задача — преобразование входного бинарного файла формата elf в другой, специфичный для Symbian — e32 image. Меня долго донимало любопытство — как вообще работает эта утилита и почему порой глючит? Немного позже меня начал донимать другой вопрос — тема курсовой работы =) Решил совместить приятное с полезным и скачал ее исходный код. И понеслось...
Первый коммит — не собирается
Второй — включаем нестандартные расширения gcc, добавляем из исходников отсутствующие классы, функции, константы. Сабж радостно собирается и падает. Прогресс однако. Запускаем под отладчиком — отладчика заносит в класс который лишь инициализирует другой, который иинициализирует следующий… Ура! Настоящая функция! Зааходим. Упс. Где мы?!!! Стоп отладчик! Хирург! Скальпель! Спирт! Огурчик! Классы-аппендиксы ф топку! Даёшь nullptr вместо NULL! У нас С++14! Ух какой устрашающий конструктор инициализующий всё нулями! И ещё, и ещё, и опять — но у нас C++14 призывает инициализацию по умолчанию для классов! Какое всё теперь лаконичное...
Лаадно. Исправляем как можно больше за раз. Разобрался-таки почему отладчик скачет по исходникам аки припадочный — афтары ударились головой об абстракцию, вырастив наследование 80 левела из класса UseCaseBase :) Тогда видимо из глаз и полетели классы-конструкторы статических экземпляров для классов Message & ParameterManager. Синглтон Майерса? Нет, не слышали. Ф топку абстракцию! Viva revolucion!!! Viva POD!!!
Ух! Как интересно это дерево растили. Основную работу делает функция BuildAll(). Если заданы все параметры, функция собирает библиотеку импорта, файл задающий имена функций и переменных и порядок в котором они доступны в библиотеке импорта и собственно сам бинарник. Все потомки UseCaseBase меняли ее алгоритм через перегрузку. Порой в потомках подготавливаем вспомогательные данные, но чаще всего просто выключали создание некоторых файлов. Например не задано имя файла для сборки чего-нибудь — создается новый класс. Идиоты. Достаточно прервать выполнение такой функции-сборщика если надо. Несложно понять мои действия B-)
Продолжаем удалять пустые классы, заменяем NULL на nullptr_t, заменяем range-итераторы на for(auto x: *).
Исправляем ошибки в обработке параметров коммандной строки.
Надо проверить код статическим анализатором. С чего начать? Хм, под XPшкой выбор небольшой — cppcheck, и codeblock поддерживает его из коробки. Ого какой улов! Есть даже delete для char[]! Блин, я знаю куда полгига свободной RAM делось =)
Так добавляем файлы сгенериванные из elf-файла libcrypto.dll и сам файлик описывающий параметры коммандной строки для их создания.
Упс. CPPCheck ошибся… Должно быть (a||b)...
Попробую собрать в Visual Studio 15 да и Win10 потыкать бы палкой. Поставим на виртуальную машину. Сделано, качаем и запускаем онлайн установщик студии. Что? Не хочет сохранять скачку в папку общую с хостом?! Да подавись ты! Качай куда тебя научили… А теперь переносим скаченное в папку и запускаем установку. Что? Опять игнорирует shared folder??! Да подавись ты! Становись куда тебя научили...
В принципе десятка неплохо шебуршит на одном ядре и 3 гигах рамы. Студию в студию! Призадумалось, но не надолго. Открываем мой проект в студиии Опять ругается на папку… Сколько уже можно? Да подавись ты… Собираем, ругается на нестандартное расширения STL hash_set. Удаленное. Удаленное??? Включаем
Ух какой забористый код:
int ElfFileSupplied::UnWantedSymbolp(const char * aSymbol)
{
static hash_set<const char*, hash<const char*>, eqstr> aSymbolSet;
int symbollistsize=sizeof(Unwantedruntimesymbols)/sizeof(Unwantedruntimesymbols[0]);
static bool FLAG=false;
while(!FLAG)
{
for(int i=0;i<symbollistsize;i++)
{
aSymbolSet.insert(Unwantedruntimesymbols[i]);
}
FLAG=true;
}
hash_set<const char*, hash<const char*>, eqstr>::const_iterator it
= aSymbolSet.find(aSymbol);
if(it != aSymbolSet.end())
return 1;
else
return 0;
}
Немного подумаем… И вуаля:
int ElfFileSupplied::UnWantedSymbolp(const char * aSymbol)
{
int symbollistsize = sizeof(Unwantedruntimesymbols) / sizeof(Unwantedruntimesymbols[0]);
for (int i = 0; i<symbollistsize; i++)
{
if (strstr(Unwantedruntimesymbols[i], aSymbol))
return 1;
}
return 0;
}
Моя преелесссстььь...
Так с, почему программа бросает исключение если этот флаг неправильно установлен или не установлен вообще? Зачем ты так жестоко, прекрасное далёко… Давайте-ка будем просто сбрасывать этот флаг на безопасное значение. И этот флаг тоже хорошо бы… И этот, и этот, и эти. А может это лучше вынести в отдельную функцию? Хорошая идея! Назовем её ParameterManager::CheckOptions()!
Шаг влево — падение, шаг вправо — неотловленное исключение, прыжок на месте — спасибо хоть не BSOD =)
Скуучно… Глюки и кривизна...
Оля-ля!!! Эмуляция CleanUpStack Symbian на STL?:
В принципе ничего особого:
std::vector<char*> cleanupStack;
Очистка:
std::vector<char*>::iterator aPos;
char *aPtr;
aPos = cleanupStack.begin();
while( aPos != cleanupStack.end() )
{
aPtr = *aPos;
delete[] aPtr;
++aPos;
}
Какая-то светлая голова вместо left/right иcпользовала l/r. Спасибо cppcheck.
Ай, лениво перед монитором логи cppcheck разбирать… Что гитхаб нам предложит?.. Codacy… Подключаем проект… Немного подумал и готово! Теперь можно читать сообщения об успехах в борьбе с ошибками лежа на диване ^^
Так-с вроде не глючит… Соберем что-нибудь, например libcrypto.dll. Работает, хотя несжатый файл больше на сто байт чем созданный утилитой из SDK. Далее будет постоянно сравниваться бинарники созданные этой версией утилиты и из SDK. Параметры командной строки само собой идентичны.
Такс, где взять аналог diff для бинарных файлов? Хм, наваяю-ка скрипт на пистоне. Слишком много информации — надо что-то сильно проще. Dll для распознавания pdf/djvu — AlternateReaderRecog.dll — хороший вариант, выхлоп менее 4 килобайт. Такс, смещения различаются в import section. Открываем их в hex-редакторе. Начало совпадает, в моей версии дальше мусор, как раз после окончании секции в оригинальной версии. Но в моей версии следующая секция начинается на 100 байт позже. На эту же величину в байтах файлы и различаются! Смещения далее указывают на верные адреса… Бинарник корректен!!! Ааааа!!!
Месяц спустя. Так, откуда взялись эти сто байт?
Что ж, раз непонятно как работает — начинаем ломать алгоритм создания E32Image. Продолжаем издеваться над AlternateReaderRecog.dll. Увеличиваем размер бинарника на выходе — никак, затираем memset секции — никак, уменьшаем размер бинарника — никак. Грррр. Что за?!!! Я ломаю выхлоп в релизной версии, а запускаю отладочную?!!! Привет мочало, начинай сначала… Тааак секция затерлась — хорошо! Увеличили размер бинарника! Хорошо!!! Уменьшим-ка размер секции импорта! Есть!!! Побайтово идентична этой же секции в выхлопе этой утилиты из SDK!
Смотрим в код создания этой секции. "sizeof(char*)" — что-то вспомнились статьи Андрея Карпова, одного из разработчиков Pvs-studio, о том что типы могут занимать разный объём памяти — и сколько же он занимает места? MinGW — 8 байт, Visual Studio — 4!!! Делим пополам эти 8 байт, делов-то. ФФсе! А секция кода как? Эта дллка без глобальных переменных. Нет глобальных переменных — нет и секции… Возьмем что-нибудь потяжелее — libcrypto.dll.
Файл на выходе моей утилиты теперь меньше на 100+ байт… Какого??? Секция импорта побайтово идентичны — хорошо. Секция кода — нет?!!
На глаз сравнивать такую стену текста я не шмогу… Пойду поищу diff для побайтового сравнения…
Через пару дней игры с гуглопоиском всё же нашёл. vbindiff — консольная утилита с интерфейсом аля Norton Commander, показывающая различие между двумя файлами в двух горизонтальных панелях. Для перехода к месту отличия жмем ввод. Хорошо! Можно перетянуть на иконку два файла для сравнения и программа их откроет! Отлично!!!
Сравниваем — тааак в заголовке отличаются его crc и время создания. Ничего такого. Вот байтик отличается, вот ещё сотня… Ого!!! Десятки, сотни, тысячи байт разницы?!!! Таак, смотрим смотрим какой секции они принадлежат… Смотрим смещения… Ага, секция данных…
Проворачиваем трюк, что и для секции импорта… Обнуляем memset'ом, есть. Увеличиваем размер секции… Падает… Увеличиваем. Предлагает руку и сердце отладчика… Блин. Открываем функцию создающую секцию — каша из функций… Грр.
… Ай, завтра… Пока исправлю что-нибудь другое...
Например добавлю тесты, но тут такая каша, что невозможно разделить программу на небольшие модули. Вставлять тесты прямо в код нельзя — потом хрен разберешь. Идея! Постоянные запуски программ с разными аргументами — я же так все время тестировал программу… А давай-ка лучше это всё оформим отдельным скриптом на питоне. Да отличная идея, просто отличная. Скрипт при ошибках выполнения теста должен продолжать работать сообщая о них но не падать. Вот и готово!
Возвращаемся к нашим баранам… Эта функция вызывает эту, затем эту, идём сюда… Так-с, куда меня занесло? Тьфу, запутался…… Ай, завтра… Пока исправлю что-нибудь другое...
И так прошло два месяца...
Блин, где формируется эта секция кода? Пришлось уйти в академический отпуск, так хоть с тобой разберусь!!! Таак. Сюда на вход идут символы для секции… Что покажет printf? В буфер консоли всё не влазит… Сохраним выхлоп в файл… Таак, пока ничего особого… Стоп! Одинаковые строки!!! Много одинаковых строк!!! Откуда?! Добавляем printf на каждый источник данных (терпения хватило на 3 из пяти, ха). Пусто! Смотрим один из оставшихся вызовов функции… Таак. Приращение итератора после цикла??? И TODO на предупреждение codacy??? Переносим в цикл. Запуск!!! Есть совпадение размера! Есть побайтовое совпадение!!! Исправлено!!! git blame имя героя отказывается назвать… Смотрим оригинал — такое не я сотворил. А может это была "бомба" для разработчиков не связанных с Nokia? Гррр.
Тщательно проверяем выхлоп тестов, сверяем побайтово файлы. Все работает как надо! В релиз!
Оляля! Пришло Время Большой Чистки!!! Пора выкорчевать древо UseCaseBase с корнем!!!
Большую часть потомков уже извел, выносим полезные функции в класс-генератор. Остались только UseCaseBase и его потомок ElfFileSupplied. UseCaseBase — представляет собой обертку над классом, обрабатывающим параметры командной и объявляющий несколько чисто виртуальных функций для класса ElfFileSupplied. Короче скрипач не нужен… Какое небо голубое, хорошо… Еще часок… Разберусь с этим классом и можно пойти погулять… И подышать воздухом, погреться, хорошо… Поехали! Так, закомментировать эту функцию. Собираем! Тааак надо подумать как это красиво переделать… Готово!!! Следующая функция! Готово! Следующая! Готово! Готово! Да! Да! Да! Последняя функция… Уффф. Запускаем после сборки… Семикратное ускорение работы?!!! Выхлоп корректный… Забавно. Отладочная версия тоже усохла на 2 метра?!!! Ничего себе!!! Можно и погулять. Ночью?!!! Каак??? Где мой день?!!! Лаадно запущу тесты и отдыхать… Тесты тихо отработали — можно и отдохнуть...
Дай-ка я теперь свое что-нибудь напишу… О, класс работающий с функциями и переменными доступными извне выглядит жутковато. Принцип работы: чтение из файла, разбор строк и сохранение в файл. Под разбор строк выделили аж целый класс отборной лапши на С… Тааак… Подумаеем… Какая красота вышла:
читаем строку std::getline(), удаляем пробелы с краев строк и парсим.
Продолжение следует… Исходный код — https://github.com/fedor4ever/elf2e32
Автор: Леонид Якубович