Кто угодно может пнуть мёртвого льва. Assembler.. Assembler. basic.. Assembler. basic. QuickBasic.. Assembler. basic. QuickBasic. Visual Basic.. Assembler. basic. QuickBasic. Visual Basic. x86.. Assembler. basic. QuickBasic. Visual Basic. x86. интерпретатор.. Assembler. basic. QuickBasic. Visual Basic. x86. интерпретатор. История IT.. Assembler. basic. QuickBasic. Visual Basic. x86. интерпретатор. История IT. Компиляторы.. Assembler. basic. QuickBasic. Visual Basic. x86. интерпретатор. История IT. Компиляторы. Реверс-инжиниринг.. Assembler. basic. QuickBasic. Visual Basic. x86. интерпретатор. История IT. Компиляторы. Реверс-инжиниринг. трансляция.. Assembler. basic. QuickBasic. Visual Basic. x86. интерпретатор. История IT. Компиляторы. Реверс-инжиниринг. трансляция. фейки.

Кто угодно может пнуть мёртвого льва. Мёртвый лев не рыкнет на наглеца. Мёртвый лев не откусит ему ногу «по самое не хочу», хотя стоило бы. Лев мёртв, и теперь его может пнуть каждый ишак, что конечно же не показывает превосходство ишака над львом. Эта статья будет полна негодования и ненависти. Кровь ещё не закончила кипеть от негодования. Но, разумеется, помимо эмоций будут и сухие объективные факты, немножко исследования и расстановка точек над i. В интернете кто-то не прав… опять…

Существует целый ряд инструментов, технологий и вообще вещей, которым по какой-то непонятной вселенской несправедливости не повезло: нашлась масса непонятных людей, которые по какой-то необъяснимой причине начали распускать про эти инструменты/технологии/вещи разные небылицы, идиотские фейки, слухи и прочий порочащий репутацию «компромат». Можно не переживать, если речь идёт о технологии, которая находится «на пике» — у неё будет большое community и правда восторжествует. Совсем другое дело, когда речь идёт о чём-то, что далеко не на пике, чья минута славы в прошлом (возможно даже давно в прошлом) — здесь мёртвый «лев» не может дать сдачи, и что самое обидное, что в какой-то степени «лев» сейчас отчасти потому и мёртв, что ещё при его жизни началось необоснованное распространение всяких бредовых поверий и мифов про него. И сегодня речь пойдёт об одном из таких случаев.

Всё началось со статьи Что было бы, если BASIC развивался вместо C и Python. 06:30 утра, я едва продрал глаза, беру смартфон, чтобы посмотреть, нет ли важных уведомлений, и тут в новостной ленте попадается эта статья. И я просто никогда не могу пройти мимо подобных статей, потому что есть незыблемое правило: если на Хабре появляется статья про какой-нибудь древний Бейсик, то в комментариях к ней обязательно, гарантированно и непременно вспомнят QB и VB, и появятся странные люди со странной мотивацией,которые будут нести свою шаблонную ахинею про то, что QBasic/QuickBasic и (тут особенно обидно) Visual Basic, дескать, недо-инструменты, потому что не умеют в компилирование, а умеют лишь интерпретировать исходный код.

Тут надо сделать ремарку, что у меня особые отношения с Visual Basic. Ещё примерно с 1998-го года был (и есть по сей день) интернет-ресурс VBStreets, который был одним из самых подробных ресурсов и самых больших сообществ, посвящённых VB/VBA/VBScript/ASP и т.п. В былые времена мы проводили конкурсы совместно с Microsoft, мы издавали бумажные книги совместно с BHV и Ozon. И уже много-много лет я являюсь бессменным администратором этого ресурса. Сейчас в силу положения VB ресурс находится скорее в анабиозе, но речь не об этом. Я не просто администратор этого сайта, я в силу этого обстоятельства потратил какое-то умопомрачительное количество времен на исследование внутреннего устройства VB, на реверс-инжиниринг и тому подобные вещи, так что я знаю внутреннее устройство и внутренний мир VB/VBA как никто другой, и должен вам сказать, этот продукт, эта технология таит массу интересных вещей (если повезёт со свободным временем, я расскажу об этом в отдельных хабра-статьях — рассказы обещают быть очень интересными). И таким образом, досконально зная внутреннее устройство, я не могу спокойно проходить мимо какой-то вопиющей ахинени, которую пишут в частности про VB. На QB я конечно тоже когда то (очень) давно понаписал массу кода, однако QB я никогда досконально не исследовал и его внутренний мир я не знаю так хорошо. Тем не менее, по старой памяти и из ностальгических чувств, когда на QB льют лживые помои, я тоже пройти мимо молча не могу.

Так вот, как вы думаете, обошлось ли в этой статье, ссылку на которую я дал выше, точнее в комментариях к ней без бредовых баек про интерпретаторы? Конечно же нет! Никогда без таких комментариев такие статьи не обходятся.

Вот и сейчас хабра-юзер@Kreyне смог пройти мимо и решил прокомментировать имевшееся в исходной статье утверждение:

>>QuickBASIC/Basic Compiler от Microsoft, который переводил код BASIC в исполняемый .EXE

Ничего он не переводил, а просто упаковывал исходник в виде ресурса и прицеплял его к exe интерпретатора

И вот тут у меня возникает вопрос. Несколько вопросов.

  • Какая сила или какая мотивация заставляет людей писать подобные комментарии?

  • Почему когда она приходят с таким утверждением, они не начинают его со слов «Одна бабка сказала» или «Я где-то от каких-то мутных людей слышал, что …» или «На заборе было написано…»

  • Если они считают, что эта информация проистекает из достоверного источника, почему не указывают этот достоверный источник, а если у них эта информация в голове на правах предположения или где-то услышанной байки, почему они не перепроверяют вброс, который собираются опубликовать? Вообще-то хорошим тоном считается отвечать за свои слова и проверять достоверность того, что собираешься сказать.

