Пользователь
Информация
- В рейтинге
- 591-й
- Откуда
- Петропавловск, Северо-Казахстанская обл., Казахстан
- Зарегистрирован
- Активность
Специализация
Десктоп разработчик, Инженер встраиваемых систем
Pure C
Assembler
X86 asm
Win32 API
Visual Basic
MySQL
Git
ООП
Разработка электроники
Обратная разработка
Ещё раз сделал косметические правки в исходнике, чтобы он хорошо умещался на картинке.
Исходник (слева) и его дизасм:
То же самое, но блоками и стрелкам показал соответствие между строками исходного текста и кусочками машинного кода, которые из этих строк получились:
Напоминаю, что показанный на скриншотах машинный код не сгенерировался в момент запуска EXE (путём какой-то там мифической интерпретации/дотрансляции), а изначально находится в EXE-файле.
Достаточно ли этой картинки чтобы опровергнуть утверждения «QuickBasic не настоящий компилятор, когда он генерирует EXE-файл он просто сшивает интерпретатор и исходник и выдаёт это за скомпилированный EXE»?
Пардон, там в папке целая гора скриншотов и заготовок для статьи, так что я перетащил и прикрепил к комменту не ту картинку.
Приведённый выше дизасм соответствует вот этому исходному тексту:
Там по ходу статьи исходничек претерпевал некоторые косметические изменения (в основном чтобы картинки были не занимали в высоту больше места, чем мне хочется).
Ну... так бывает, когда оппоненты своими лютыми фейками задевают за что-то личное, чему посвятил много времени и сил в своё время.
Вот скажем, потратили вы какую-то часть своей жизни, работая инженером по топливной аппаратуре двигателей Ту-154. Досконально изучили их, участвовали в гонках двигателей, налаживали, устраняли неисправности. И тут вам попадаются заявления, что Ту-154 дескать, ненастоящий и неполноценный самолёт. Мотогондолы у него бутафорские, двигателей нет, а взлетает он потому что во время взлёта пассажиры крутят педели из-за всех сил, а дальше кое-как по инерции долетает куда ему нужно.
Да, по сегодняшним меркам Ту-154 устаревший самолёт, его не производят, его не эксплуатируют для пассажирских перевозок, у него много своих «моментов», но когда натыкаешься на такие одиозные заявления, имея за плечами опыт работы авиа-техником, инженером, то спокойно сложно пройти мимо. Особенно когда это не один какой-то фрик распространяет такие слухи, а когда это довольно популярное у обывателей мнение.
Что именно вызывает вызывает сомнение? Что скриншоты сняты не с экрана, а сфабрикованы/нарисованы?
Давайте ещё раз. Возможно когда вы читали статью, у вас не загрузились картинки или что-то такое.
Вот исходный текст:
Смотрим на нижнюю часть, потому что это основное тело программы. В Quick Basic-е можно было не иметь процедуры main. В Python-е и прочих PHP можно. В VB, к слову, так нельзя. Так вот, смотрим на основе тело программы.
DECLARE SUB — это просто объявление прототипа процедуры, оно не должно скомпилироваться ни во что. Дальше идут стейтменты, которые непосредственно что-то делают.
Теперь смотрим на кусок машинного кода из EXE-файла, порождённого при компиляции:
Здесь на картинке .(скриншоте) я взял дизасм-листинг и «врезал» между строк строчки исходного бейсик-кода. Таким образом, для каждой бейсик-строчки под ней написан блок машинны команд, соответствующих этой строке.
CLS транслируется в вызов функции CLS (
call 0014:0019B) из стандартной библиотеки:.PRINT "We are ready..." транслируется в
mov ax, str_constзатемpush ax, затем call функции из стандартной библиотеки, отвечающей за PRINT.Синтаксическая структура FOR транслируется в целый ряз инструкций: пролог цикла задаёт начальное значение переменной (mov ax, 0BEEF), далее идёт джамп на концевое условие (CMP+JLE), п
Внутри цикла был вызов процедуры HELLO с передачей ему переменной i — это скомпилировалось в 3 инструкции, которые кладут на стек адрес переменной i и вызываются собственно процедуру HELLO.
Здесь ни сырой исходный бейсик код как текст. Ни токенизированный бейсик текст в виде байтов, каждый из которых означате токен. Ни какая-то сериализация AST. Ни набор инструкций для какой-то QB-специфичной виртуальной машины. Ни что-либо ещё такого.
Здесь ровно те машинные инструкции, которые получились бы на выходе Borland C, если бы ему на вход скормить аналогичный сишный код, делающий аналогичный цикл. Ну, максимум, с поправками на какие-то ABI-шиные особенности и моменты с разницей в соглашениях о вызове.
Что тут может быть сомнительным?
Конечно генерировал. В первоначальной статье это было продемонстрировано.
И что это доказывает? Что QuickBasic-код не компилируется в машинный, а интерпретируется в момент выполнения?
Аналогичным образом программа на C или C++, если стандартная библиотека не будет статически влинкована в исполняемый файл на этапе сборки, будет зависеть от внешнего файла со стандартной библиотекой, и закуксится и попроситься к мамке, если не получится её нигде обнаружить.
Каким боком зависимость от внешнего файла, при том опциональная (на выбор разработчика — хочешь влинкуем всё в EXE, а хочешь будет внешняя зависимость) доказывает что язык является интерпретируемым и опровергает, что программа компилируется в машинный код?
Вот уж кто мастер натягивания совы на глобус с такой-то аргументацией...
И что это доказывает? Что QuickBasic-код не компилируется в машинный, а интерпретируется?
В архитектуре AVR8 нет FP-сопроцессора и FP-инструкций. Если мы пишем на C или C++ и компилируем с помощью avr-gcc, то FP-математика тоже будет эмулироваться (конечно, не с помощью прерываний). Какой вывод, по мнению автора, из этого нужно сделать? Что C и C++ в случае компилирования AVR8 становятся интерпретируемыми языками? Что avr-gcc — неполноценный компилятор? Или только факт использования инструкции INT волшебным образом делает разницу? Или где та чёртова грань или хоть какая-то логика? К чему эта демонстрация эмуляции FP-математики?
Уж если кто и натягивает сову на глобус и другие предметы, так это автор этой статьи.
Во-первых, в той статье опровергался тезис о том, что QuickBasic тупо подшивает исходный текст программы в EXE-файл и при запуске EXE-файла начинается интерпретация исходного текста. Тезис был опровергнут просто-напросто демонстрацией машинного кода, в который переродился исходный QuickBasic-код. Если бы имела место интерпретация в момент запуска, готового машинного кода в EXE-файле, соответсвующего логике исходной программы, явно бы не было.
Но здесь @axe_chitaпытается это переиграть, обнаружив в бинарнике тексты сообщений об ошибках:
Так продемонстируйте же факт синтаксического разбора исходного кода в уже готовом EXE-файле? Прямо с выдачей этих самых сообщений в случае, когда что-то не так с систаксисом исходной программы? Сам по себе факт наличия этих строк с сообщениям об ошибках ничего не доказывает. Он мог бы лишь навести на мысль об интерпретации исходного кода в момент запуска, породить просто в голове такую гипотезу, но демонстрация наличия в EXE-файле машинного кода (который соответствовал бизнес-логике исходного QuickBasic-кода) опровергает эту гипотезу.
---------
Во-вторых, и это главное, автор на протяжении всей статьи навязывает своё толкование понятия «интерпретируемый» и «интерпретация».
Выполнение P-code почему-то ему религия не позволяет называть выполнением: «нет, это не выполнение, ни в коем случае не выполнение, это интерпретация — вы не перепутайте». И дальше всё опровергается именно в таком ключе, присыпанное разными глупостями.
Непонятно только почему в таком случае выполнение x86-кода непосредственно процессором автор отказывается тоже называть не выполнением кода, а его интерпретацией. Ведь там мы имеем декодирование инструкций, ведь там мы имеем некую логику проверку корректности операндов инструкции, ведь там, как известно, CISC-инструкции транслируются в RISC-микроинструкции.
Пусть и это тогда называется не выполнением машинного кода, а интерпретацией. И все программы, в том числе написанные на C, C++, да хоть даже ассемблере, мы будем называть интерпретируемыми программами. Если автора устраивает такой расклад, я умываю руки и мне не о чем больше спорить.
Любая P-code инструкция, точнее её опкод, это просто индекс в таблице, определяющей, куда будет осуществлён переход (джамп) с одного куска native-кода на другой кусок native-кода. Работа виртуальной машины — это просто выполнение native-кода с периодическими джампами туда-сюда. Почему-то это нужно называть словом «интерпретация»... (Кто кого интерпретирует?)
Когда в C++ коде происходит вызов виртуальной функции класса, машинный код тоже берёт адрес из vftable и делает переход неизвестно куда (в том смысле, что это заведомо неизвестно на этапе компиляции). Повод ли это считать, что C++-программа, в которой есть вызовы виртуальных функций классов, тоже считается интерпретируемой? Если нет, то где принципиальная грань?
Что в принципе может быть непонятного в бомблении по поводу фейков и дезинформации?
Почему не получилось бы?
Бейсик — понятие размытое. У него куча диалектов и разных реализаций. Поэтому нужно говорить о конкретном диалекте и конкретном продукте. Я не говорю про все Бейсики на свете, особенно про совсем допотопные «нечто» из 60-х годов, где даже блочных IF не было и их приходилось эмулировать с помощью GOTO (убеждён, что Дейкстра именно по этой причине наезжал на Бейсик).
Но меня всегда задевают наезды на два диалекта: Visual Basic (особенно сильно) и QuickBasic (в меньшей степени, но тоже).
Для этих двух диалектов выражение «не существует компиляторов в код с нативными инструкциями» — не верны, потому что и QuickBasic-овский компилятор компилирует в нативный код для x86 (для него это вообще единственный режим), и Visual Basic тоже это делает (для него это основной режим, режим по умолчанию, но есть альтернативный режим с генерацией в код собственной виртуальной машины).
Тем не менее, даже после того, как продемонстирован код на QB/VB и непосредственно машинный код, в который его превратил соответсвующий компилятор, находятся люди, которые пишут «вы всё врёти, там виртуальный машина и вообще построчная интерпретация». Я не понимаю, что с этими людьми — наверное тяжело разрушить в голову догму, с которой жил очень долго.
Это опять клевета на VB. С чего бы ему быть более медленным?
VB будет ровно настолько медленным, насколько вы его заставите. Если сама библиотека, с которой вы намерены работать, предлагает изначально медленный интерфейс взаимодействия, то взаимодействие через этот интерфейс будет естественно медленным. Но оно таким будет хоть из Delphi, хоть из C++.
То есть ли библиотека предполагает, что взаимодействовать с объектами можно только через позднее связывание (по терминологии COM) через IDispatch, то естественно это будет медленно: потому что нужно имена методов/свойств транслировать в DISPID, все аргументы упаковывать в VARIANT, если используются именованные аргументы — ещё их их dispid резолвить.
Если же вы работаете с библиотекой через вызовы по типу раннего связывания (по терминологии COM), то и код вызовов не будет «куда более медленным» (потому что с чего бы ради ему таким быть?), чем эквивалентный код на Delphi или на C++.
Либо если библиотека предлагает оба способа, но вы насильно со стороны VB-кода выбираете более медленный (но более гибкий) способ, то это будет медленнее, но это же были целиком вашим решением.
Я ещё раз предлагаю посмотреть на пример взаимодействия с Direct3D:
https://habr.com/ru/companies/ruvds/articles/971476/#comment_29219988
Какие излишества тут сгенерировал компилятор VB? В каком месте и за счёт чего Dephi или C++ обыграли бы его, сгенерировав более компактный и быстрый код?
Если этот вопрос относится к фразе «компилятор у Борланда генерировал более убогий машинный код», то ответ — по сравнению со здравым смыслом.
Я много раз в машинном коде, сгенерированном Борландом, замечал идеому в духе:
или
вместо просто
xor eax, eaxMicrosoft-овский компилятор такими глупостями не страдал, а вот борландовский — да. Неужели никогда не замечали?
Нет, не корректное.
У QuickBasic в скомпилированном виде не было никакого Pcode и никакой виртуальной маины. Статья это чётко демонстрирует. У Visual Basic виртуальная машина в скомпилированном виде была, но во-первых, не в EXE, а в msvbvm?0.dll, а во-вторых, она не крутилась, а лежала там мёртвым грузом, если проект был скомпилирован в Native Code.
А это и не работает эффективно: это очень сильно зависит от кода, от того, что там делается и от того, как он написан — какой-то код будет быстрее при компиляции в native code, какой-то в P-код, плюс ещё от системы, на которой он выполняется и на которой происходит замер. В общем, русская рулетка и вопрос удачи.
Но спорить с тем, что тысячу раз выполненный вызов одной и той же native code процедуры быстрее, чем огромный native code, состоящий из тысячу раз продублированной начинки этой процедуры, тоже вряд ли имеет смысл.
Это бессовестная манипуляция термином «интерпретируемым». Если вы можете себе позволить сказать, что процессор x86 интерпретирует свой машинный код, то да, у меня нет претензии к вашей фразе «интерпретирумый Pcode». Если же по вашему это неуместное употребление слова «интерепретирует», и правильно говорить, что процессор x86 выполняет свой машинный код, то я требую и в отношении Pcode говорить, что виртуальная машина его выполняет, а не интерпретирует.
Я ни в чём не ошибаюсь насчёт Pcode. Если есть что возразить, пишите конкретно, в чём именно я ошибаюсь.
Всё смешали в одну кучу.
Не надо «ЕМНИП», я в качестве реакции на «бесподобные» комментарии под данной статьёй написал свою статью: https://habr.com/ru/articles/973594
Просто тем, что тот же самый алгоритм представляется малым количеством много раз повторяемых макро-блоков машинного кода.
Без всякой P-кодной виртуальной машины можно представить себе такую же ситуацию на примере программы на Си или C++: представьте, что вы задали такие ключики компиляции, что приказали заинлайнить абсолютно всё. Инлайнятся абсолютно все функции, маленькие и большие, а также, допустим, вы как-то заставили компилятор разворачивать все циклы, где число итераций известно в момент компиляции. И плюс ещё вы отключили COMDAT Folding у линкера. Каким будет результат?
Очень запросто этот трюк с инлайнингом может замедлить выполнение нативного исполняемого файла, потому что:
Он раздувает размер машинного кода, и это сказывается на увеличении количества страниц, и если у вас включен включен и штатным образом работает механизм подкачки, то просто может оказаться, что время от время выполнение будет переходить на страницу, котороая нет в физической памяти, которая выгружена, и ОС придётся её подгружать с диска, а это медленно. Естественно, это происходит только при первом обращении к странице, но если в системе много одновременно работаюших приложений, большой объём суммарно выделенных страниц памяти, большой объём файла подкачки (или файлов подкачки — их может быть несколько), но по сравнению с этим малый объём физической памяти, то конкуренция за страницы ФП может быть большой и та же страница в скором времени опять может быть выгружена и всё повторится при следующем обращении. И
Он просто раздувает код и заставляет появляться там повторяющимся паттернам машинных инструкций, которые процессор вынужден декодировать каждый раз как в первый раз, хотя он мог бы взять это в uop-кеше. Когда я говорю «каждый раз как в первый раз», я не имею в виду повторные вызовы того же самого кода, я имею в виду что если у вас развернулся цикл из 1024 итераций в «китайский код», то тело цикла будет декодироваться 1024 раза. Опять же, когда дело дойдёт того следующего раз, когда этот код должен будет выполниться, не факт, что он к этому моменту сохранится в кешах процессора. В системе ведь может быть ещё куча других процессов, и все конкурируют за кеши.
Вот в статье я приводил в качестве примера функции GetMinAndMax и Fact, показывая, что при компиляции в машинный код их код достаточно рационально компактен и ничем не уступает аналогиному сишному коду. Естественно, эти примеры взяты не с потолка: когда я пишу код (хоть на VB, но на C и C++), у меня в голове уже есть представление, во что это скомпилируется или примерно может скомпилироваться. Точно так же, как я могу выбрать удачный пример, я могу выбрать и анти-пример:
Абсолютно пустая функция, ничего не делающая, но аргументы типов — Variant и они передаются ByVal, а под капотом Variant это 16-байтная структура, и всё усугубляется правилами владения и управления ресурсами, на которые есть «ссылки» изнутри VARIANT-структур.
Поэтому эта пустая абсолютно ничего не делающая функция компилируется вот в такой машинный код:
Вот тебе и абсолютно пустая функция!
Если присмотреться и разобраться, то здесь просто есть огромный пролог и сразу же огромный эпилог функции.
Пролог делает:
Установка EBP для нового фрейма.
Добавляет новый SEH-хендлер в цепочку.
Резервирует место под семь локальных переменных типа VARIANT — шесть под копии аргументов и одну под переменную, хранящую возвращаемое значение функции до непосредственно самого возврата из функции (это одноимённая переменная Heavyproc с именем таким же, как у своей родительской функции).
Зануляет первое поле .vt у каждой структуры, чтобы структура считалась инициализированной (и при этом пустой).
Шесть раз вызывает __vbaVarDup для копирования переданного аргумента в локальную переменную-копию.
Эпилог делает обратное:
Шесть раз вызывает __vbaFreeVar для зачистки VARIANT-структуры с освобождением ресурсов, которыми она могла владеть.
Вручную перемещает начинку Heavyproc в предоставляемый вызывающей стороной приёмник возвращаемого значения.
Восстанавливает неизменяемые регистры (EDI, ESI, EBX), убирает SEH-фрейм.
Во всём этом машинном коде нет никакой бизнес-логики приложения (функция-то пустая у нас в исходнике, только само объявление), но есть масса инструкция для выполнения всмогательных вещей.
Компилятор догадался соптимизироваь вызов
__vbaVarDup, поместив адрес вызываемой функции в ESI и делая потом только call esi, но аргументы для вызова (а__vbaVarDupиспользует fastcall) вычисляются (с помощью LEA) каждый раз в индивидуальном порядке.И этого всего лишь 6 аргументов (а могло бы быть 26) на фоне абсолютно пустой функции.
Если же скомпилировать тот же VB-код в P-code, то функция Heavyproc будет состоять всего лишь из 7 P-кодных инструкций:
Здесь и проявляется разница: когда виртуальная машина 6-раз выполняет VM-ную инструкцию FDupVar, она шесть раз выполнят один и тот же макро-блок машинного кода, который уже давно сидит как просто в L1I-кеше (и эти инструкции не надо дёргать из памяти), так и в uop-кеше (и процессору даже не надо заново их декодировать). И поскольку это довольно часто используепмые P-кодные инструкции, вызываемые практически из любой процедуры VB-кода, то их машинные имлементации никогда не вылезают из кешей, и страницы памяти, на которые приходятся эти имплементации в машинном коде, тоже вряд ли выкидываются из working set'а.
Сама имплементация P-кодной инструкции FDupVar в виртуальной машине выглядит вот так:
По сути тут тот же самый вызов __vbaVarDup, который шесть раз делался из native code в примере выше. Только там это было 6 раз разных, подряд идущих вызовов, а здесь это один и тот же вызов, повторяемый шесть раз как бы в цикле.
Цикл образуется последней инструкцией (JMP, подсвеченный жёлтеньким). Во это вот
mov al, [esi+4]иjmp [tblByteDisp + eax*4]это фетч новой P-код инструкции и переход на её хендлер. Когда этот кусочек машинного кода закончит обработку первой P-кодной инструкции FDupVar, он дойдёт до конца и вот этот жёлтый JMP перенесёт нас обратно на lblEX_FDupVar — и так 6 итераций в общей сложности.И да, у инструкции FDupVar два операнда размером WORD (откуда и куда копировать Variant-значени в виде смещений относительно базы текущего стекового фрейма) и поэтому в хендлере этой инструкции (т.е. в этом кусочке машинного кода, что на картинке выше) есть два обращения к памяти (а в native коде их как бы нет, те же самые смещения вшиты в операнды LEA-инструкций). Однако, по всей видимости, эти обращения обходятся супер-дёшево, потому что тут операнды идут вплотную к опкодам FDupVar-инструкции, при «фетчинге» P-кода сразу вся кеш-линия попадает в кеш данных.
Конечно могу: самое лучшее предложение, это ничего вообще не переводить, потому что программист, не знающий английского языка на таком базовом уровне — это нонсенс.
Но если совсем невмоготу, можно было перевести как «Сборка». А так, очевидно, перевод на картинке вообще был сделан тупо засовыванием строк в ПРОМТ без понимания им контекста, в котором употребляются фразы.
Все так, не обращайте внимание. Нынче и за длинное тире линчуют, потому что якобы так делают только LLM.
Я вот уже 20 лет ставлю длинное тире везде и всегда, где это нужно и возможно, и формулирование мыслей у меня в голове завязано на представление информации в виде таблиц и списков. Даже не знаю, как теперь писать статьи на фоне таких капризов аудитории.