Пользователь
Information
- Rating
- 1,936-th
- Location
- Петропавловск, Северо-Казахстанская обл., Казахстан
- Registered
- Activity
Specialization
Software Developer, Embedded Software Engineer
Pure C
Assembler
X86 asm
Win32 API
Visual Basic
MySQL
Git
OOP
Electronics Development
Reverse development
Всё потому что кто-то не знает сам, но охотно плодит и тиражирует популярные мифы, которые незаслуженно бросали и бросают тень на отличный, в общем-то, продукт.
Всё совершенно не так. В VB6 IDE или VBA IDE (которые суть одно и то же, потому что скомпилированы из одних и тех же исходников) исходный код в виде человеко-читаемого текста перестаёт существовать уже в тот момент, когда вы заканчиваете ввод/редактирование очередной строки в редакторе кода и перемещаете каретку на другую строку (нажатием ли клавиши Enter, или же стрелочками/мышкой), либо же когда окно редактора кода теряет фокус (и редактор проводит валидацию, если на тот момент он находился в режиме правки строки).
VB IDE под капотом, то есть внутри себя, в своей памяти вообще не хранит исходный код модулей проекта в виде текста — ни в виде текста целиком (то есть в том виде, в каком код хранится в .bas/.cls/.frm/.ctl-файлах на диске), ни в виде крупных фрагментов этого текста, ни в виде отдельных строк, ни в виде отдельных токенов (с некоторыми исключениями — о них далее). Она хранит исходный код (и работает с ним) в компактном бинарном виде, значительно обработанным при том по сравнению с исходным текстом. В этом легко убедиться: если присоединиться к IDE отладчиком (типа OllyDbg) или снять дампа памяти, то в адресном пространстве, хоть всё целиком его прошерсти, не удастся найти ни кода целиком, ни отдельных строк.
Для простоты будем считать, что мы только что написали в редакторе кода новую строку кода и нажимаем Enter. Сразу же в этот момент строка (line) кода, которая, кстати, может быть многострочковой(!) строкой (multi-row line) благодаря наличию возможности переноса строки (символом
_
), распарсивается — сперва на основе строки (как цепочки букв) строится древовидная бинарная структура данных, которую условимся называть PCR-деревом или просто PCR. Затем на основе PCR-дерева формируется опять же бинарная, но уже не древовидная, а линейная структура — назовём её BSCR, а также в некоторых случаях создаётся ряд дополнительных бинарных структур в памяти IDE.На самом деле любая строка в VB состоит из трёх частей:
[МЕТКИ] [СТЕЙТМЕНТЫ] [КОММЕНТАРИИ]
Эти части опциональны — любая из них может отсутствовать, в том числе и все три (что даёт нам просто пустую строку). Но если какие-либо есть, то они обязаны идти именно в таком порядке — комментариев
/*
в духи Си*/
здесь не бывает; комментарий (будь тоRem
или'
), если и имеется, то идёт самым последним в строке.Так вот, первый этап парсинга состоит в попытке выделить метку (её может и не быть), после чего начинается последовательный обход токенов и построение PCR-дерева, и в конце, если нет претензии на продолжение стейтментов (которого по факту нет), делается попытка выделить комментарии (если они есть).
Таким образом на первом этапе на вход поступает строка (line) в виде строчки (row) или набора строчек (rows), если в строке осуществлялся перенос строки. На выходе же образуется:
:
).Таким образом, PCR-дерево описывает не всю строку целиком, а только среднюю «часть», то есть только стейтменты. С учётом того, что метка и комментарии могут присутствовать, а стейтментов не быть, PCR-дерево может оказаться пустым деревом. Например, вот такая строка является синтаксически корректной, но в ней нет стейтментов и PCR-дерево будет пустым:
или даже такая «многострочковая» строка:
PCR-дерево не является конечной формой представления строки после распарсивания: если первый этап (главным образом это построение PCR-дерева) прошёл успешно (что обычно означает, что в строке не было синтаксических ошибок, «незакрытых» скобок и тому подобного) проводится некая проверка PCR-дерева на корректность (что довершает проверку корректности синтаксиса и даже чуть-чуть захватывает зону ответственности семантического контроля), после чего древовидное PCR-представление новоиспечённой строки кода трансформируется в опять же бинарное, но уже не древовидное, а линейное представление — назовём его BSCR.
В отличие от PCR-представления, BSCR-представление менее гибкое (в нём нет возможности представления заведомо синтаксически или семантически недопустимых конструкций, а в PCR такая возможность есть), но значительно более компактное. Оно совершенно не древовидное, оно линейное: если в PCR-дереве взаимоотношение между сущностями выражается в том, что в родительском узле есть указатели на дочерние, то в BSCR-сущностях отношения между сущностями выражается тем, в каком порядке следуют BSCR-сущности в цепочке, при этом используется идея обратной польской нотации. Тогда как PCR-представление описывает только среднюю часть строки (стейтменты), BSCR-представление охватывает строку целиком, включая и информацию о метке (если есть), и о комментарии в конце строки (если он есть).
Если PCR-дерево состоит из 40-байтовых узлов (нод), каждая из которых имеет такие поля как «тип ноды», «флаги» и до 6 параметрических полей, суть и смысл которых зависит от типа конкретно взятого узла (в большинстве случаев это адреса, то есть указатели на дочерние узлы), которые могут как угодно лежать в памяти, BSCR-цепочка состоит из следующих строго друг за другом (без дырок и промежутков) 16-битных (то есть двухбайтных) сущностей, в череду которых вплетаются включения, кодирающие либо бинарное представление литералов (числовых и строковых констант), либо какие-то параметры сущностей, при этом всё выравнивается по 16-битной/двухбайтовой границе. При этом в BSCR-цепочке никогда не бывает никаких указателей/адресов (но бывают индексы), в результате чего BSCR-блоки можно свободно перемещать по памяти, не корректируя никакие указатели, а также их можно рассекать и раздвигать, вставляя в середину новые BSCR-сущности.
Выше я написал, что если текстовое представление строки кода удалось превратить в древовидное PCR-представление, вторым шагом сразу же по PCR-представлению строится BSCR-представление. На самом деле, BSCR-представление строки генерируется даже если редактору кода подсунули чуть-чуть некорректную или абсолютно некорректную строку: для этого используется специально выделенная для таких случаев BSCR-сущность типа «некорректная строка кода». Этот как раз тот исключительный случай, когда вся строка целиком в своём первозданном виде копируется в BSCR-представление кода, и только в этом исключительном случае при попытке отыскать что-то в памяти IDE у вас получится найти образец исходной строки. Подобная строка, парсинг которой закончился ошибкой, в редакторе кода затем показывается красным цветом — до тех пор, пока программист не предпримет попытку поправить её, после чего жизненный цикл строки кода начнётся с самого начала по пути
ТекстоваяСтрока → PCR-дерево → BSCR-представление
PCR-дерево является временным форматом представления строки и после формирования BSCR-представления строки сразу же уничтожается.
BSCR же является долгосрочным способом существования/хранения/обработки исходного VB-шного кода внутри VB IDE. Ещё раз: исходный VB-шный код в виде сырого текста внутри IDE не хранится! Ни одним целным блоком. Ни как массив отдельных строк. Ни как массив отдельных токенов. Каждая строка кода представляется последовательностью 16-битных BSCR-сущностей. В такую последовательность вшиты строковые литералы (да и числовые) и комментарии, если они есть в данной строки. Исключения составляют синтаксически некорректные строки, подсвечиваемые при отображении красным — в таком случае используется специальная BSCR-сущность, вслед за которой идёт некорректная строка в своём первозданном виде.
Итак, IDE не хранит исходный код в виде текста. Когда редактору коду нужно отрисовать на экране ту часть (потенциально гораздо более объёмного) исходного кода, которую сейчас должен видеть пользователь, она по бинарному BSCR-представлению реконструирует текстовое представление только лишь той части всего кода, которую нужно отрисовать. При этом же происходит и подсветка синтаксиса (включая подсветку красным некорректных строк). То же самое происходит, когда нужно сохранить исходный код в файл или скопировать в буфер обмена — текстовое представление кода вновь воссоздаётся, но лишь на короткое время, т.е. на время отрисовки или записи в файл. Первоисточником исходного кода для IDE является именно бинарное BSCR-представление.
VB6 IDE при сохранении проекта записывает всё в файлы, имеющие текстовый формат (.bas/.cls/.frm/.ctl) — в момент сохранения по чисто бинарному BSCR-представлению реконструируются текстовое человеко-читаемое представление кода. В момент открытия проекта и загрузки файлов IDE обрабатывает строки одну за другой, распарсивая каждую точно так же, как если бы каждая последующая строка просто писалась с нуля в редакторе, после чего нажимался бы Enter, с той лишь единственной разницей, что вывод ошибок (жалоб на ошибки синтаксиса) подавлен, в результате чего если в сохранённом файле были некорректные строки, то они сразу молча станут красными, без большого числа выводимых сообщений (при этом и для режима интерактивной правки кода можно отключить вывод сообщений об ошибках синтаксиса).
А вот VBA IDE при сохранении записывает в файл BSCR-представление, не конвертируя его в текст. Поэтому если в Excel-евском или Word-овском файле (или базе Access) открыть редактор VBA и написать там любой код, после открытия .xls/.doc-файла в блокноте или hex-редакторе там не удастся найти ни одной строчки VB-кода. Ни визуально, ни используя поиск. Это не потому, что код зашифрован или сжат. Это потому, что VB IDE разбирает код (в виде текста) не в момент запуска
этого кода, не в момент исполнения, а в момент попадания кода в саму IDE, и в случае VBA в MS Office это бинарное представление кода (BSCR) записывается прямо в файл, откуда потом и загружается.
BSCR-сущность — это 16-битное число. Из BSCR-сущностей составляется BSCR-представление строк кода. Некоторые BSCR-сущности имеют параметры, которые в BSCR-представлении следуют за сущностью как 16-битное число.
Чему в коде соответствует BSCR-сущность? Каждому ключевому слову или токену — своя сущность? Нет, одна BSCR-сущность соответствует скорее логической сущности из кода, и такими сущностями могут быть разнородные вещи: это и отдельно взятое число (числовой литерал), и комментарий, и control structure.
Начнём с простых примеров:
Строчка
Option Explicit
в BSCR-представлении кодируется как0x10CD
.Строчка
Option Compare Text
кодируется как0x08CD
.Строчка
Option Base 1
кодируется как0x04CD
.Не нужно быть особо внимательным, чтобы заметить похожее 0xCD в младшем байте 16-битной BSCR-сущности. На самом деле BSCR-сущность имеет следующий формат:
Младшие 10 бит 16-битной сущности определяют тип сущности, от чего зависит интерпретация сущности и следующих по соседству с ней данных. Старшие 6 бит определяют подтип сущности или некие дополнительные флаги. У большинства BSCR-сущностей никаких подтипов нет, и это поле (старшие 6 бит) содержат нули и ни на что не влияют.
Для наглядности я теперь буду использовать форму
[xxxxx]
для обозначения BSCR-сущности, у которых нет подтипов или флагов, и форму[xxxxx/yy]
для BSCR-сущности типа xxxxx с флагом yy.Тогда для конструкций
Option ...
предусмотрены следующие способы BSCR-представления:Как можно видеть отсюда, BSCR-сущности соответствуют не отдельно взятым ключевым слвоам или токенам, а «единицам смысла».
Каждая конструкция или то, что называется control structure, кодируется своей отдельной BSCR-сущностью.
Например,
End Function
это [0x69],End If
это [0x6B],End Property
это [0x6D],End Select
— [0x6E] (флаги, как видно, не используются вообще).Некоторые BSCR-сущности имеют параметр. Параметр сущности как правило (но не всегда) является 16-битным числом и всегда следует после BSCR-сущности. Некоторые сущности имеют несколько параметров. Некоторые сущности имеют параметры переменной длины, и в таком случае один из параметров определяет размер остальных параметров (это касается BSCR-сущностей для представления строковых литералов, комментариев, некорректных строк кода).
Как я уже писал выше, одна line кода может иметь несколько statement-ов. Целые процедуры в VB можно записать в одну строчку, используя символ двоеточия (
:
).Разделителю двух statement-ов соответствует своя BSCR-сущность, при этом она имеет параметр, означающий, на какой колонке должен начинаться следующий за разделителем statement, то есть, грубо говоря, какой отступ относительно начала строки он должен иметь. Если statement должен начинаться сразу же после предыдущего (не упуская из виду двоеточие и следующий за ним пробел), этот параметр имеет значение 0.
Так, например, вот такая строка кода:
в BSCR-представлении будет кодироваться вот так:
Или, если не использовать наше соглашение о записи BSCR-сущностей, а использовать простой hex-дамп:
Можете вставить вышеприведённую строчку кода в модуль VB- или VBA-проекта, после чего подключиться к процессу VB/VBA отладчиком и попробовать в памяти найти хотя бы строчку «Option Explicit» — уверяю вас, вы не найдёте её там, или же найдёте, но это будет мусор, который можно затереть чем угодно, и это ни на что не повлияет.
Зато вы гарантированно найдёте в памяти ту последовательность байтов, которая показана на вышеприведённом hex-дампе. Более того, если вы поменяете в ней
0x08CD
([0xCD/2]) на0x10CD
([0xCD/4), в редакторе кода тотчас же строчка поменяется наOption Explicit: Option Explicit: Option Base 1
Если же вы замените
0x0046 0x0000
на0x0046 0x0020
, то второй Statement будет начинаться на 32-й колонке:Option Explicit: Option Explicit: Option Base 1
Если первоначальную строчку вставить в VBA-проект в Excel и сохранить книгу, а затем открыть .xls-файл, те же самые байты вы найдёте внутри него, но ни за что не найдёте там стрчоку «Option Explicit» или «Option Compare Text», потому что, как я уже писал, в .xls-файл сохранится BSCR-представление кода как есть, без конвертации в текстовое представление.
Некоторые конструкции языка предполагают наличие в них каких-то выражений. В этом случае BSCR-запись прибегает к правилам, характерным для обратной польской нотации (RPN): в цепочке BSCR-сущности сначала будут идти BSCR-сущности, соответствующие представлению выражение, и только в самом конце будет идти BSCR-сущность, соответствующая самой конструкции.
Начнём с простых примеров.
Три разных конструкции, существующие в синтаксисе VB для создания циклов, позволяющие задать условие (в виде выражения, которое будет вычисляться и проверять на каждой итерации), по которому осуществляется прекращение/продолжение работы цикла.
В BSCR-формате они кодируются сущностями [0x62], [0x61] и [0xF4] соответственно. Эти BSCR-сущности сами по себе не имеют параметра (который шёл бы после самой сущности), но вот конструкции, кодируемые этими сущностями, зависят от выражения, и в BSCR-кодировании конструкции целиком BSCR-кодирование выражения будет предшествовать BSCR-сущности, кодирующей тип конструкции.
То есть в BSCR-представлении эти три конструкции будут выглядеть так:
Как же кодируется BSCR-представление выражений в данном? Во-первых, любые выражения (а не только в контексте условия цикла Do/While) кодируются единым образом, так что в только что заданном вопросе можно смело убрать словосочетание «в данном случае». Во-вторых, давайте поговорим о концепции выражений.
Что такое выражения и из чего они могут состоять? В простейшем случае выражение является атомарным и состоит из одного лишь упоминания литерала (числовой, текстовой или булевой константы), либо упоминания идентификатора (имени переменной или константы, свойства или функции, не требующей аргументов). С использованием операторов, скобок, или обращений к функциям или параметрическим свойствам из атомарных выражений могут быть составлены составные выражения. Впрочем, с использованием операторов, скобок и обращений к функциям сложные выражения могут быть составлены и из других сложных выражений.
Начнём с примеров кодирования простых выражений. Для кодирования булевых литералов (логических констант True и False) в BSCR-предусмотрена сущность [0xB7/u], где u — 0 или 1, в зависимости от того, False или True мы кодируем.
Таким образом, строка кода, представляющая собой типичный пример бесконечного цикла, в BSCR-представлении будет выглядеть вот так:
Как я уже писал выше, сложные выражения могут быть образованы с других выражений (атомарных или составных) и операторов: унарные операторы образуют операции с участием одного операнда, бинарные — с использованием двух операндов, операндами при этом являются другие выражения (атомарные или составные).
Каждый оператор имеет свою BSCR-сущность, кодирующую его. При этом, в случае с операторами в BSCR-форме сначала следует BSCR-запись операнда или операндов, и лишь после него/них следует BSCR-сущность самого оператора.
Так, например, для оператора
Xor
используется BSCR-сущность [0x02], а для оператораOr
— [0x03].Таким образом, строка кода
под капотом будет закодирована не иначе как
Числой литерал, являющейся целочисленной константной, укладывающейся в диапазон типа Integer, кодируется в BSCR следующим образом:
где [W:val] — непосредственно само число в виде знакового двухбайтового значения.
Таким образом, строка
While 1 And 2
будет закодирована какЕсли выражение взять в скобки:
While (1 And 2)
, то это будетЕсли его немного усложнить:
BSCR-представление будет таким:
Не трудно догадаться, если таким образом можно закодировать сколь угодно сложное выражение, если знать BSCR-сущности для всех операторов (выше приведена табличка), для кодирования всех типов литералов, для кодирования обращения к идентификаторам и обращения к функциям или параметрическим свойствам.
Упоминание идентификатора (например переменной, константы, непараметрического свойства) в коде в BSCR-виде кодируется сущностью [0x20/fff], где fff — флаги, например 0x20 в случае, если упоминаемый идентификатор должен быть взят в квадратные скобки.
В VB предусмотрена возможность использовать идентификаторы, нарушающие собственные правила VB в отношении идентификаторов — для этого идентификатор берётся в квадратные скобки. Это жизненно необходимо при работе с объектами/интерфейсами/функциями, имплементированными на других ЯП с другими правилами в отношении идентификаторов, а также для работы с объектами, чьи имена могут содержать пробелы и другие непозволительные для идентификаторов символы.
Типичный пример: если в Excel мы имеем лист, названный «Summary», то в VBA-макросе мы можем написать
для удаления этого листа. Но если лист называется «Our $$$», то мы можем выкрутиться из ситуации вот так:
Что, впрочем, является просто альтернативой менее компактной формы записи
Однако если мы работаем с COM-объектом, реализованном, к примеру, на С++ и имеющим имена свойств, нормальные для С++, например, имеющим свойство «__hidden_prop», нарушающее правила VB в отношении идентификаторов, потому что в VB идентификатор не может начинаться на символ подчёркивания, то единственный способ работать с этим свойством — обрамить его упоминание в квадратные скобки:
После этой 16-битной сущности [0x20/fff] обязательно следует 16-битная сущность, являющаяся индексом идентификатора в глобальной коллекции идентификатором.
Идентификаторы, в отличие от строковых литералов или комментариев, не попадают в BSCR-представление напрямую. Вместо этого парсер кода, разбирая строку кода сразу же после её внесения в проект, когда он встречает нечто, что должно быть идентификатором, пытается найти идентификатор в коллекции идентификаторов, либо, если его там нет, добавляет в коллекцию идентификаторов новый идентификатор.
Всякий раз, когда в коде встречается упоминание идентификатора, в BSCR-представлении этого кода будет фигурировать сущность [0x20/f], после которой будет идти индекс идентификатора.
Такая архитектура является причиной того, что в VB в принципе нельзя в одной процедуре объявить переменную
foo
, а в другойFOO
. Написание идентификатора не может быть разным в разных процедурах, оно будет одинаковым в пределах всего проекта, потому что представление кода (BSCR-представление) обращается к идентификаторам по их индексам, а коллекция идентификаторов глобальна для всего проекта.Кроме того, менеджер базы данных идентификаторов не имеет механизма контроля за использованием идентификатора и сборки мусора, поэтому идентификаторы в коллекцию заносятся при первом же появлении где-либо в коде, но никогда не удаляются из коллекции, даже если в коде проекта не осталось ни одного упоминания. Следствием такого подхода является забавный баг, проявляющийся тем, что если маниакально переименовывать какую-нибудь переменную или константу, всякий раз меняя её имя на ранее не использованное, то число таких попыток не может превысить 32 тысячи раз — причина в исчерпании свободных индексов для идентификаторов, ведь обращение к идентификаторам из BSCR-представления кода осуществляется именно по их индексам. Разумеется, перезапуск IDE вызывает парсинг кода и заполнение коллекции идентификаторов с чистого листа, поэтому ограничение на количество попыток переименования идентификатора сбрасывается перезапуском IDE.
Так вот, с учётом того, что мы знаем, как осуществляется BSCR-кодирование упоминания идентификатора в коде, можно показать, как кодируются следующие конструкции:
(здесь 101, 102, 103 и 104 — случайно выбранные индексы идентификаторов
bSomeFlaaag
,fBaaz
,foo
иzulu
.Ещё одним автоматическим следствием такого подхода является возможность найти в памяти IDE этот словарь идентификаторов, поменять в нём идентификатор, из-за чего все упоминания идентификатора в коде поменяются разом — во всех модулях проекта. Одна правка в одном месте в памяти может изменить тысячи упоминаний какого-нибудь «популярного» идентификатора в коде.
Если с While/Do всё более менее понятно, стоит сказать, что больштинство подобных Control Structures (конструкций) используют похожую схему BSCR-кодирования — отличие только в коде типа BSCR-сущности.
Например, конструкция
If <condition> Then
кодируется как<expr_repr> [0x9C]
.Таким образом, вот такой код:
внутри VB IDE, под капотом среды разработки никогда не будет храниться в текстовом виде, в том, в каком его видит на экране программист, или в том, в каком код хранится будучи сохранённым в файл. Он будет храниться в виде BSCR-представления этих строк кода, а именно — вот так:
Или то же самое, но без использования компактной формы записи, а в виде хекс-дампа:
Тот же самый код можно уместить в одну длинную строку, используя символ разделения стейтментов (двоеточие):
И тогда она будет кодироваться в BSCR-представлении так:
Исходя из прочитанного, вы должны понимать, почему если в редактор кода VB IDE вставить вот такой код:
то среда автоматически исправит его на
Это не какая-то дополнительная логика по fancy-фикации кода, которую можно было бы закомментировать в исходниках самого VB и получить поведение, при котором среда не исправляла бы регистр, не удаляла бы ненужные пробелы, не привода бы по разному написанные идентификаторы к единому виду в плане регистра символов. Это не дополнительная фича. Это неизбежное следствие его архитектуры, и чтобы этого не было, нужно не отключить/закомментировать что-то в коде самой среды, а наоборот, нужно было бы написать очень много дополнительного кода.
VB IDE неизбежно удаляет лишние пробелы и приводит регистр символов ключевых слов к правильному, а регистр символов к единообразному ровно по той причине, что исходный код не хранится под капотом IDE как текст и как код, а хранится в интерпретированном (сразу же после загрузки кода или сразу же после ввода кода) виде, в бинарном виде — в виде BSCR, и в BSCR попросту не предусмотрено место под хранение числа избыточных пробелов и флагов исковерканности ключевых слов. BSCR компактен и не хранит лишней информации, а поскольку код, отображаемый в редакторе кода, всего лишь воссоздаётся по BSCR в момент отрисовки, он выглядит в fancy-фицированном/канонизированном виде.
Тем не менее, для некоторых конструкций в BSCR всё же предусмотрено хранение информации о числе пробелах, точнее об отступах:
As <typename>
) в объявлении переменных, констант и членов User-Defined-типов (структур), но не в объявлении аргументов и типа возврата процедур.Так что вот в таком коде избыточные пробелы (нужные для выравнивания и красивого оформления) убраны не будут — эти выравнивания запоминаются в BSCR:
Кстати говор, внимательный читатель должен задаться вопросом: если всем хорошо известно, что VB почти в любом месте кода разрешает воспользоваться символом переноса строки и вместо
написать
или даже
то как кодируется в BSCR-виде форма записи конструкции
End If
с использованием переноса или нескольких переносов, если учесть, что вся конструкцияEnd If
целиком кодируется одной единственной BSCR-сущностью [0x6B]?Разгадка такова: информация о переносах строки хранится в BSCR-представлении этой строки, но она хранится отдельно от «смысловой нагрузки». Для любой строки BSCR-представление её смысловой нагрузки записывается абсолютно независимо от того, были ли в этой строке переносы строки (хоть через каждое слово), или же строка была введена без единого переноса. Сведения о переносах (если они имели место) записывается в BSCR-цепочку отдельной сущностью [0xA6], вслед за которой идёт информация о местах в строке, где длинную строку при последующей реконструкции (для рендеринга на экран или сохранения в файл) нужно целенаправленно разбить на подстрочке и повставлять символы переноса строки (нижнее подчёркивание) при отображении. При этом «координаты мест разлома» запоминаются в не символах, а в токенах, поэтому тот факт, что происходит неминуемое и неизбежное удаление избыточных пробелов, не приводит к тому, что места разлома длинной строки на несколько строчек уползают в середину токенов и портят строку.
Но этого мало. Ну хорошо, пусть мы теперь точно знаем, что VB IDE никогда не хранит внутри себя исходный код открытого VB-проекта в виде текста, в том сыром необработанном виде, в каком его знает программись. VB IDE парсит код прямо в момент ввода и спазу же проводит значительную часть обработки и хранит строки кода в уже обработанном бинарном виде. Лишь в моменты, такие как необходимость нарисова код на экране, исходный код реконструируется (но не весь, а только в необходимом объёме).
Но как это мешает VB быть интерпретируемым языком, где среда по мере выполнения процедуру интерпретирует строку за строкой, просто на вход интерпретатору поступает не сырой текст, а частично обработанная BSCR-форма записи того же текста? В общем-то, такая гипотеза имеет право на жизнь, ведь BSCR не имеет отношения к выполнению кода, а имеет отношение только к его хранению, интерпретации, визуализации.
Но, увы, и эта гипотеза ни имеет ничего близкого с действительностью.
Когда пользователь (программист) осуществляет ввод очередной строки, помимо того, что из строки вычленяются метки, statement-ы анализируется и по ним строится PCR-дерево, выделяется комментарий, а затем строка переписывается в BSCR-форму, которая включает в себя информацию о метке в начале строки, информацию о местах переноса строки, смысловую нагрузку statement-ов, информацию о комментарии (если он есть) — помимо всего этого, создаются или модифицируются определённые блоки (большине структуры), если выясняется, что новоиспечённая строка модифицирует текущий scope, то есть если имеющиеся в ней конструкции относятся к объявлению начала новой процедуру, начала нового энума или user-defined типа.
Редактор кода в любой момент знает, какая строка кода к какому scope-у относится, поэтому когда какая-то существующая строка кода правится или в какое-то место модуля вставляется новая строка, редактор прекрасно знает, к какому scope-у относится это изменение.
У каждого такого блока есть dirty-флаг, и блок, описывающий процедуру не является исключением.
Что же происходит, когда пользователь VB IDE нажимает кнопочку Run? У VB есть два режима запуска проекта:
VB IDE (точнее движок EB/VBA — подробнее об этих терминах читайте тут) компилирует проект по-процедурно. В случае использования простой опции «Start», VB практикует ленивый и экономный до ресурса подход on-demand компиляции процедур, иначе называемый JIT-компиляцией. Он не пытается скомпилировать процедуру, пока кто-нибудь не попытается вызвать эту процедуру.
Это позволяет не компилировать процедуры, которые никто никогда не вызовет, и очень значительно сокращает временнУю задержку на запуск проекта из под IDE. Проект может быть гигантским и иметь очень много кода, но запуск проекта будет происходить сверхбыстро — и не только на современных компьютерах, но и на очень ограниченных компьютерах образца 90-х годов.
Как только происходит попытка вызвать процедуру, которая ещё не скомпилирована, VB быстренько компилирует её и спокойно продолжает работу проекта совершенно незаметно для программиста. Однако, если процедура, до которой дошло дело, имеет ошибку, из-за которой компиляция процедуры вообще невозможна (например: обращение к нигде не объявленной переменной при задействованной директиве
Option Explicit
). В этом случае on-demand подход к компиляции процедур перестаёт быть заметным для пользователя: ошибка становится очевидной не в момент нажатия кнопки «Start», а в момент, когда процедуру попытались вызвать.Я серьёзно полагаю, что именно это наблюдение, что в режиме «Start» некоторые серьёзные ошибки в процедурах «всплывают» только в момент захода выполнения внутрь процедуры, стал основанием для наивных людей считать, что VB IDE интерпретирует VB-код непосредственно в момент выполнения процедуры. Тот факт, что в момент запуска проекта на исполнение VB не находит таких ошибок, а обнаруживает их в последний момент, когда программа уже частично поработала, вкупе с тем фактом, что в момент приостановки работы проекта («пауза») можно серьёзно правит код, заставляет людей делать догадку, что «руки» у среды доходят до кода только когда код исполняется, а при запуске VB IDE ни коим образом не анализирует код и уж точно не компилирует его (иначе ошибка отлавилась бы в момент запуска?)
Но это совершенно ошибочная позиция. Причина неотлова подобных ошибок на ранних стадиях — это использовани on-demand/JIT подхода к компиляции. Это фишка, фича, а не недостаток или баг. Если же вместо опции «Start» (F5) запускать проект опцией «Start With Full Compile» (Ctrl+F5), on-demand подход использоваться не будет. VB IDE попытается скомпилировать абсолютно все процедуры, какие только есть в проекте, и найдёт все ошибки компиляции, какие только имеются, и уж точно не даст проекту начать кое-как работать, если хоть в одном месте есть compile error.
Просто это дольше и не так эффективно.
В настройках IDE существует опция «Compile On Demand» (чекбокс), которую можно снять, и в этом случае проект будет всегда компилироваться полностью перед запуском, а значит и полностью проверяться.
Я не зря выше упомянул, что у каждой процедуры (но в более общем случае — у каждого scope-а) есть своя структура, содержащая dirty-флаг. Если какая-то процедура была единожды скомпилирована, но с тех пор в её исходный код не вносилось никаких правок, то в перерывах между запусками проекта результат компиляции кода этой процедуры не теряется и не выбрасывается. Процедура, которая не менялась между запусками проекта, при перезапуске проекта не перекомпилируется повторно.
Теперь о том, что представляет собой процесс компиляции.
Как и другие схожие платформы и решения, такие, например, как Java, дотнет, PHP и т.п., VB располагает своим собственным байт-кодом (со своей собственной системой команд) и своей собственной виртуальной машиной, которая выполняет этот байт-код. По сравнению с кодом на языке VB этот байт-код является низкоуровневым, но по сравнению с машинными командами архитектуры x86 (или любой другой, под которую скомпилирован VB) он является весьма высокоуровневым, потому что одна инструкция машинного кода VB может делать работу, эквивалентную тысячам инструкций процессора, десяткам системных вызовов.
В терминологии самого VB его собственный машинный код называется P-кодом.
VB IDE никогда в принципе не занимается ни какой интерпретацией, в том значении, в каком этот термин актуален для интерпретируемых языков, а абсолютно всегда компилирует процедуру из BSCR-представления и служебных структур в P-код, причём целиком, и только затем выполняет P-кодное воплощение процедуры на своей виртуальной машины.
Даже команды, которые пишутся в Immediate Pane — и те сперва компилируются в P-код, и только затем этот P-код отдаётся на выполнение.
Что представляет собой P-код и как устроен процесс выполнения P-кода виртуальной машиной? Ведь можно предположить, что за P-кодом может скрываться некий код на чуток менее высокоуровневом языке, чем VB, но код, существующий в виде текста (например, как ассемблерный листинг), а виртуальная машина идёт по строчкам-инструкциям этого низкоуровневого листинга, интерпретирует их и пытается выполнить. В таком ключе исполнение кода VB-проекта, пусть он и скомпилирован в P-код, можно было бы всё равно называть интерпретацией, коль скоро P-код представляет собой текст и его нужно парсить и интерпретировать? Увы, но, к счастью, подобное предположение не имеет ничего общего с реальностью.
P-код представляет собой никакой не текст, а бинарный код, во многом похожий на машинный код x86 или большинства аппаратныз процессорных архитектур. P-кодные инструкции — это короткие последовательности байтов, начинающиеся с байта, кодирующего сам тип инструкции — с опкода — вслед за которым идут закодированные параметры команды P-кода. Определение количества и смысла параметров команды (инструкции P-кодной машины) зависит от опкода и выполняется в зависимости от его значения.
Выполнение P-кода виртуальной машиной устроено очень простым и эффективным способом:
tblByteDisp
.То есть виртуальная машина читает первый байт первой инструкции скомпилированной процедуры — он является опкодом, а дальше по значению этого байта берёт адрес из соответствующей ячейки таблицы и передаёт управление туда. Таблица устроена так, что для каждой инструкции в ней содержится адрес кода виртуальной машины (машинного кода, исполняемого аппаратно, то есть процессором), который отвечает за исполнение P-кодной инструкции.
Этот код делает всю специфичную для конкретно взятой P-кодной инструкции работу — извлекает и декодирует параметры инструкции (если они имеются, конечно), сдвигает ESI (который в рамках виртуальной машины используется как аналог EIP, но для инструкций виртуальной машины), выполняет полезную работу, подчищает за собой (если требуется), после чего (к этому моменту) оказывается, что ESI указывает уже не следующую инструкцию. В конце-концов обработчик инструкции делает
Этот код определяет опкод следующей (на данный моменит уже легитимно говорить «текущей») инструкции и передаёт управление на её обработчик. Если в нашем P-коде у нас было две одинаковых инстуркции подряд, то обработчиком следующей инструкции окажется этот же самый обработчик.
Подобно тому, как в архитектуре x86 для всех форм кодирования всех существующих инструкций не хватило 256 возможных комбинаций однобайтового опкода, так и в случае P-кода количество всех форм кодирования всех существующих P-кодных команд не умещается в набор из 256 возможных значений. По этой причине для некоторых команд используются однобайтовые опкоды, но существуют также и команды с двухбайтовым опкодом. В x86 в своё время для этого был выделен псевдо-префикс расширения набора опкодов 0Fh. В P-коде двухбайтовые опкоды начинаются на FBh, FCh, FDh, FEh, FFh. По этой причине помимо таблицы tblByteDisp с 256 элементами-указателями существует ещё большая таблица tblDispatch, в которой 256+256+256+256+70=1094 ячейки — таблица состоит из 5 подтаблиц, каждая из которых отвечает за префиксы FBh, FCh, FDh, FEh, FFh.
P-кодная виртуальная машина VB — стековая. У неё нет как таковой концепции регистрового файла или регистров, как, например, у x86. Все манипуляции над значениями она предпочитает выполнять на стеке. В этом смысле она чем-то похоже на идеологию работу инструкций сопроцессора x87 — его инструкции оперируют своим собственным стеком плавающих чисел. Если обычная инструкция ADD архитектуры x86 принимает два операнда, при этом оба операнда содержат слагаемые, а результат помещается в регистр или ячейку памяти, обозначенную первым операндом, то в случае P-кодной системы команд тоже есть инструкция, которая складывает два числа, но она вообще не принимает никаких явных параметров: она предполагает, что операнды уже лежат на стеке к моменту вызова команды — она извлекает из стека два числа, складывает их, и результат тоже кладёт на стек. При этом виртуальная машина VB в качестве стека использует обычный стек, тот же самый, которым манипулируют машинные инструкции общего назначения, тот, что предоставляемый потоку операционной системой, и указатель на который содержится в регистре ESP.
Поскольку VB — типобезопасный язык, который должен оградить программиста от выстрела в ногу, инструкция, складывающая два числа, не просто выполнит арифметику над числами, но и проверить совместимость типов, а также тот факт, не произошло ли в результате выполнения операции сложения переполнение, и если произошло — сгенерирует ошибку.
Например, опкод
FB 8E
соответствует инструкции AddUI1, которая выполняет сложение двух значений типа Byte. Реализация кода этой инструкции в виртуальной машине выглядит так:Первая инструкция извлекает из стека второе слагаемое, вторая инструкция не извлекая из стека первого слагаемого, сразу добивается того, что на верхушке стека будет лежать сумма двух слагаемых, которые были на стеке до выполнения этой инструкции. Третья инструкция проверяет, не произошло ли переполнение беззнакового 1-байтового числа, и если да — то выполнение улетает в код, генерирующий ошибку.
Следующие интрукции прочитывают опкод следющей инструкции в EAX, сдвигает указатель не текущий декодируемый байт (ESI), и, основываясь на значении опкода (в ргеистре EAX), выбирает подходящий обработчик следующей инструкции и передаёт туда управление.
В целом, все обработчики P-кодных инструкций работают схожим образом.
В данном примере показана целочисленная арифметика над типизированными переменными — это, пожалуй, самое простое из всех задач, которые могут стоять перед виртуальной машиной. Операции над Variant-переменными или объектами с использованием позднего связывания — вот примеры того, где за одной двухбайтной инструкцией P-кода может стоять сотня, а то и тысяча машинных инструкций процессора.
Изначально движок EB/VBA был устроен так, что компилирование VB-кода в P-код и его исполнение виртуальной машиной было единственным возможным способом работы. Как минимум, P-код давал такое преимущество, как кроссплатформенность: документ MS Office (например, книга Excel) содержащий макросы на языке VB, мог бы быть создан под ОС Windows на платформе x86, а затем открыт под версией MS Office для MacOS на платформе PowerPC — при этом код макросов работал бы там без перекомпиляции, поскольку один и тот же платформо-независимый байт-код выполнялся бы соответствующим билдом виртуальной машины под кокнертную аппаратно-программную платформу.
В случае Standalone VB, была добавлена возможность компиляции проекта не только в P-код, но и в Native-код (машинный код под целевую аппаратную платформу, главным образом 386+) — однако только при компиляции проекта в EXE-файл. В режиме отладки проекта под IDE проект, то есть его процедуры, всегда компилируется в P-код.
Когда-то давно, когда накопители были маленькими, P-код давал значительное преимущество в том, насколько компактными получались результирующие исполняемые файлы: там, где с использованием Native-кода потребовалось бы с полсотни инструкций, можно было обойтись одной P-кодной инструкцией. Платить за это приходилось производительностью: P-код был более медленным.
С современными процессорами с большими и быстрыми кешами данных и инструкций ситуация стала ещё более интересной: P-кодные билды стали часто обыгрывать Native-кодные билды по той причине, что одна и та же программа, один и тот же алгоритм в P-кодном воплощении становился намного компактнее и охотно умещался в кеше данных процессора. Наиболее часто используемые P-коды команды, точнее их Native-кодные реализации в составе виртуальной машины стали полностью умещаться в кеше инструкций: если для полностью Native-кодной программы, где какая-нибудь процедура вызывается впервые и имеет размер в 100 условных машинных инструкций, пришлось бы делать множество чтений из памяти, и к тому же в код этой процедуры гарантированно отсутствовал бы в кеше инструкций (поскольку процедура вызывается впервые), то для P-кодного вариантй той же процедуры её размер в P-кода составлял бы условные 10—15 P-кодных инструкций. Да, их тоже пришлось бы вычитывать из памяти, но количество чтений было бы на порядок меньшим, зато с большой вероятностью эти P-кодные инструкции вызывали бы исполнение уже неоднократно поработавших фрагментов с имплементацией работы этих P-кодных команд, уже попавших и в кеш инструкций, а потому выполняющихся намного быстрее, чем аналогичный по функциональности Native-код.
Подытожим:
Когда после осмысления и осознания всех этих фактов я вижу, как кто-то утверждает, что VB6 IDE или VBA IDE выполняет VB-код путём его построчной интерпретации прямо в момент выполнения процедур, буквально как какой-нибудь bash-интерпретатор интерпретирует шелл-скрипт (работая с ним как с текстом), у меня глаза наливаются кровью от злости.
Сосед с перфоратором — теперь и на Марсе.
Пробежавшись по абзацам про Thunder, Silver, Ruby, Алана Купера сложилось впечатление, что вы вдохновлялись вот этой статьей. Угадал?
Но мимо этого я не мог пройти:
Вы серьёзно? For Each Джоэль позаимствовал не из C-Sharp'а, а из CSH — скриптового языка для написания шелл-скриптов, который появился на 32 года раньше Шарпа. C# и csh это не одно и то же. Да и как можно было допустить мысль, что до си-шарпа такой конструкции ни в одном другом языке не было?
В конце концов, даже если не знать ничего про csh, как при создании Excel Basic'а (который позже стал VBA и VB), которое происходило на рубеже 80-х и 90-х, можно было позаимствовать что-то из C#, до появления которого оставалось ещё 10 лет? Это как писать, что Аристотель позаимствовал что-то из трудов Ленина.
Чьи требования?
До сих пор думаю, что это был такой «экспресс-тест на психопата и неадеквата»; при этом интересно, ко всем ли они применяют такой подход, или что-то в моём поведении или образе им сходу показалось подозрительным.
Уже забыл, как в точности провоцировался конфликт, но общая формула создания конфликтной ситуации примерно следующая:
2. Вы окидываете взглядом кабинет, и видите, что рядом со столом, за котором сидит врач и медсестра, нет стула, предназначенного для вас.
3. «Куда садиться?» — спрашиваете вы.
4. «Вот сюда» — вам показывают рукой на то место, где по идее должен стоять стул.
5. «Но здесь нет стула», возражаете вы.
6. «У тебя глаз или нет или как?» — надменно начинает хамить врачиха, перейдя на «ты».
7. «В смысле?»
8. «Ты сюда в кабинет заходил, не видел, что в коридорчике стоит стул?»
9. «Ну...»?
10. «Он тебе что? Не годится??? Или тебе какой-то особенный стул нужен?»
11. «Эээ....»
12. «Ну так пройди тиуда, возьми, переставь его сюда и сядь! В чём проблема? Или мы тебе ещё стулья должны подносить, как барину? Тут лакеев нет, знаешь ли?»
13. Но дальше хамские интонации отключаются и разговор продолжается в более-менее мирном русле.
По-моему ещё был какой-то аналогичный подкол на тему заполнения бумаг (вписать в бланк свои паспортные данные я должен был сам, они бы только подпись поставили), не помню к чему именно относилась фраза по лакеев, но сама фраза запомнилась очень хорошо.
В общем, спектакль заключался в том, чтобы максимально хамски и оскорбительно вести себя по отношению к «пациенту». Я сходу предположил, что прямо сейчас передо мной разыгрывается спектакль и что надо не поддаться на провокации. А так они ведут себя таким образом, чтобы по максимуму вызвать у тебя желание развернуться и, хлопнув дверью, уйти со словами «да подавитесь вы своей справкой».
После нескольких дежурных вопросов, включая вопрос «чем занимаетесь?», разговор продолжился в духе того, что а вот у моей дочки/подруги/сестры есть такие-то проблемы с компьютером. «Чем это может быть вызвано?».
Дальше набор симптомов компьютерной проблемы на ходу переиначивался, дополнялся новыми подробностями, задавались уточняющие вопросы, которые были всё глупее и глупее. Возможно замысел был в том, что если человек выдержал испытание хамством, он может «взорваться» на стадии «тупых и назойливых вопросов».
Исходники далеко не полные. Нет, к примеру, кода oleaut32.dll. Это, конечно, не относится к описанным здесь проблемам, но и части ядерных вещей недосчитаешься.
Про стюардессу не поддерживаю
Вы это про Windows или про nix-системы говорите?
Если про Windows, то что сейчас, что 20 лет назад, ничего подобного не практиковалось.
Когда вы выделяете 1 гигабайт памяти с помощью VirtualAlloc, создаётся структура MMVAD (она является узлом дерева, всё дерево хранит всю карту выделенных или зарезервированных областей виртуального АП отдельно взятого процесса, точнее «пользовательской» части всего АП), в которую заносятся параметры выделения (база, размер, параметры защиты страниц).
Никаких страниц физической памяти или файла подкачки, ассоциированных со страницами этого гигабайтного региона, в момент выделения памяти не выделяется.
Единственное, что есть такой счётчик как «page file quota», от которого минусуется количество выделенных страниц — этот счётчик не даёт всем процессам в сумме выделить больше страниц, чем теоретически может обеспечить файл подкачки.
Когда вы попробуете записать тот пресловутый 1 байт, произойдёт page fault, который ОС разрулит незаметным для процесса образом — странице виртуального АП, в которую был записан 1 байт, начнёт соответствовать некая страница физической памяти. Эта страница физической памяти будет выбрана из списка заблаговременно заготовленных занулённых страниц, а если таких готовых страниц на данный момент нет — будет занулена прямо сейчас (что дольше, чем если брать из простаивающих занулённых).
В случае, если ОС будет испытывать недостаток страниц физпамяти, страница физпамяти, соответствующая странице виртуального АП, в которую был записан 1 байт, будет сброшена в файл подкачки, а при необходимости затем подгружена.
Все остальные остальные страницы этого гигабайтного региона по прежнему будут существовать только номинального — не более, чем как числа в структуре MMVAD. Никаких страничных фреймов физической памяти или страничных фреймов файла подкачки им (без необходимости) соответсововать не будет.
Я прекрасно знаю об этом, но в XP поддержка физпамяти больше 4 Гб принудительно зарезана. Где только я говорил про PAE, не могу понять? В комментариях к этой публикации я PAE не упоминал — разве что в комментах к другим статьям мог где-то сказать про PAE в рамках развенчания мифа про лимит в 4 Гб для 32-битных систем, налагаемый, якобы, архитектурой.
Вот я сижу на 32-битной ОС, объём поддерживаемой физпамяти ограничен числом чуть меньшим, чем 4 Гб. При этом у меня огромный файл подкачки, размещённый на специально выделенном ради этого SSD-диске. Естественно у меня процессов, выдеряющих огромные объёмы, сумма которых больше, чем объём всей физпамяти на порядок.
Вам не приходило в голову, что и 16-битный софт под DOS кому-то может быть интересно или нужно реверсить хотя бы потому, что люди хотят функциональность перенести на более современные рельсы? Вот есть софт под DOS, который управляет станком, а хочется, чтобы был подо что-то современное. Но ни документации, ни исходников, ничего.
Понимаете мысль?
Это какая-то терминологическая демагогия пошла. Общепринято, что более гибкий и многозначный термин «память» используют часто как синоним понятия «адресное пространство».
В любом случае, вы написали
Можете сделать процесс, который резервирует 1 Гб (или вообще все свободные регионы) своего адресного пространства (после чего засыпает или зацикливается), и запустить 200 таких процессов. Никакие системные ресурсы не исчерпаются. Будет потрачено 200 фрагментов неподкачиваемого пула ядра на структуры MMVAD_SHORT. Ровно столько же было бы потрачено, если бы процесс закоммитил 200 несмежных 4-килобайтных страниц (800 кб в общей сумме).
Не могу говорить о всех операционных системах на свете, но к Windows ваши слова точно не применимы.
Во-первых, похоже, что вы путаете резервирование (
MEM_RESERVE
) и выделение (MEM_COMMIT
) страниц. Прочитайте статью об устройстве виртуальной памяти в Windows, которую tyomitch написал в далёком 2006 году. Если не хотите читать, то коротко: разница между резервированием и выделением страниц такая же, как между бронированием номеров в гостинице и заселением в них. Вы не сможете создать дефицит чистого постельного белья в городе, даже если забронируете все номера всех гостиниц в нём (но не станете никем из заселять).Во-вторых, даже если вы не резервируете, а именно выделяете какое-то количество страниц с помощью VirtualAlloc(), последняя лишь создаёт (или обновляет) структуру MMVAD_SHORT, описывающую диапазон страниц, но совершенно никак не обновляет таблицы страниц, не инициализируя новые PDE или PTE.
Инициализация/создание новых PDE/PTE происходит лишь при первом обращении к соответствующей странице. Когда происходит первый page fault при попытке доступа к ней. То есть каталог страниц обновляется только по мере необходимости (on demand).
Не верите: можете раздобыть утёкшие исходники ядра и прочитать от корки до корки исходники ядерной функции
NtAllocateVirtualMemory
(VirtualAlloc
лишь переходник к ней). Единственным исключением является вызовVirtualAlloc
с флагомMEM_PHYSICAL
.Огласите весь список, пожалуйста :) Если вы пишите «ошибки», значит их много? Отсутствие автосохранения — одна из них?
Замысел хороший, но сразу после появления плагина стал очевиден способ спасения данных, описанный в первой половине статьи (перенаправление выполнения на код процедуры
MudDoSave
из плагина), автосохранение не рассматривалось как нужная фича, без которой нельзя жить и на которую не жалко тратить время. Ну разве что против внезапного отключения электричества или BSOD-а, но таких случаев может и не было вообще.Но идея хорошая, я подумаю над автосохранением, правда я считаю, что идеологически неверно запихивать эту функциональность в плагин Markup Dumper — это должен быть отдельный плагин, какой-нибудь OllyAutosave.
Странный комментарий. Имеется в виду установить OllyDbg на живой диск заведомо, подозревая диск (на котором живёт том
H:\
) в нездоровости заранее? Или установить OllyDbg на другой диск уже после того, как диск с томомH:\
отвалился? Я надеюсь, что вы имели в виду второй вариант.Если так, то вы невнимательно читали статью.
Во-первых, как я по вашему реверсил
Insertname()
после аппаратного сбоя, если не установил OllyDbg на другой диск? Конечно же OllyDbg был сразу же установлен на другой том другого диска из резервного хранилища, чтобы начать работы по разрешению ситуации.Во-вторых, утилиту-дампер пришлось писать не потому, что OllyDbg стал недоступным для использования из-за отвала, а потому что был риск, что OllyDbg при подключении к полумёртвому процессу (коим был тоже OllyDbg) запнётся и завершится. А когда отладчик завершается, не вызвав перед этим
DebugActiveProcessStop()
, отлаживаемый процесс уничтожается вместе с процессом-отладчиком. Тогда бы я окончательно потерял спасаемые данные.Т.е. утилита-дампер писалась ровно из тех соображений, чтобы воздействие на «коматозный» процесс было минимально инвазивным. Четыре аккуратных и точных вызова
ReadProcessMemory
должно было стать меньшим из возможных зол. Я даже не был уверен, чтоReadProcessMemory()
вообще сработает в отношении этого процесса и сможет прочесть хоть что-нибудь. Это сейчас примерно понятно, что произошло, а в тот момент не было уверенности, что адресное пространство сохранилось хотя бы частично (в Windows объект-процесс может существовать, а адресное пространство как совокупность структур PDE+PTE с одной стороны, и MMVAD с другой стороны — могут не существовать; на каких-то этапах жизненного цикла объекта-процесса это так). Кто мог в первые минуты гарантировать, что всё произошло именно из-за аппаратного сбоя, а не потому, что какие-то ядерные структуры были повреждены каким-то глючным драйвером?Если вы внимательно читали, то видели, что сначала я собирался сдампить все закоммиченные страницы умирающего процесса, разобраться со сбоями, перезагрузиться, а потом уже в дампе находить нужное, не боясь, что вот-вот случится BSOD. Но Process Explorer не мог создать дамп, падал с ошибкой.
Причём было не удивительно, что он падал: если страница нечитаема, потому что механизм подкачки не может её подгрузить, то и ReadProcessMemory, которым пользуется любой дампер, её контент прочитать откуда-то волшебным образом не сможет. Был бы дампер, который читал не все подряд страницы. а только нужные — он бы не падал. Но нужно было знать, какие именно страницы читать, для чего и был начат реверсинг. А если знать, что читать нужно два больших блока плюс маленький кусочек с метаданными, зачем мне было искать подходящий дампер, если быстрее было написать его самому?
Если первое, то там масштабы такие, что не кассетницы нужны, а фидеры для pick-and-place-машин, отрезающие нужное количество деталей с катушки с лентой. А второе, то с их накрутками кто такую систему сможет себе позволить?
Да уж, стоило изобретать цифровую шину с поддержкой адресации, чтобы потом мучиться с матрицами.
Это не вам или автору камень в огород, это я просто об абсурдности ситуации.
Но ведь IDA Pro умеет присоединяться к существующему процессу и выступать отладчиком. Зачем тогда нужна Olly?
Дело в привычке. Я тоже пользуюсь Олей лет 15, и все действия в ней делаю уже как пулемёт. А IDA Pro всегда отталкивала нездоровой атмосферой вокруг Ильфака и его отношением к поддержке и пиратству. Окей, у вас платный продукт, а я воспользуюсь бесплатным инструментом, а для того, что он не умеет, сделаю собственные инструменты — вот так я примерно всегда размышлял.
Аннотации не влияют на вероятность зависания и вылета. Правда, длинные аннотации, создаваемые плагинами в нарушение спецификации, могут провоцировать вылеты, об этом я написал.
Просто если не аннотировать, вылеты не так критичны и не воспринимаются, как маленькие трагедии.
А зависания, как я уже писал, происходят по вине
win32k.sys
, в основном. Например, жмёшь в Olly Ctrl+C и она зависает намертво. Смотришь Process Explorer-ом бэктрейс основного потока — там вызовSetClipboardData
из OllyDbg, из неё идёт вызовuser32!_NtUserSetClipboardData@12
, а это лишь переходничок, делающийSYSENTER
. Дальше код уже продолжается в режиме ядра. И где-нибудь там оно висит. Аннотации тут явно не причём. Тут виноваты какие-то system-wide объекты синхронизации. Тот же буфер обмена — если один процесс открыл его с помощьюOpenClipboard()
и завис, другие процессы сессии не могут работать с буфером обмена.В подобных случаях не не переключался на отладку ядерной части системы — а то это могло зайти далеко. Был у меня случай, когда я реверсил «железный» музыкальный синтезатор фирмы Roland, и использовал цифровой осциллограф, управляемый с компьютера через SCPI (GPIB), чтобы программировать его на отлов нужных событий, автоматизированно захватывать их анализировать. В реализации SCPI осциллографа была найдена дыра, позволяющая получаь доступ к прошивке осциллографа. Реверс-инжиниринг синтезатора плавно перетёк в реверс-инжиниринг осциллографа. В итоге ни то, ни другое не было доведено до конца.
Спасибо. Есть ли смысл публиковать плагин Markup Dumper, обозревать его применение в контексте ведения распределённого командного реверсинга? Есть смысл публиковать статью об упомянутых ARG-файлах?
Собственно говоря, это не эфемерная мечта: у меня уже давно (лет 14 как) лежит написанный классный и быстрый дизассемблирующий/ассемблирующий движок, который может удобно расширяться. Есть к дизассемблирующей половинке этого движка GUI-примочка, показывающая отдельную процедуру либо в плоском виде, как это делает OllyDbg (даже интерфейс скопирован один в один), либо в виде графа, как делает IDA. Есть самописный простенький отладчик (tinydbg), правда у него не графический, а совершенно дубовый текстовый интерфейс (не с использованием псевдографики в консольном окне, а именно сугубо текстовый «диалог» с отладчиком) — его единственное преимущество в том, что антиотладочные приёмы, которые основываются на слабых местах популярных отладчиков, на него не действуют, а так же тем, что любую нестандартную команду или функцию я в него могу добавить сразу же, как возникнет такая необходимость, и мне не надо будет искать нужный плагин или придумать, как ограниченным инструментарием скриптинга реализовать какую-нибудь пошаговую деобфускацию отлаживаемого процесса. Есть собственный вьювер DBG/PDB-файлов, собственный вьювер COFF.
Остаётся только объединить кучу собственных разрозненных и сырых утилит в мощный и доделанный конгломерат.
И конечно, я говорю сейчас о собственном интересе. Так сказать, с точки зрения научного интереса было бы интереснее написать собственный инструмент. Список мастхев фич сформирован уже давно.
А вот с точки зрения практической, конечно, более перспективно возраждать или поддерживать инструмент, у которго уже есть имя, репутация, комьюнити и своя экосистема, чем пробивать дорогу чему-то совершенно новому.
Непонятно, почему автор OllyDbg не обернул все обращения из хоста в плагины в
__try ... __except
и сам не сделал какого-нибудь аварийного автосохранения в фильтре необработанных исключений. Даже VirtualDub имеет какой-то нестандартный top-level хендер для исключений, показывающий и контекст потока, и дизасм проблемного места, хотя, казалось бы, мультимедийной утилите такой функционал не обязателен, а вот в коде отладчика уже есть многий инструментарий для реализации подобной плюшки, но самой плюшки и вообще хоть какого-то аварийного процессинга исключений внутри самого себя — нет.Вообще, писать подобный плагин-автодампер для себя я не вижу особого смысла. У меня есть пара утилит, упомянутых в статье:
memdumper.exe
иextractor.exe
. В случае проблемы достаточно вызватьУ этого подхода есть один неоспоримый плюс: спасительные утилиты находятся вне зоне поражения сошедшего с ума кода. А вот плагин-спасатель может сам оказаться повреждён, ведь он разделяет с умирающим отладчиком одно адресное пространство.
А в первой версии такой API нет вообще. Но даже если бы была, я ведь упомянул, что есть за OllyDbg грешок тихо портить UDD при сохранении.