Знаете что я думаю? Я думаю это отголоски холиваров 35-летней давности. Был, допустим, холивар (религиозная война) между сторонниками QuickBasic и Turbo Pascal примерно 35 лет назад. И поскольку это религиозная война, а не аргументированный спор, то ни с одной стороны ни с другой не было знаний и действительной аргументации, а была только слепая вера в превосходство своей любимой игрушки. Нужно было просто сделать вброс, направленный на противную технологию, и чем громче и унизительнее он был звучал, тем лучше. Всё равно никто ничего не будет проверять и исследовать, ведь на то это и религиозная война. «— Я верую, что QuickBasic интерпретируемая какашка, и мне плевать, как там на самом деле!». Холивар давно угас, но отдельные кричалки и посылы словно информационные вирусы путешествуют до сих пор.

Спойлер для самых нетерпеливых

Ничего он [QuickBASIC] не переводил, а просто упаковывал исходник в виде ресурса и прицеплял его к exe интерпретатора

Достоверность этого утверждения — 0%. Это чушь, миф, байка. Дальше будет очень короткий, поверхностный, но действующий способ проверить, что это не правда.

Так вот: QuickBasic действительно умел порождать на выходе EXE-файлы. Которые могли работать отдельно и самостоятельно от IDE. И очень обидно будет/было бы за QuickBasic, если бы он под видом генерации самостоятельного EXE просто порождал бы копию программы-интерпретатора, в ресурсы которого засовывал исходник программы. Кстати, во времена 16-битных EXE-файлов реального режима не было понятие ресурсов, было понятие оверлеев. Это просто выглядит как какой-то обман, надувательство.

И вот что удивительно: я никогда на самом деле не копал внутрь QuickBasic. Я не смотрел и не проверял, что там содержится внутри сгенерированного (скомпилированного) EXE-файла. Вдруг там реально зашит исходник, который просто интерпретируется? Нет! Всегда хотелось считать и, даже стоит сказать иначе, не просто хотелось считать, а было логичным считать, всё всё устроено не так — что внутри EXE-именно машинный код нашей бейсик-программы, а не просто интерпретатор в связке с пришитым к нему исходником.

Давайте включим логику: это сейчас безумные времена, когда в порядке вещей на веб-странице иметь JS-скрипты, которые являются реализацией интерпретатора какого-то другого языка. В те годы интерпретатор был слишком дорогим удовольствием. Он жрёт много памяти. Он требует тактов на своё исполнение. Кто хоть раз писал интерптератор чего либо, знает, что это просто огромное дерево ветвлений и каскады if-ов для проверки всех возможных вариаций синтаксических конструкций на предмет отклонения. Нужно обработать все возможные отклонения от правильного синтаксиса интерпретируемого вами языка и выдать что-то вразумительное в случае ошибки (вы же не будете выдавать Syntax error на всё подряд?)

В таком случае интерпретатор был бы довольно массивным, и, как ни крути, он был бы обязан включать в себя хотя бы «текстовки» ошибок (сообщений об ошибках) для всех возможных вариантов нарушения синтаксиса. И даже за счёт одних только этих текстов сообщений об ошибок он уже получился бы прилично раздутым. А теперь представьте, что кто-то хочет написать 10 абсолютно простых, миниатюрных программок на QB. Тогда получается, что каждый EXE-файл содержал бы вшитую в него логику интерпретирования и ещё пачки строковых последовательностей с сообщениями об ошибках? Выглядит не очень логичным, но умозрительная рациональность или логичность какого-то подхода так себе аргумент «за» или «против» того, насколько такой подход соотносится с реальностью.

Поэтому давайте представим, что я тот самый человек, который хочет сделать вброс и заявить, что скомпилированная QB-программа на самом деле не скомпилированная, а просто склейка заранее заготовленного интерпретатора и исходного кода, который нужно интерпретировать. Или наоборот, я хочу сделать вброс, опровергающий такое общеустоявшееся мнение. Как бы там ни было, прежде чем делать вброс, я бы проверил свои тезисы, чтобы не сесть в лужу.

Я бы запустил QuickBasic и написал простенькую программу в духе Hello world:

Процедура HELLO просто выводит строку «I love number {x}.» затем проигрывает аккорд ля-мажор, затем дописывает завершающее «Number {x} is very nice!» на такой же строке, делает задержку на 1 секунду и на этом всё. В основном теле программы у нас цикл for, только вместо канонического i у нас переменная MyCoolrVariableI. Таким образом мы признаёмся в любви ко всем целым числам от 1 до 13.

Процедура HELLO просто выводит строку «I love number {x}.» затем проигрывает аккорд ля-мажор, затем дописывает завершающее «Number {x} is very nice!» на такой же строке, делает задержку на 1 секунду и на этом всё. В основном теле программы у нас цикл for, только вместо канонического i у нас переменная MyCoolrVariableI. Таким образом мы признаёмся в любви ко всем целым числам от 1 до 13.

Хотя это не важно в данном случае, выглядит результат работы этой программы вот так:

Только при этом ещё и происходят спецэффекты в виде мажорных трезвучий, доносящихся из PC-Speaker'а.

Только при этом ещё и происходят спецэффекты в виде мажорных трезвучий, доносящихся из PC-Speaker’а.

Но нам интересно вовсе не это, нам интересно скомпилировать эту программу в EXE-файл:

Меню с соответствующим пунктом для создания EXE-файла

Меню с соответствующим пунктом для создания EXE-файла
Диалог, запрашивающий у нас параметры компиляции

Диалог, запрашивающий у нас параметры компиляции

Обратите внимание, что здесь нам предлагают выбор: породить на свет EXE-файл, нуждающийся во внешнем BRUN45.EXE, или породить полностью самостоятельный или независимый EXE-файл. Давайте подыграем распространителям фейков и поверим в то, что тот самый BRUN45.EXE и есть интерпретатор, и нам предлагают либо вшить интерпретатор в сам итоговый EXE-файл, либо оставить в выходном EXE-файле маленький кусочек со ссылкой на и подгрузкой внешнего интерпретатор. Выберем вариант с зависимостью от внешнего BRUN45.EXE — в таком случае в нашем EXE-файле должен якобы остаться только исходный код программы и небольшой кусочек машинного кода, подгружающий интерпретатор из внешнего файла.

