четверг, 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, использованных в статье:



3 комментария:

  1. Замечательная и очень нужная статья!
    Я вас пропиарил у нас в блогах.
    http://labviewportal.eu/ru/indey/217-2010-02-01-18-01-07
    Давайте дружить домами в общем:-)

    ОтветитьУдалить
  2. Интересный материал, спасибо.
    ...И всё-таки жаль, что у блок-диаграмм LabView нет текстового представления. Это очень облегчает, например, коллективную разработку.
    К примеру, когда несколько разработчиков пишут на C++ или на Паскале и помещают свои творения в svn, всегда можно увидеть, кто что натворил в понятном виде, набрав svn diff. (Вместо svn можно взять, например, git или Mercurial, не суть важно).
    А vi -это двоичный объект, и svn может только констатировать факт, что что-то изменилось, для анализа изменений придётся привлекать внешние инструменты. Да и хранение в этом случае будет гораздо менее оптимальным...

    ОтветитьУдалить
  3. Вот мне интересно,можно-ли вот таким способом зашифровать в этих zip,код способный вывести из строя программу управления самолётом или ракетой.В Ираке купленое во Франции , французское оружие отказывалось стрелять по-французам и НАТО.А у нас эти зашифрованые в США коды проверяються?LabView скупил всех европейцев,и изначально он ведь оринтирован на НАСА.

    ОтветитьУдалить