Переезд на Астра Линукс
Бывает ситуация, когда вот стоит нормальный, не ветхий дом с жильцами, с работающими коммуникациями. И простоял бы он так еще много лет. Но принято решение проложить новую магистраль и дом ей мешает. Можно снести дом, недалеко построить новый и жильцов туда переселить. А можно переместить в нужное место сам этот дом прямо вместе с жильцами и тараканами.
Такая аналогия приходит на ум после принятия решения о переходе с платформы Windows на пока единственную сертифицированную альтернативу – ОС Астра Линукс. Как жильцы гипотетического дома не могли отменить решение о магистрали, так и мы не можем отменить это решение. Придется переходить. Однако легко сказать «перейти». Когда я познакомился с IBM-PC/XT, какой-нибудь Торвальдс еще школу не кончил. За эти годы (чего уж там годы - десятилетия) появилось много привычек, приемов, навыков. Жалко все это оставлять ради принудительного перехода в другую среду. И созрела мысль – попытаться переехать, так сказать, всем домом, а не строить новый.
С одной стороны, а чего сложного-то? Языки программирования были разработаны, в том числе и для возможности работать в разных ОС. Перетащил исходные тексты на каком-нибудь Си в другую ОС и порядок. Так-то оно так, да не так. Если программа общается с ОС только на уровне open/close и read/write, то да. Но, например, в 15 наиболее часто используемых системных библиотеках Windows 10 имеется около девяти с половиной тысяч различных функций. Именно они позволяют создавать, например, сложные графические интерфейсы, вообще использовать всю мощь современных компьютеров. И большая часть этих системных функций никак не отображается в терминах языков программирования, их надо явно вызывать. В другой ОС придется заменить обращения к системным функциям на аналоги. Если, конечно, такие аналоги найдутся.
В нашем случае проблема перехода с Windows на Астра Линукс еще осложняется (или наоборот, упрощается, как посмотреть) тем, что по историческим причинам мы сами сопровождаем свои средства программирования и отладки. И много лет (тьфу, десятилетий) они тоже развивались и их не хочется оставлять вместе с Windows. Хотя используемый нами язык имеет соответствующий американский (ANSI X3.74-1987 (R1998)) и международный (ISO/IEC 6522:1992) стандарты, доступных версий под Линукс не существует. Да если бы и существовали, это все равно были бы чужие, непривычные средства.
А хочется сохранить все свое, привычное. Важная для понимания деталь – мы не занимаемся веб-программированием. Т.е. архитектура нашего ПО это не клиент-сервер (читай посетитель-сайт), а моделирование и отображение результатов моделирования, что, пожалуй, ближе к играм.
Поэтому придуман план поэтапного перехода (переезда всем домом) с минимальными изменениями. Т.е. сначала перенести свои средства программирования и отладки и только уже потом сами программы.
На свой рабочий компьютер с Астрой мы установили программу Wine 3.0, а в нее Far 3.0.5100, сразу получив более-менее привычную среду работы с файлами и возможность перекомпилировать свои средства программирования. На целевом компьютере требования установки программ типа Wine могут быть недопустимыми, да даже и не этично требовать, чтобы пользователи Линукс обязательно тащили себе интерфейс Windows. А вот на свои рабочие компьютеры мы вольны ставить то, что нужно. В любом случае на всех компьютерах самой Windows физически больше нет – юридически все чисто.
К используемому редактору связи добавили небольшой модуль, который из почти готового PE-файла теперь делает ELF-файл. Оба эти формата похожи по типам данных, поэтому перевод несложен.
Таким образом, в окне Wine+Far мы компилируем «старым» (т.е. для Windows) компилятором новый компилятор. Физически и новый компилятор – это все та же 32-х разрядная программа под Windows. Но выход у нее теперь не EXE-файл, а уже ELF-файл. И этот ELF-файл может запускаться безо всяких Wine и Far (которые остаются только на наших рабочих местах), например в консоли «Терминал Fly». На первых порах этот ELF-файл, конечно, выполняться не может, поскольку внутри у него остались вызовы WinAPI, а не UAPI. Постепенно меняя WinAPI на аналоги из UAPI, мы начинаем «оживлять» свои программы (для начала тестовые) в новой для нас среде.
Wine очень хорошо эмулирует 32-х разрядную среду Windows. Поэтому и «старый» и «новый» компиляторы прекрасно работают в таком режиме, и пока нет никакой необходимости пытаться и их самих перетащить в ELF-формат. А самое важное, все имевшиеся компоненты остаются практически неизмененными, добавился только перевод из PE в ELF и замена имен функций и библиотек. Например, был «KERNEL32» и «READFILE», а стал «GLIB_2.2.5» и «READ». Причем имеющийся механизм вызова системных функций в программах тоже сохраняется. Еще, разумеется, меняются загрузки аргументов в регистры при вызове системных функций (т.е. формат ABI), но это уже мелочь. А большая часть компонент наших средств не меняется и как работала – так и продолжает работать.
Могут спросить, а тогда почему вы все свое прикладное ПО просто не запустите под Wine? Ведь тогда и задача перехода будет сразу выполнена. Увы, не все так просто. Во-первых, наше «главное» прикладное ПО 64-х разрядное, поскольку данных в памяти там требуется существенно больше допустимых в 32-х разрядных программах 3 Гбайт. А такие программы Wine пока не очень хорошо выполняет, например, указанное выше ПО просто не запускается. А во-вторых, все время оставаться под Wine – это значит не пользоваться всеми преимуществами другой среды, по сути, так и не перейти в нее. Другое дело – только лишь этап компиляции под Wine. Тем более, что для ОС компилятор - это очень простая программа: читает из файлов и пишет в файл. Это не сложная игрушка, ради которых Wine, собственно, и создавалась. Поэтому я уверен, что даже никогда не потребуется обновлять Wine, поскольку ее будущие возможности нам лишь для выполнения компиляции просто не нужны.
Следующий, может быть неочевидный этап – перенос средств отладки. Дело в том, что много лет (да что это я сегодня, не лет, а десятилетий!) мы используем такую удобную штуку, как встроенный интерактивный отладчик. Это небольшой модуль (26 Кбайт команд и 21 Кбайт данных), который автоматически входит в состав каждого исполняемого файла и основан на обработчике исключений Windows, причем три четверти его объема занимает дисассемблер. Такую возможность важно иметь на целевом компьютере, где какую-нибудь IDE иногда и не поставишь. А этот инструмент всегда наготове. Логи-логами, но ведь всего в протоколах выдачи не предусмотришь. А с помощью этого отладчика можно не только посмотреть любую переменную, но и даже попытаться смоделировать ошибку.
За свою долгую жизнь он сэкономил нам немало часов на поисках ошибок. В Линуксе, конечно, есть «стандартный» отладчик gdb. Но он просто бесит меня своим интерфейсом. Например, при указании адреса памяти нужно не забывать начинать его с 0x, а то услужливый gdb переведет его в никому не нужное десятичное значение и выдаст ерунду. А главное, gdb - это отдельная программа. Требовать запускать на целевом компьютере ПО под gdb так же неэтично, как требовать установки Wine. К сложностям отладчика я еще вернусь ниже.
Перенеся свои средства отладки и став, таким образом, «зрячими», мы в привычной среде отладки начинаем менять вызовы системной библиотеки языка, пока все базовые свойства языка, например, работа с файлами, запрос времени, создание потока и т.п. не начнут действовать в среде Линукс, так же, как они раньше действовали в среде Windows.
И вот только тогда, получив и реализацию языка и средств отладки под Линукс, сохранив возможность редактировать свои исходные тексты и перекомпилировать их в привычной среде (кстати, с возможностью нормального использования кириллицы), можно приступать, наконец, к собственно переносу прикладного ПО, созданного этими средствами, в другую операционную систему. Но это, как говорится, уже совсем другая история.
Объем кодирования уже выполненных работ по переносу, из-за того, что он происходит на самом низком уровне, просто ничтожен. В основном, время тратится на изучение и осмысление. И нужно привыкнуть, что одни возможности у программиста исчезают, а другие появляются. Иногда по смыслу такие же, иногда – совершенно другие. Но в целом, все равно получается перенос в виде марафонского забега. Многолетний опыт работы в одной среде невозможно быстро адаптировать в другой. Кучи грабель еще поджидают нас на этом пути. Но дело движется, дом целиком отъезжает от магистрали, а не стоит на месте под крики «все пропало, шеф, у нас нет ПО под Линукс, а имеющееся невозможно переделать».
На сегодня было встречено лишь две трудности перехода и обе они касаются переноса встроенного отладчика. Казалось бы, чего сложного: в Windows есть обработчик исключений, а в Линуксе – обработчик сигналов. Для возврата в программу они обращаются к функции «установить контекст» (т.е. восстановить состояние процессора). Но у Windows эта функция здорового человека, которая для восстановления ныряет в ядро и оканчивает свои действия машинной инструкцией IRET, автоматически восстанавливающей и флаги процессора. Если нужен пошаговый режим выполнения команд, я устанавливаю «в контексте» (т.е. в памяти) соответствующий бит, а Windows любезно все той же IRET устанавливает соответствующий режим процессора, не выполняя специально для этого никакого кода. И все работает, как нам нужно
Убогая функция «setcontext» в Линуксе не годится для обработчика исключений, поскольку не восстанавливает ни флагов, ни даже регистра RAX. Подразумевается, что в Линуксе вести отладку просто встроенным отладчиком нельзя, а обязательно нужен родитель-трассировщик и потомок-трассируемая нить. Но нам проще и удобнее, когда трассировщик находится прямо внутри трассируемого. Тогда, например, для доступа к любым объектам программы не нужны всякие PEEK/POKE. Пришлось все-таки заводить функцией fork папашу-трассировщика, который следит за своим сынком, но при получении сигнала ничего не делает, а только передает этот сигнал сыну и все. Вместо выхода в программу через «setcontext» я написал в тексте отладчика свой десяток команд, восстанавливающий и флаги командой POPFD. Но так как использовать инструкцию IRET вне ядра я не могу, пришлось оставить выход через просто RET.
В режиме пошагового исполнения эта RET успевает выполниться, и управление переходит в нужное место отлаживаемой программы (например, в текущее). Но это и все, так как немедленно срабатывает сигнал трассировки. Вот тогда находится работа и для папаши-вырожденного трассировщика. Он ловит этот сигнал, но не передает его сыну, а ставит «законный» PTRACE_SINGLESTEP. Теперь одна команда в программе, наконец, выполняется, опять выдается сигнал трассировки, который папаша опять просто сплавляет сынку. В отладчике это выглядит так, как будто пришел сигнал от выполнения очередной команды (на самом деле было два сигнала, первый от команды RET).
Насколько же элегантнее решение в Windows, и, главное, она ничего для этого специально не делает и даже понятия не имеет, что потребовался пошаговый режим. Считаю, что и в Линуксе тоже необходима подобная функция восстановления контекста через ядро и команду IRET, которая сразу сделает необязательным наличие трассировщика именно как родителя.
Вторая трудность не преодолена до сих пор. Речь идет об использовании в отладчике отладочных же регистров DR0-DR7. Для начала я не нашел в заголовочном файле PTRACE.H (из linux-headers 5.15.0-70) обещанных констант PTRACE_GETDBREGS и PTRACE_SETDBREGS, которые и должны были бы обеспечивать работу с этими регистрами.
Вообще говоря, отсутствие нужных именованных констант в принципе не закрывает использование соответствующих возможностей. Например, если бы я вдруг захотел скрыть какую-то возможность, я бы выбросил именованную константу из заголовочного файла, а в исходном тексте ядра подставил бы прямо численное значение. И тогда, чтобы найти эту возможность, пришлось бы анализировать исходный текст ядра, а не небольшой заголовочный файл. Но зачем так делать?
И при этом даже из gdb я так и не смог пока выжать срабатывания ни одной аппаратной точки, хотя он и писал мне строки вроде «Hardware assisted breakpoint 1(-probe 0x401200) pending». Однако никакие «probe» ни на какие адреса на чтение/запись/исполнение никак не реагировали. Возможно, как новичок в Линуксе, я просто что-то неправильно понял.
Аппаратные контрольные точки очень важны для нас в принципе. Они позволяют сэкономить огромное время поиска ошибок просто поймав запись в заданную переменную или позволяя ставить контрольные точки в библиотеках, куда нельзя поставить обычную (не аппаратную) контрольную точку. За десятилетия мы очень к такой возможности привыкли.
И опять, насколько элегантно это сделано в Windows, где отладочные регистры программист может просто менять в памяти «контекста», а ОС эти измененные значения переписывает в ядре в соответствующие регистры DR0-DR7, даже не зная и не заботясь, нужно ли это или нет.
Оптимизм внушает лишь то, что Wine, прекрасно эмулируя среду Win32 (но не Win64), выполняет во встроенном отладчике нашего компилятора и контрольные точки по чтению и контрольные точки по записи. Значит, до отладочных регистров в Линуксе он как-то добирается.
Ниже приведен пример запуска одного из первых перенесенных на Астра Линукс тестов. Задача теста - проверить реагирование встроенного отладчика на специально внесенную ошибку – попытку вычислить логарифм отрицательного числа. Директивой отладчика «Z» выведено состояние FPU в момент ошибки.