Компилируем! А теперь берём hex-редактор HIEW и смотрим содержимое только что сгенерированного EXE-файла. Прокрутимся в самый конец, ведь именно там должен быть исходный код, бережливо засунутый туда компилятором для последующей интерпретации в момент запуска:

Конец содержимого EXE-файла

Конец содержимого EXE-файла

Упс! Где же чёртов исходный код? Где же так милые сердцу ключевые слова DECLARE, SUB, END, FOR? Где же SOUND и PRINT? Где наш милый исходный код? Кажется, им тут и не пахнет! Может он не в конце, а в начале?

Начало содержимого EXE-файла

Начало содержимого EXE-файла

Но и в начале его нет! Ни в каком месте полученного EXE-файла исходного кода на языке QuickBasic не наблюдается и нет вообще.

Его здесь нет ни целиком. Ни в виде отдельны процедурх. Ни в виде отдельный statement-ов. Может хотя бы идентификаторы из нашего кода найдутся? Поищем-ка идентификатор HELLO и идентификатор MyCoolVariableI (именно для этого там в цикле не просто каноническое «i», а переменная со столь длинным именем):

Ищем подстроку «HELLO», ищем подстроку «MyCoolVariable»...

Ищем подстроку «HELLO», ищем подстроку «MyCoolVariable»…

Но никаких следов идентификаторов «HELLO» и «MyCoolVariableI» в содержимом EXE-файла не находится даже близко:

«Здесь рыбы нет!» (с)

«Здесь рыбы нет!» (с)

Как же так? Ведь нас уверяют, что в EXE-файл просто тупо вшивается исходник, а при запуске EXE-файла встроенный (или не встроенный, а лежащий рядышком?) интерпретатор начинает его интерпретировать? Но на поверку оказывается, что в EXE-файле не обнаруживается исходный код ни в каком виде. Не то, что даже в виде отдельных строк, а даже отдельно взятые идентификаторы в EXE-файл не попадают.

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

Ничего он не переводил, а просто упаковывал исходник в виде ресурса и прицеплял его к exe интерпретатора

Здесь не сказано, что компилятор просто копировал исходный код в EXE-файл как есть, а сказано, что он упаковывал его. Наверное имеется в виду сжатие каким-нибудь PKZIP или LZW. Ведь это конец 80-х, и нам не на что больше тратить драгоценные такты CPU, кроме как на сжатия и разжатие исходного кода. <sarcasm>Тогда абсолютно логично, почему в содержимом файла мы не видим зашитого исходника — он сжат алгоритмом сжатия!</sarcasm>

Тебе не придётся наблюдать внутри EXE-файла исходный код, если упаковать его упаковщиком!

Тебе не придётся наблюдать внутри EXE-файла исходный код, если упаковать его упаковщиком!

Но подождите. Если сжать исходный код упаковщиком, пройтись по нему каким-то алгоритмом сжатия, тогда от исходного текста действительно не останется и следа. А я, кажется, вижу в содержимом EXE-файла признаки человеческой речи:

Строковые литералы из исходного кода всё-таки попали в содержимое EXE-файла в неизменном виде. Версия со сжатием отменяется.

Строковые литералы из исходного кода всё-таки попали в содержимое EXE-файла в неизменном виде. Версия со сжатием отменяется.

Так что нет, версия, что исходный код при компиляции сжимается, а при запуска готово EXE-файла распаковывается в первозданный вид и передаётся интерпретатору — не оправдывается.

Но подождите вновь! В современном сумасшедшем мире давно есть такая вещь как «минификация»: фронтендеры скармливают свои JS-файлы минификаторам, которые удаляют все пробельные символы, вырезают комментаторы, заменяют идентификаторы на однобуквенные (или имеющие минимальное достаточно симло букв), но строковые литералы при этом остаются как есть. Может и здесь что-то такое же происходит? Может создатели QB пошли дальше и все ключевые слова и идентификаторы заменили на бинарное представление, а строки остались? Может именно это имелось в виду под упаковкой?

А что если мы пойдём ва-банк? Для этого мы немного изменим наш тестовый код:

  1. Мы модифицируем цикл FOR так, чтобы он пробегал по числам не от 1 до 13 (с дефолтным шагом 1), а от 0xBEEF до 0xCAFE с шагом 0x0123.

  2. Посколько идентификатор «MyCoolVariableI» всё равно в итоговом EXE-файле не обнаруживается, мы заменим его на каноническое «i».

  3. А ещё мы добавим процедуру EatMarker, принимающую 32-битное число и выводящую его:
    SUB EatMarker (xxx AS LONG)
    PRINT "PASSED MARKER:"; xxx
    END SUB

    И вызовем её из основного тела программы, передавая примечательное число 0xDEAD4FEE (мёртвый за плату).

В итоге мы получаем вот такую маленькую программку:

Кто угодно может пнуть мёртвого льва - 11

Смысл использования примечательных чисел (BEEF, CAFE, 0123, DEAD4FEE) в возможности поискать их в хекс-редакторе и посмотреть, как они вплетены в окружающие их бинарные данные. Компилируем этот код и опять открываем его в hex-редакторе HIEW.

Мы зайдём с конца. С числа 0xDEAD4FEE. Это 32-битное число, а между тем, QB генерировал 16-битный машинный код для 16-битного реального режима работы процессора. Если компилятор генерирует не машинный код, а какое-то промежуточное его представление, которое затем интерпретируется (насколько вообще может быть применён термин интерпретация к сильно переваренному коду, значительно отличающемуся от исходного) — то мы видим в содержимом EXE-файла это 32-битное число как есть, с той лишь оговоркой, что это будет little-endian представление, то есть байты EE 4F AD DE. Если же я прав, и компилятор компилирует самый обыкновенный машинный код (как делал бы это компилятор C++), то мы увидим упаковку числа 0xDEAD4FEE в стек за два приёма (поскольку режим работы процессора — 16-битный).

