четверг, 28 января 2010 г.

§ 3. Интерпретатор или компилятор?

Здравствуйте, коллеги!

Сегодня — несколько слов о внутреннем устройстве файлов LabVIEW, плюс парочка "хакерских" упражнений.

Много лет назад, только начиная работать с LabVIEW, я был абсолютно уверен в том, что LabVIEW — интерпретатор, в чём-то отчасти похожий на Basic. Блок-диаграмма наверняка представлена в виде некоего дерева, которое интерпретируется средой выполнения. В пользу этого также говорило наличие увесистой Run-Time Engine, необходимой для запуска "скомпилированного" приложения, возможность "подсветки" кода при выполнении, наличие файлов VI "как есть" внутри скомпилированного приложения, ну и малая скорость выполнения по сравнению с компиляторами типа С или Delphi (впрочем и сегодня оставляющая желать много лучшего). Однако моя уверенность значительно пошатнулась после прочтения любопытной статьи, в которой был продемонстрирован ассемблерный листинг простенького цикла:


Но даже увидев реальный код, я был уверен, что код этот — результат работы классного интерпретатора, выдернутый из памяти приложения. Я даже попытался сбросить память LabVIEW-программы в дамп и деассемблировать его, да ничего хорошего из этого не вышло.
Желающим повозиться самостоятельно скажу сразу, что пытаться деассемблировать скомпилированную LabVIEW программу "в лоб", равно как и отдельные SubVI смысла не имеет.
Однако как любому ребёнку хочется заглянуть внутрь любимой игрушки, так и мне никак не давал покоя вопрос — как же всё-таки устроен VI и где же код?

Что ж, давайте набросаем простенький код, например тот, который был приведён в статье выше:



и заглянем внутрь обычным hex-просмотрщиком (я использую обыкновенный Far):


среди двоичного мусора можно легко заметить осмысленные четырёхбуквенные теги: LVIN, VIDS, BDHP, FPHP, ICON, CONP и так далее. Нетрудно догадаться что BD и FP - отвечают за блок-диаграмму и переднюю панель соответственно. На самом деле структура файла VI практически идентична ресурсам Макинтоша (более подробно можно прочитать в Википедии: Resource fork). Оно и понятно — ведь самые первые версии LabVIEW выпускались исключительно для Mac, и хранить ресурсы было удобнее в формате самой операционной системы.

Уже морально приготовившись к написанию парсера, я наткнулся на роскошную функцию LabVIEW:REdLoadResFile, с помощью которой можно не только получить список ресурсов из VI, но и вытащить двоичные данные для каждого ресурса:

Кстати, возьмите на заметку — эта функция может вытащить ресурсы не только из *.vi, но и из *.ctl файлов; кроме того её можно вызывать как из LabVIEW, так и из Run-Time Engine.


Пользоваться этой функцией можно примерно так:



(Сниппет не выкладываю ввиду известных багов LabVIEW с кластерами и property node в сниппетах - ну да мы к багам в LabVIEW привычные)

А получится вот что:


Тут мы видим, что в нашем простеньком VI находится порядка трёх десятков ресурсов. Пытливый читатель может попытаться проанализировать двоичные данные самостоятельно, нас же сейчас интересует один единственный ресурс с тегом VICD. Это и есть скомпилированный код. Небольшая тонкость заключается в том, что код (как и некоторые другие ресурсы) упакован алгоритмом zip (сигнатура начала архива хорошо видна после чётвёртого байта).
Давайте извлечём и распакуем его:


ZLIB Inflate взят из OpenG. На вышеприведённой диаграмме код извлекается из VI и сохраняется в *.bin файле.
Ну вот, теперь похоже на некое подобие кода:


Дальше собственно дело техники - нам потребуется подходящий дизассемблер. Кто-то предпочитает HIEW, а мне нравится IDA, тем более что старенькая версия абсолютно бесплатна. Скачать можно вот отсюда: Free IDA Disassembler.

При открытии файла нас спросят про детали - отвечаем что файл двоичный:


Затем вопрос про режим деассемблирования:

Разумеется соглашаемся с предложением 32-битного режима.

Ещё маленькое напоминание о том, что точку входа найти не удалось и её придётся указать вручную:

Вот почти и всё. Когда файл откроется в IDA его надо промотать немного, скажем до адреса 20, и нажать клавишу "С" для запуска анализатора:


После этого получится вот что:


Между адресами 119...16A находится тело цикла. Сравнение и увеличение счётчика показаны на скриншоте. Напомню как выглядел исходный VI:

Нельзя сказать что сгенерированный код оптимален, но в принципе неплохо.

Давайте сделаем маленький эксперимент. Возьмём простенький VI с увеличением счётчика:


LabVIEW генерирует вот такой код:

Всё просто и логично.
Любопытно взглянуть что получится если перейти к типу I64:


А получится вот что:

инкремент заменили на сложение с единицей, кроме того старшие и младшие байты обрабатываются отдельно. Мораль — целочисленный 64 бит тип обычно приводит к замедлению кода и им не надо пользоваться если он явно не нужен.


Ну и наконец двойной инкремент:


LabVIEW накомпилирует следующую конструкцию:




Сравните этот листинг с листингом для одинарного инкремента I32. Хорошо видно, как компилятор работает что называется "в лоб", вставляя совершенно ненужные пересылки из памяти в регистры (три операции mov между инкрементами абсолютно не нужны).

Что можно извлечь из всего написанного?
Прежде всего — LabVIEW действтельно компилятор, причём компилятор совершенно фантастический — ведь генерация кода происходит практически "на лету". Вы нажимаете кнопку Run - и программа немедленно запускается. Процесс компиляции прозрачен и незаметен. Такого, пожалуй, нет ни у одного самого продвинутого компилятора. Мало того, что скомпилированный код также вставляется в исходные файлы (он сохраняется в VI вместе с блок-диаграммой), так он ещё и упаковывается при этом! При запуске приложения код распаковывается "на лету" средствами Run-Time Engine.
Важно также понимать, что сохранив VI без блок-диаграммы мы оставляем в файле только скомпилированный код, который превратить обратно в блок-диаграмму уже невозможно.
Сам по себе скомпилированный код нельзя назвать оптимальным, и именно поэтому в большинстве случаев аналогичный по функциональности участок диаграммы, переписанный на Cи и скомпилированный в DLL будет работать быстрее "нативного" LabVIEW кода.

В заключение - пара VI, использованных в статье:



пятница, 22 января 2010 г.

§ 2. Об автоматической обработке ошибок

В LabVIEW есть пара опций, которые обычно отключают (хотя они включены по умолчанию):

Суть автоматической обработки ошибок заключается в том, что при возникновении ошибки на выходе VI исполнение программы может быть остановлено (за диалог отвечает как раз вторая опция):


В вышеприведённом примере мы создаём новую папку, но такая папка уже существует, что и вызывает ошибку 10 (если у вас нет папки Windows на диске с:\ просто запустите этот пример дважды). Поскольку выход примитива CreateFolder не подсоединён, то возникающая ошибка вызывает прерывание выполнения программы.

В следующем примере это дилоговое окно не появится:


В ряде случаев подобные сообщения мешают. Например в вышеприведённом примере теоретически надо проверять код возврата для того, чтобы выяснить причину ошибки — либо папка уже существует, либо её создание невозможно по каким-либо причинам (например, недопустимая буква диска, либо нет прав на создание, либо недопустимые символы в имени и т.п.). При "первых набросках" или прототипировании программы проще отключить мешающие сообщения и не проверять ошибки типа описанной выше (большинство программистов — оптимисты и считают, что после вызова CreateFolder требуемая папка непременно будет создана если её ещё не существует).

Однако на заключительном этапе имеет смысл включить эту опцию и вычистить участки кода,  вызывающие "оборванные" ошибки (ну или как минимум обратить внимание на такие места).

Важно также не лениться (вы, конечно, можете просто соединить выход с блишайшей границей цикла или последовательности). Если вы не готовы принять решение о логике обработки какой-то конкретной ошибки немедленно, то лучше сделать SubVI со входом ошибки и подосединить его к выходу ошибки в "проблемном" месте. Внутри этого SubVI вы можете организовать запись в лог-файл или в лог отладки (более детально мы это рассмотрим в другом параграфе). Таким образом вы всегда сможете найти места в программе, где проблема пока не устранена (кстати, это будет работать и в исполняемом приложении).

§ 1. Что отличает профессионала от любителя

Запомните простую вещь — профессионал никогда не напишет «Labview» или «LabView» или как-либо ещё.

Единственно верное написание — «LabVIEW». Только так и никак иначе.

Тем не менее, даже если вы пишете «LabVIEW» правильно, то это ещё не делает вас профессионалом.