Итак, компиируем, открываем в HIEW и пытаемся найти следы 0xDEAD4FEE:

Кто угодно может пнуть мёртвого льва - 12

И находим! Но находим не как 4 смежных байта, а сначала младшую часть (0x4FEE => байты EE 4F), а затем спустя 4 байта и старшую часть (0xDEADxxxx => байты AD DE). Давайте-ка не будем тянуть интригу, нажмём F4 (Mode) и выберем режим Disasm.

У машинного кода x86 нет битов самосинхронизации (в отличие от UTF-8, например), поэтому вывод дизассемблера зависит от того, откуда будем начинать. Вообще-то я ожидал, что там будет использовать инструкция push, но реальность оказалось чуть иначе:

Кто угодно может пнуть мёртвого льва - 13

Что же мы тут такое видим? А мы видим, что эти байты EE 4F и AD DE является частью обычного, рядового, простого и привычного машинного кода x86! Никакой это не исходный код. Ни в сыром виде. Ни в упакованном/сжатом виде. Ни в минифицированном виде. Ни в промежуточном представлении.

Это самый настоящий машинный код, исполняемый код x86:

mov word ds:[0dc8], 4FEEh ; помещаем мл. часть числового литерала во вр. перем.
mov word ds:[0dca], DEADh ; помещаем ст. часть числового литерала во вр. перем.
mov ax, 0dc8              ; пушаем адрес временной переменной
push ax                   ; пушаем адрес временной переменной
call 0000:0109            ; и вызываем процедуру EatMarker

Я чуть-чуть ошибся, ожидая увидеть два push-а, а разгадка проста: на самом деле в QuickBasic аргументы процедур/функций всегда передаются ByRef (по ссылке), а не по значению (ByVal), то есть на физическом уровне передаётся не значение переменной, а адрес этой переменной, указатель на неё. Если же на уровне исходного кода в процедуру передаётся не переменная, а непосредственное значение (числовой литера), то создаётся временная переменная, куда сохраняется числовой литерал, и указатель на эту временную невидимую переменную передаётся в вызываемую процедуру.

Но может это только для вызова процедур генерируется настоящий машинный код? А для control structures типа ветвлений и циклов используется интерпретация? Вспомним про наш FOR-цикл и вернёмся к нему: у нас там были примечательные числа 0xBEEF, 0xCADE, 0x0123. Поищем-ка их.

И конечно же мы их найдём. Далеко идти не надо, достаточно чуть-чуть прокрутиться наверх:

Кто угодно может пнуть мёртвого льва - 14

На этом экране в этом дизасм-листинге виден весь код основного тела нашей QB-программы. Видно, что и для цикла FOR тоже используется машинный код. У нас цикл FOR от значения 0xBEEF до 0xCAFE с шагом 0x0123. И мы здесь видим, что переменная «i» у нас живёт по адресу DS:[0DC6].

В начале цикла FOR инициализируется начальное значение: mov ax, 0BEEF. Сразу же идёт джамп на код проверки условия (продолжать ли цикл) — значение переменной i должно быть не больше 0xCAFE. И действительно, переменная i сравнивается с 0xCAFE (cmp ax, 0CAFE). Если условие выполняется, идёт условный джамп (jle) на тело for-цикла, если нет — выполнение переходит к следующей за циклом FOR инструкции. В теле цикла у нас в исходном коде строка «HELLO i%». Переменная i% передаётся в процедуру HELLO — и в машинном коде мы это видим (вместо значения переменной i% на стек кладётся её адрес), после чего i% инкрементируется на 0x123. За циклом мы видим вызов PRINT "We are done with..., а вслед за ним — вызов процедуры EatMarker, куда передаётся число DEAD4FEE (через временную переменную).

То есть буквально вот этот исходный код:

Кто угодно может пнуть мёртвого льва - 15

Превратился в 73 байта машинного кода. Исполняемого кода x86-процессора. Можно прямо поставить группы машинных команд строкам исходного кода:

Кто угодно может пнуть мёртвого льва - 16

В общем, что мы здесь видим? Исходный код на языке QuckBasic скомпиловался сразу и непосредственно в машинный код, в инструкции x86-процессора для 16-битного реального режима. В такие же инструкции и в такой же код, в каком бы скомпилировался for-цикл, будь он написан на Си. Никакой упаковки исходного кода здесь нет, никакой интерпретации кода здесь нет — интерпретировать эти инструкции будет процессор, его декодер команд.

А теперь вспомним изначальную цитату:

Ничего он не переводил, а просто упаковывал исходник в виде ресурса и прицеплял его к exe интерпретатора

Немая пауза… Мы открыли исполняемый файл и нашли там, чёрт его возьми, исполняемый код! Кто бы мог подумать, что внутри исполняемого файла будет исполняемый код? Никогда такого не было и вот опять… И обратите внимание, что вместо использования какого-нибудь отладчика я специально выбрал простейший hex-редактор HIEW, чтобы ни у кого не было соблазна сказать, что найденный код и найденные инструкции образовались в памяти процесса в результате распаковки/интерпретации/компиляции кода в процессе запуска EXE. Я показываю то, что уже есть в EXE-файле, не допуская никаких самораспаковок.

И это далеко не единственный комментарии такого толка в той статье. Вот «прекрасный» спор между @checkpointи @PerroSalchicha:

@checkpointпишет:

Каким бы хорошим не был Basic, это всё равно интерпретируемый язык. Взрослые парни пишут на компилируемых языках. В этом смысле все питонисты – дети. ;)

На что @PerroSalchichaсправедливо возмущается:

Бейсик стал компилируемым ещё в 1980-х, с появлением Турбо Бейсика :)

Но @checkpoint не успокаивается:

AFAIK, он не был компилируемым как таковым, просто среда упаковывала интерпретатор с кодом в один .EXE файл. Нормальных компиляторов с “Васика” я не встречал.

В этот момент воин света @PerroSalchichaпереходит на сторону тьму и тоже начинает писать ахинею:

По-моему, TB был как раз честным компилятором. Исполняющую среду с кодом в один EXE паковал Visual Basic

И @checkpointподытоживает:

Скорее всего Вы правы, мне на глаза попадались только QBASIC и Visual Basic. Оба умели генерировать .EXE, но не настоящий. :)

Изумительно…

—Я не потерплю ложь, а тем более ложь в письменной форме!!!

—Я не потерплю ложь, а тем более ложь в письменной форме!!!

С тем, что QuickBasic всё-таки умеет генерировать .EXE и при это настоящий .EXE, самый настоящий, более настоящего не придумать — мы вроде бы разобрались. Но QuickBasic никогда меня особо не интересовал. Совсем другое дело — Visual Basic — внутреннюю кухню которого я изучил вдоль и поперёк.

Эти дурацкие байки,я клянусь, всплывают в абсолютно любой статье на Хабре, где хоть как-то затрагиваются всевозможные бейсикоподобные языки. Эта паста постонно пережёвывается кем-нибудь и передаётся из уст в уста.

Мне хочется воззвать хотя бы к логике людей, которые тиражируют подобные байки. Если люди в Microsoft в 80-х годах сумели сделать и осилили такую вещь как генерацию настоящих полноценных EXE-файлов с машинным кодом в таком в общем-то несерьёзном и игрушечном продукте, как QuickBasic, неужели вы хоть на минуту допускаете, что в таком монструозном продукте как Visual Basic кто-то сделал бы такую дичь как засовывание в EXE-файл исходника в склейке с интерпретатором? Разве хоть немного правдоподобным это выглядит?

Чтобы понимать, что за зверь такой VB, нужно понимать, как он родился и из каких проектов у растут ноги. Здесь всё не так просто. Есть два пути разобраться в этом вопросе: короткий и более углубленный. Предполагая, что у вас не так много времени, расскажу об этом коротко, а затем, для тех, кто хочет узнать поглубже, дам несколько ссылок на свои переводы и статьи, проливающие свет на этот вопрос. Так вот, если говорить коротко:

  • На рубеже десятилетий (80-е/90-е) в Microsoft существовало такое подразделение, как DABU — Data Applications Business Unit. В ведении DABU тогда находилась разработка СУБД под название Omega. Кроме того, именно DABU занималась разработкой и поддержкой QuickBasic. В составе СУБД Omega должен был быть бейсикоподобный язык, и он тогда назывался EB, что расшифровывалось как Embedded Basic. Речь не идёт о бейсике для электроники типа Raspberry Pi (если бы оно в то время существовало), речь идёт о возможности встраивать что-то бейсикоподобное в своим приложения. Не трудно догадаться, что проект СУБД Omega со своим EB развился потом в MS Access/JET/MS SQL. Помимо этого, подразделению DABU было поручена разработка инновационной среды для объектно-ориентированной разработки приложений под Windows с бейсико-подобным языком программирования под кодовым названием Silver.

  • В то же время небезызвестный Joel Sploslky в одной из своих статей рассказывал, как в своё время Билл Гейтс решил, что в Excel должен был встроенный язык программирование, и что это должно быть нечто бейсикоподобное. Именно Джоэлю было поручено написать спецификацию для будущего языка программирования, который на начальном этапе тоже назывался EB — только команда расшифровывала это как Excel Basic, то есть бейсик для Excel. Джоэль, в частности, гордится, что принёс в спецификацию 4 инновационных момента: новый тип переменных Variant, который мог бы хранить что угодно (потому что ячейка Excel может хранить что угодно), поддержку позднего связывания наряду с ранним связыванием в ООП-вызовах, конструкцию For Each, позаимствованную из csh и конструкцию With…End With, позаимствованную из Паскаля.

  • Одновременно с этим в те же года некто Алан Купер — IT-предприниматель — разработал концепцию инструмента под названием Ruby (никакого отношения это не имеет к языку программирования Ruby). Ruby у купера — концепция (и не только концепция, но и рабочий прототип) менеджера рабочего стола, где пользователю даровали возможность самому себе в режиме конструктора делать то окружение, которое ему будет удобно.В Ruby была концепция так называемых «штуковин» (gizmos), эти штуковины можно было легко рисовать в любом месте на desk-ах, связывать друг с другом, привязывать к разным событиям и свойствам штуковин различные действия, например, при щелчке на пункт в списке могла запускаться какая-нибудь команда ОС/программа. В Ruby не было никакого своего языка программирования: ни бейсикоподобного, ни Си-подобного, ни какого либо вообще. Алану Куперу удалось выгодно продать эту концепцию и свой рабочий прототип Биллу Гейтсу в Microsoft.

  • Таким образом, сначала EB из проекта Omega попал и начал свою жизнь в рамках проекта Silver (IDE для Windows с ООП и бейсикоподобным языком). Потом проект Silver стал частью проекта Excel Basic (снова EB). Далее этот EB развился в то, что теперь известно как VBA.

  • Не отказываясь от первоначальной идеи проекта Silver (бейсикоподобная IDE с ООП для разработки Windows-приложений), Microsoft решило подружить проект EB с проектом Ruby — результат слияния и сращения двух изначально независимых продуктов сперва назывался Thunder, но позже отдел маркетинга решил иначе: EB стал называться Visual Basic for Applications, а Thunder, который представлял собой результат слияния EB и Ruby, стал называться просто Visual Basic.

Для тех, кто хочет детальнее изучить эти аспекты истории становления продуктов Microsoft, информация под спойлером:

Расширенная информация

Я давно написал статью о слиянии EB и Ruby, но прежде чем приступать к этой статье, рекомендуется подготовить свой мозг и прочитать две других, которые являются переводами статьей, написанных непосредственно теми людьми, кто работал в Microsoft и работал над Silver/Omega/EB/VBA/Ruby/VB. Читать рекомендую именно в таком порядке:

  1. Почему меня называются «отцом Visual Basic’а — перевод одноимённой статьи Алана Купера, придумавшего систему Ruby.

  2. Thunder… рождение Visual Basic — перевод статьи Скотта Фергюсона, человека из DABU, кто имел отношение к разработке Silver/Thunder/EB. Это буквально взгляд на VB с другой стороны тоннеля, потому что к уже имеющемуся ядру бейсикоподобного языка поставили задачу присоединить чужеродную систему — Ruby.

  3. Ruby + EB = Visual Basic — моя статья, полагающаяся на материалы предыдущих двух, а также статьи Joel Spolsky и собственную аналитику и результаты глубоких исследований внутреннего устройства VB/VBA.

В итоге получается, что генеалогическое дерево VB очень сложное и переплетённое, и вообще, скорее даже не дерево, а просто граф — VB является побочным продуктом слияния VBA (EB) с другой независимой и купленной ранее у Алана Купера концепцией/идеей/технологией — Ruby.

А теперь важные вещи, касающиеся VBA/EB и VB в плане генерации кода:

  • EB/VBA никогда исторически не был интерпретируемым. Собственно, даже QuickBasic не интерпретирует исходный код в момент запуска программы под отладчиком — код интерпретируется в момент его вводы в редакторе кода и в дальнейшем внутри QB представлен не как код, а как нечто более абстрактное и предобработанное (но не как AST). EB унаследовал эту концепцию, и тоже не интерпретировал код (как это делал, скажем VBScript).

  • Поскольку EB должен был быть кроссплатформенным, поскольку, Excel, например, поставлялся так же и под Mac, а под Mac была своя аппаратная архитектура, то EB исторически никогда не компилировался в машинный код. Вместо этого авторы EB разработали свою виртуальную машину (почти как в Java) со своей собственной системой высокоуровневых команд. Байт-код для этой виртуальной машиной назывался P-код. Весь код, написанный на EB/VBA, в конечном счёте компилировался в P-код и в рабочем режиме (а также в режиме отладки) этот P-код исполнялся собственной P-кодной виртуальной машиной. Это был единственный и основной режим компиляции EB/VBA, что, в общем-то и логично. Реализация же самой виртуальной машины на разных аппаратных архитектурах и под разными ОС могла быть совершенно разной, а вот система команд была одной и той же, что теоретически означало бы, что единожды скомпилированный VB-код в P-код мог выполняться без перекомпиляции под сильно разными платформами. Отдельно стоит сказать, что EB/VBA не перекомпилирует P-код процедур и методов классов (и других объектных сущностей) всякий раз при запуске. Код компилируется по принципу JIT (если каких-то процедур не коснулись — они вообще не компилируется), и один раз скомпилировавшись, продолжает существовать между запусками, если процедуры не модифицировались. Более того, этот подход с P-кодом позволяет VBA давать разработчику уникальную возможность: поставив выполнение кода на паузу и словив его на паузу брекпоинтом или выполняя пошаговое выполнение, разработчик может очень существенно по живому переписывать код, менять строки кода местами, удалять что-то добавлять, дописывать, редактировать выражения. И это всё не требует перекомпиляции проекта, перезапуска, за исключением редких случаев (когда, например, был объявлен массив одного типа, а после правки он остался массивом, но уже другого типа, либо поменялась размерность).

  • Однако то, что было хорошо для EB/VBA, для Thunder/VB могло оказаться не самым оптимальным. Поэтому в VB, который умел (в отличие от VBA) порождать самостоятельные независимые EXE-файлы (включим сюда и DLL/OCX и т.п.), сделали целых два режима компиляции кода проекта.

Первый механизм, используемый VB — это компиляция кода во всё тот же P-код, как в VBA. Все процедуры компилируются в P-код и помещаются в EXE-файл вместе во вспомогательными структурами данных, чем-то похожими на RTTI в C++. Виртуальная же машина, исполняющая этот P-код, жила в отдельном файле, например, для Visual Basic 6 этот файл назывался MSVBVM60.DLL, Весь этот принцип исполнения P-кода на виртуальной машине был устроен так, что в любой момент из P-кода можно было вызвать Native-код процедуры (например системное API или какие-то функции из сторонних прикладных библиотек), а с другой стороны из любой native-кодной среды (например сишного кода) можно было вызвать процедуру, реализованную как P-код — для этого EB генерирует для такой процедуры крохотный переходничок, который передаёт управление виртуальной машине, заставляя её выполнять P-код процедуре, а затем выполнить возврат обратно в переходничок и в вызывающую сторону.

Второй механизм это то, чем не может похвастаться VBA (EB), но VBA ( = Thunder = EB+Ruby) похвастаться может — это генерация из VB-кода полноценного EXE-файла с трансляцией кода в машинный код x86, исполняемый непосредственно процессором. Без каких-либо интерпретации даже при очень поверхностном взгляде.

В обоих случаях никакой интерпретацией не пахнет. В одном случае виртуальная машина исполняет свой проприетарный байт-код, ровно так же, как это делает Java. В другом случае вообще генерируется машинный код, который исполняется процессором, как будто скомпилировали программу на Си или Си++.

Выбор, какой режим компиляции использовать, лежал на программисте. Оба варианта имели свои преимущества и недостатки. По умолчанию действовал вариант с генерацией машинного кода. Вариант с генерацией P-кода давал очень компактный код, потому что P-код инструкции были весьма высокоуровневыми: одна P-code инструкция могла делать то, что делает 50 машинных инструкций процессора. Зачастую P-кодный вариант был медленнее, чем исполняемый файл, сгенерированный в Native-код. Но не всегда: если куски машинного кода, составляющие реализацию виртуальной машины, удачно попадают в кеш инструкций процессора, выполнение P-code варианта могло (и может) наоборот обогнать Native-код.

Ну и по аналогии аналогии с QuickBasic, давайте напишем какой-нибудь бессмысленный в реальной жизни код на Visual Basic, скомпилируем и посмотрим под дизаессемблером, что же там генерируется. Но на этот раз мы не просто будем смотреть на результат компилции VB-кода, а рядом напишем аналогичный код на C++ и сравним оба варианта.

В качестве демонстрации напишем две процедуру/функции:

  • Первая — GetMinMax — принимает два числа (a и b) и возвращает минимальное из них и максимальное из них. При этом, если так оказалось, что a>b — вызывается WinAPI-функция MessageBeep с параметром MB_OK.

  • Вторая — Fact — принимает число n и подсчитывает факториал (n!) самым наивным образом, не заботясь о переполнениях при больших n.

Использовать будем два продукта одного года выпуска: Visual Basic 6 и Visual C++ 6 (более нового VB просто не существует):

Кто угодно может пнуть мёртвого льва - 18

Компилируем VB-код. Оставляем активной опцию «Compile to Native Code», которая и так выбрана по умолчанию для всех новых проектов. Compile to Native Code означает компиляцию в машинный код x86, который выполняется на виртуальной машиной, а непосредственно процессором. Ставим галочку Create Symbolic Debug Info — чтобы потом легко можно было найти наши процедуры в дизасм-листинге. Компилируем и получаем EXE-файл.

Опции компиляции VB-проекта

Опции компиляции VB-проекта

Теперь компилируемый сишный код:сишный код компилируем bat-ником со следующим содержимым:

cl test.cpp /O2 /link /dll /noentry  /debugtype:coff user32.lib 
pause

Здесь мы передаёт ключ /O2 компилятору, потому что это соответствует VB-шной оптимизации «Optimize for Fast Code» (см. скриншот выше), линкеру же мы передаём ключи /dll и /noentry (чтобы не писать функцию main ни в каком виде, ибо она в данном эксперименте не нужна), а также /debugtype:coff, чтобы нужные функции были как-то подписаны в дизассемблере.

И теперь сравниваем результат дизассемблирования обоих бинарных файлов:

Кто угодно может пнуть мёртвого льва - 20

Что мы здесь видим? Главным образом мы видим то, что внутри EXE-файла, порождённого на свет силами VB, содержится, чёрт его возьми, машинный код. Не исходный код, как говорят нам бредовые байки, не какое-то там промежуточное представление, а нормальный 32-битный машинный код.

Во вторую очередь мы видим, что на уровне машинного кода результат компиляции GetMinAndMax вообще абсолютно идентичен для VB6 и MSVC++6. И это в то же самое время, когда кто-то пишет глупость в духе:

Исполняющую среду с кодом в один EXE паковал Visual Basic

Оба умели генерировать .EXE, но не настоящий. :)

Я просто ума не приложу, чем EXE-файл, генерируемый силами VB, не является настоящим, если он, зараза, байт-в-байт, инструкция-в-инструкцию идентичен результату компиляции эквивалентного кода, написанного на C++? Но, конечно, я знаю ответ: авторам не интересно докапываться до истины или хотя бы проверять правомочность своих слов. Куда прикольнее просто пнуть тушу мёртвого льва, ведь лев уже не даст сдачи.

В случае с функцией Fact() варианты, которые выдали компиляторы VB и C++ отличаются: VB-шный выхлоп более многословен. Но это ни в коем случае не означает, что VB-шный компилятор в каком-то смысле менее полноценный или оптимальный.

Просто VB — это не Си (и не C++), а C++ — не VB. В Си язык никогда не будет сам заботиться о том, что у вас может произойти переполнение при выполнении целочисленной арифметики. VB — совсем другое дело. VB гарантирует вам, что случайное целочисленное переполнение не останется назамеченным: на каждую операцию, потенциально грозящую переполнением, VB сгенерирует код, выполняющий проверку, не произошло ли оно, и выбрасывающий ошибку (путём выкидывания SEH-исключения), позволяющую программисту отреагировать каким-либо образом.

Таким образом, в случае функции Fact() выхлоп от VB получился длиннее просто потому, что VB предполагает дополнительные телодвижения и автоматически делает их. Но вы, если вам вдруг это не надо и вы точно уверены, что переполнения не будет (или вам всё равно на результат, если оно всё-таки случится), можете деактивировать это поведение:

Продвинутые оптимизации — убираем автоматические проверки переполнений.

Продвинутые оптимизации — убираем автоматические проверки переполнений.

После этого (я также сделал общение, что в коде нет aliasing-а — ситуации, когда на одно местоположение в памяти ссылаются две различные переменные), если перекомпилировать код, то и для функции Fact() выхлоп на выходе VB-компилятора станет таким же, как у компилятора C++:

Кто угодно может пнуть мёртвого льва - 22

Машинный код идентичен инструкция-в-инструкцию, байт-в-байт, бит-в-бит.

Но мы с вами помним — VB просто вшивает исходный код в EXE-шник, наряду с интерпретатором. VB умеет делать EXE, но эти EXE априори ненастоящие, неполноценные, недоделанные какие-то. Ну, по крайней мере, так искренне считают люди, которые распространяют эти бредовые байки и поверия.

Вообще же, сравнение результата работы компилятора VB, работающего в режиме «Compile to Native Code», и компилятора C/C++ от Microsoft — тупейшее и бессмысленнейшее занятие. И дело здесь в следующем. (Речь, разумеется, идёт о компиляторах одной эпохии, одного поколения).

Нужно знать, как устроен компилятор (транслятор) C/C++, являющийся частью Microsoft Visual C++ 6.0 (для более ранних версий это актуально в той же степени — не только для шестой). Компилятор (транслятор) оформлен в виде исполняемого файла CL.EXE. На самом деле, внутри CL.EXE почти нет ничего интересного. Архитектура компилятора (транслятор) C/C++ предполагает двухкомпонентный подход: фронтенд и бэкенд (рассматривайте эти термины просто в контексте двухкомпонентной архитектуры, забудьте о веб-разработке и дополнительных коннотациях, связанных с веб-разработкой). CL.EXE разбивает работу по компиляции (трансляции) исходного файла на два этапа:

  • Первый этап выполняет фронтенд, который зависит от языка исходного кода. У Си свой фронтенд (C1.DLL), у Си++ — свой (C1XX.DLL). Задача фронтенда — выполнить первые, языко-специфичные шаги трансляции. Это обработка директив препроцессора, токенизация, построение AST-деревьев, построение таблиц имён, объектов, олицетворяющих процедуры, генерация графов хода выполнения, разворачивание циклов и тому подобное.

  • Второй этап выполняет бэкенд, который получает от фронтенда частично транслированную программу в максимально абстрагированном от конкретики ЯП формате — IL (Intermediate Language, ничего общего не имеет с MSIL в .NET). Задача бэкенда абстрактное представление программы низвести до уровня машинных команд, а точнее не только машинных команд, а объектного файла с его специями кода, данных и т.п. В случае компилятора CL.EXE из MSVC++6 бэкенд один — C2.DLL — которому абсолютно безразлично, компилировался ли сейчас исходник на C или на C++.

Таким образом, CL.EXE просто создаёт конвейер/пайплайн между фронтендом (выбирая его в зависимости от языка) и бэкендом (выбирая его в зависимости от аппаратной архитектуры, правда в случае с MSVC6 выбора нет — поддерживается только x86, 32-битный режим).

В случае же Visual Basic для генерации EXE-файлов в режиме «Compile into Native Code» Microsoft позаимствовали бэкенд (C2) компилятора C/C++ у команды Visual C++ и включили его в состав продукта VB. С единственной оговоркой: что теперь это не C2.DLL, а C2.EXE, который в процессе компиляции вызывается средой (VB IDE) и выполняет всю грязную работу.

Значительная часть оптимизаций уровня того, какие оптимальные инструкции выбрать, какой порядок чередования инструкций, какой план использования регистров и т.п. — всё это удел бэкенда C2. И абсолютно глупо сравнивать или задаваться целью сравнить, какой компилятор сгенерирует более хороший, либо быстрый, либо компактный машинный код для одной и той же задачи — VB или C/C++ — просто потому, что генерацией машинного кода и в том и в другом случае занимается один и тот же компилятор (а точнее его половинка) — C2.

Но вопрос не в том, что оптимальнее и насколько оптимальнее. Вопрос в том, что генерация машинного кода в случае с VB не уступает таковой у C++ (того же поколения, версии) просто по той причине, что делается силами и средствами того же самого механизма кодогенерации. И на фоне этого в комментариях циркулируют байки про интерпретацию или какие-то неполноценные EXE-файлы… Потому что мёртвый лев не даст сдачи; и про него можно писать любые гадости: достоверность никто не пойдёт проверять (не интересно же, когда есть модные молодёжные языки а-ля Rust или Zig), зато с радостью кто-нибудь перескажет вашу дурацкую небылицу.

Компиляция в P-code же — совсем другая история. Да, в этом случае код пользовательских процедур не превратится в машинный код. Он превратится в P-код и будет исполнен виртуальноый машиной VB. Но это ни коим образом не делает эти EXE каким-то неполноценными и не даёт право называть их интерпретируемыми, так же как выполнение Java-приложения на JVM не делает Java-код интерпретируемым.

Виртуальная машина VB — стековая. Все манипуляции операции делаются на собственном стеке ВМ. Подход чем-то похож на работы инструкциями FPU и регистрами FPU,которе организованы как стек.

Код процедуры GetMinAndMax:

Public Sub GetMinAndMax(ByVal a As Long, _
                        ByVal b As Long, _
                        ByRef Min As Long, _
                        ByRef Max As Long)
    If a > b Then
        Min = b
        Max = a
        MessageBeep MB_OK
    Else
        Min = a
        Max = b
    End If
End Sub

Например, превращается в такой P-код:

  loc_401AF8: ILdI4 arg_C   ' Поместить на стек VM аргумент #1 (он же [a])
  loc_401AFB: ILdI4 arg_10  ' Поместить на стек VM аргумент #2 (он же [b])
  loc_401AFE: GtI4          ' Сравнить два лежащих на стеке I4-числа (Long)
  loc_401AFF: BranchF loc_401B1B ' Перейти туда-то, если первое не было больше второго
  loc_401B02: ILdI4 arg_10  ' Поместить на стек VM аргумент #2 (он же [b])
  loc_401B05: IStRf arg_14  ' Убрать его из стека в память в аргумент #3 (он же [Min])
  loc_401B08: ILdI4 arg_C   ' Поместить на стек VM аргумент #1 (он же [a])
  loc_401B0B: IStRf arg_18  ' Убрать его из стека в память в аргумент #4 (он же [Max])
  loc_401B0E: LitI4 0       ' Помустить на стек VM число-литерал 0 в формате I4 (Long)
  loc_401B13: ImpAdCallFPR4 MessageBeep()  ' Вызвать импортируемую функцию MessageBeep
  loc_401B18: Branch loc_401B27 ' Безусловный переход на эпилог процедуры (чтобы перепрыгнуть Else-ветку)

  loc_401B1B: ' else-часть условия
  loc_401B1B: ILdI4 arg_C   ' Загрузить (push) в стек значение аргумента #1 (он же [a])
  loc_401B1E: IStRf arg_14  ' Извлечь (pop)) его из стека и поместить в аргумент #3 (он же Min)
  loc_401B21: ILdI4 arg_10  ' Загрузить в стек значение аргумента #2 (он же [b])
  loc_401B24: IStRf arg_18  ' Извлечь его из стека и поместить в аргумент #4 (он же Max)
  
  loc_401B27: ExitProc      ' Выход из процедуры

Теперь, когда вы встретите людей, либо утверждающих, что QuickBasic является интерпретируемым языком (особенно, если они говорят, что EXE-файл скомпилированной программы на поверку оказывается интерпретатором, с пришитым к нему исходным кодом), либо утверждающих, что Visual Basic является интерпретируемым языком (с той же чушью относительного неполноценности EXE-файлов), вы знаете, что делать. Позорьте их, бросайте в них соответствующими тряпками, минусуйте им карму. Вежливо и конструктивно объясните им, что они мягко говоря заблуждаются. Пришлите им ссылку на эту статью. И обязательно скажите, что делать громкие, но непроверенные заявления нужно обязательно с припиской «одна бабка сказала».

Фух, а теперь можно выдохнуть…

Автор: firehacker

Источник

Rambler's Top100