§ 12. Сколько строк кода и файлов в исходниках самого VB?

Внутренний мир Visual Basic. Раскрываем тайны глубин VB, секреты устройства, недокументированные возможности и сведения.

Модератор: Хакер

Хакер
Телепат
Телепат
Аватара пользователя
 
Сообщения: 16324
Зарегистрирован: 13.11.2005 (Вс) 2:43
Откуда: Казахстан, Петропавловск

§ 12. Сколько строк кода и файлов в исходниках самого VB?

Сообщение Хакер » 30.11.2020 (Пн) 4:01

Есть множество способов оценить сложность или трудоёмкость создания программного продукта, и один из них — по количеству строк исходного кода. Можно спорить о том, насколько хорош и объективен этот способ, но нельзя отнять, что, имея представление о среднестатистической «смысловой плотности» кода на определённом языке, можно делать сравнения и выводы, используя информацию о количестве строк.

Наряду с вопросом о том, на каком языке или языках написан сам VB, вас никогда не волновал вопрос о том, сколько же кода содержится в исходниках самого VB? 50 тысяч? 200 тысяч? 500 тысяч? Миллион? 10 миллионов? Сколько ресурсов, помимо планирования и придумывания архитектуры, было потрачено на создание VB? Исходники, как известно, закрыты, и количество кода в них — тайна. Но эта статья приоткроет завесу над этой тайной.

Мы не будем взламывать серверы Microsoft и красть исходники VB, а возьмём эти сведения из открытых источников — из файлов, которыми Microsoft сама поделилась со всеми — из файлов отладочных символов.

Прежде всего, об этих файлах нужно сказать две важные вещи:
  • Широкой общественности доступны только VB6.DBG и MSVBVM60.DBG. Отладочные символы для VBA6.DLL нам недоступны, что создаёт значительную «слепую зону», потому что приличная часть кода VBA/EB попала только в VBA6.DLL, но не попала в MSVBVM60.DBG.
  • Способ хранения сведений в DBG-файлах — это не систематизированная стройная структура, а несколько наборов отдельных отрывочных сведений, между крупицами которых нет явной связи, и эти разрозненные порции полезной информации не так-то просто собрать в общую картину. В конце статьи будут подробно описаны проблемы и сложности с DBG-файлами.

Предполагается, что вы читали §10 «Ruby + EB = Visual Basic» и знаете о том, что такое Ruby и EB. Если так, то вы понимаете, что отладочными символами покрыто 100% кода Ruby, и лишь часть кода EB — та, которая в процессе линковки попала в msvbvm60.dll, в то время как о коде, входящем лишь в состав vba6.dll сведений у нас нет. И точно так же, если вы читали ту статью, вы понимаете, что некорректно говорить об «исходниках рантайма» и «исходниках IDE», поскольку нет никаких отдельных «исходников рантайма» и «исходников IDE», а есть «исходники Ruby» и «исходники VBA(EB)», из которых в разных комбинациях собираются рантайм и IDE.

Поэтому, с учётом того, что частью данных мы не располагаем, ответить можно лишь на такие вопросы:
  • Сколько всего кода в исходниках Ruby?
  • Сколько всего кода в исходниках, необходимых для компиляции и сборки VB6.EXE?
  • Сколько всего кода в исходниках, необходимых для компиляции и сборки MSVBVM60.DLL?

Наши оценки можно назвать минимальными, в том смысле, что они дают числа с оговоркой «как минимум — столько», подразумевая, что в реальности объём кода мог быть ещё большим, но уж точно не меньшим, чем числа, которые приведены далее. Есть и другая причина, почему это минимальная оценка, которая будет рассмотрена ближе к концу статьи под заголовком «Методика подсчёта или какой код учитывался?»

Итак, объём всех файлов исходников, требуемых для компиляции...
  • MSVBVM60.DLL — 446 тыс. строк в 319 файлах
    Из них:
    313 тыс. строк (70%) — файлы Ruby (201 файл)
    133 тыс. строк (30%) — файлы EB (118 файлов)

    • VB6.EXE — 467 тыс. строк
      Из них 99.9% — файлы Ruby,

    Объёмы файлов по компонентам:
    • Все файлы Ruby — 474 тыс. строк в 311 файлах
      Из них:
      305 тыс. строк (64%) — участвующие (193 файла) и в сборке MSVBVM60.DLL, и в сборке VB6.EXE
      131 тыс. строк (34%) — участвующие (109 файлов) только в сборке VB6.EXE
      8.6 тыс. строк (2%) — участвующие (9 файлов) только в сборке MSVBVM60.DLL
    • Файлы EB, участвующие в сборке MSVBVM60.DLL — 133 тыс. строк в 118 файлах

    Объём всех файлов исходников, хоть раз «засветившихся» в отладочных символах (всех) — 608 тыс. строк кода.

    Как уже упоминалось выше, эти числа скорее отражают нижнюю границу оценки размера файлов, а вовсе не верхнюю. Самое время поговорить о проблемах и погрешности методики оценки объёма исходников по DBG-файлам, чтобы понять, насколько верхняя граница может быть далека от нижней.

    Слабые места DBG-файлов
    (Осторожно! Следующий далее текст может оказаться очень сложным для восприятия. Если вы не готовы к такому погружению в тему, переходите к главе «Методика подсчёта или какой код учитывался?»)

    Формат файлов с отладочной информацией типа DBG — совсем не подарок. Важно отметить, что помимо DBG существует также формат PDB, но сейчас речь не о нём. Коротко формат DBG можно описать как структурированный способ хранения нескольких неструктурированных куч слабо структурированной информации. Неопределённость полноты информации в файле и слабая степень связанности сущностей делает DBG-файлы максимально неудобными для оценки размеров исходников.

    Один DBG-файл представляет собой скопление отладочной информации, относящейся к одному исполняемому файлу (EXE или DLL). При первом рассмотрении формат DBG-файла оказывается простым:
    • Заголовок DBG-файла, хранящий некую базовую информацию о самом файле и об ассоциированном исполняемом файле. Например это дата создания и контрольная сумма, а также информация, позволяющая найти в файле те или иные структуры, описанные далее.
    • Копия таблицы секций из исполняемого файла, к которому данный DBG-файл относится.
    • Зона со всевозможными строками нефиксированной длины, разделёнными 0-символом, на которые возможны ссылки из других структур.
    • Таблица директорий отладочной информации — по сути перечень больших разнородных разноформатных кусков отладочной информации с указанием того, где в DBG-файле начинается блок такой информации, где заканчивается, и какого типа информация в этом куске (директории) содержится.

    Фактически вся интересная и полезная информация содержится в директориях отладочной информации. Директории можно сравнить с разделами или главами книги. Регламентировано несколько (не более 10) типов директорий отладочной информации. При этом не устанавливается, директории каких типов обязаны присутствовать в файле (а какие опциональны), в каком порядке должны следовать директории и как быть, если директория одного и того же типа появляется в файле более одного раза.

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

    Наибольшую полезность представляет директория отладочной информации, известная как директория COFF-символов. Обычно эта директория является самой первой из всех директорий в DBG-файле, если она вообще есть (её может и не быть). Именно из-за директории COFF-символов файл с отладочной информацией называют файлом отладочных символов.

    Важно понимать, что само по себе понятие COFF-символа берёт начало из объектных файлов формата COFF. Это те самые файлы c расширением .obj, которые производит на свет компилятор, и которые принимает на вход линкер (он же компоновщик).

    Линкер принимает на вход один или множество объектных файлов (формата COFF с расширением .obj) и на выходе выдаёт один исполняемый файл (формата PE с расширением .exe или .dll или любым другим), иногда выдавая бонусом к нему вспомогательные файлы (например .exp и .lib). По своей сути COFF-файл представляет собой таблицу секций, где под секцией понимается блок байтов произвольного размера, являющийся либо куском скомпилированного кода (машинного кода), либо блоком произвольных данных, и таблицу символов, главным образом перечисляющую особые места в рамках секций — либо те места, в которых нужно подставить адрес какой-то сущности из какого-то другого объектного файла, либо те места, в которых находится именованная сущность (например функция или переменная), на которую где-то могут ссылаться из другого объектного файла (или даже из нашего же). Линкер же по сути сшивает (буквально склеивает конкатенацией) одноимённые секции из разных входных объектных файлов, получая, как правило, небольшой набор крупных секций, а затем проходится по всем местам, где должны быть ссылки на именованные сущности и проставляет корректные адреса, ориентируясь в этом деле на таблицу символов. К результату добавляется PE-заголовок, и исполняемый файл, упрощённо говоря, готов.

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

    Когда линкер производит линковку, содержимое таблиц символов отдельных входных COFF-файлов проходит жесткую цензуру, частичную перегруппировку, и то, что остаётся после этого, образует таблицу COFF-символов директории COFF-символов DBG-файла, который при определённых условиях формируется именно линкером как побочный продукт формирования исполняемого файла (EXE или DLL).

    Именно в силу этих обстоятельств таблица символов в составе DBG-файлов представляет собой сильно искажённый и урезанный результат слияния отдельных таблиц символов из COFF-файлов, участвовавших при линковке. Линкер пропускает в DBG-файл только те символы, которые представляют ценность с точки зрения применения для целей отладки, хотя эти крупицы информации являются второстепенными по своей роли в момент создания OBJ-файла, и их информационная ценность в значительной мере обусловлена тем, что в оригинальной таблице символов та или иная второстепенная сущность соседствует с первостепенной сущностью. После линкерной «цензуры» и перегруппировки первостепенные символы либо вообще исчезают из производной таблицы символов, либо меняют свой порядок следования, и логическая связь между символами, условно названными «первостепенными» (то есть теми, которые кодируют информацию о сопоставлении имени сущности и её положения в файле, и без которых линковку осуществить невозможно) и символами, несущими крупицы отладочной информации, утрачивается.

    Что же такое символы в таблице COFF-символов, особенно в контексте DBG-файла? Символы — это набор гетерогенных крупиц информации, перечисленных в определённом порядке. Существует несколько подтипов символов, и в зависимости от подтипа (класса) символа содержимое каждой конкретной записи таблицы символов может интепретироваться совершенно разными образами.

    Записи одного типа несут в себе имя отдельно взятого исходного файла. При этом в записи нет никакого намёка на то, к какому месту исполняемого файла относится указанный исходный файл, и нет никакой информации о сущностях (например функциях), имеющих отношение к данному исходному файлу. Записи другого типа несут в себе сопоставление определённому адресу, по которому находится некий машинный код, номера строки из файла-исходника, соответствующей в скомпилированном виде инструкции по указанному адресу. При этом в записях такого типа есть только информация о номере строки, но нет информации об имени файла, в котором находится эта строка — ни прямой, ни в виде какого-то упоминания или отсылки к записи первого типа (тем записям, что несут в себе имя файла). Также в этих записях нет никакой информации о том, к какой процедуре относится это место — только соответствие адреса и номера строки. Записи третьего типа несут в себе информацию о сопоставлении определённому адресу определённого имени. В объектных файлах (OBJ-файлах) записи третьего типа и первого типа идут в строго логически связанном порядке, что позволяет установить для каждой процедуры имя исходного файла, в котором она реализована, а для каждого файла — список процедур, реализованных в нём. Однако при формировании символов DBG-файла из символов OBJ-файла записи символов третьего типа переносятся ближе к концу таблицы символов и связь между именами файла и обитающими в них символами, которая могла быть определена только исходя из порядка следования (поскольку символы никак не ссылаются друг на друга), разрушается.

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

    Однако, поскольку при формировании таблицы символов DBG-файла из таблиц символов OBJ-файлов происходит значительная потеря и искажение информации, возникает две нетривиальных проблемы:
    1. Хотя все процедуры одного исходного файла после компиляции могут в выходном OBJ-файле попасть в одну единственную общую секцию (.text или .code), существует более продвинутый режим работы компилятора (для компилятора С/С++ от Microsoft такой режим включается ключом командной строки /Gy), при котором каждая процедура или переменная помещается в отдельную секцию особого типа (так называемую COMDAT-секцию). Хотя все такие более мелкие секции имеют одинаковые имена (например .text для процедур или .data для переменных) и в процессе линковки всё равно в итоге сшиваются линкером в большую единую секцию (по признаку одноимённости), помещение компилятором каждой отдельно взятой процедуры в свою персональную секции позволяет линкеру применить два оптимизационных трюка:
      • При склеивании одноимённых секций отбрасывать COMDAT-секции, в которых находятся процедуры или переменные, которые в программе нигде и никогда не используются, то есть к которым нет обращения из других мест — таким образом неиспользуемый код и данные не попадают в итоговый исполняемый файл. Эта оптимизация включается ключом линкера /OPT:REF (включена по умолчанию), а выключается ключом /OPT:NOREF.
      • Объединять COMDAT-секции с идентичным содержимым, например COMDAT-секции, соответствующие разным исходным процедурам, по совпадению дающим после компиляции одинаковый машинный код, либо идентичные секции, содержащие одинаковые константные данные. Таким образом из исполняемого файла устраняется дублирующаяся информация. Эта оптимизация, называемая COMDAT folding, включается ключом /OPT:ICF, а запрещается ключом /OPT:NOICF.
      Когда линкер производит устранение неиспользуемых COMDAT-секций, в таблице символов DBG-файла записи первого типа (с упоминанием имени исходного файла) сохраняются, в то время как записи второго и третьего типа пропадают. Когда линкер производит устранение дублирующихся COMDAT-секций, записи первого типа сохраняются, в то время как запись второго типа остаётся только одна, а записи третьего типа сохраняются все, однако все они начинают ссылаться на один и тот же адрес. Таким образом сопоставление между именами процедур и номерами строк, изначально существующее по принципу один-к-одному превращается в соответствие типа один-ко-многим — один адрес ко многим именам.

    2. Если говорить о компилировании исходников на языке C/C++, такие исходники могут иметь в своём составе директиву #include, позволяющую включить в родительский исходный файл содежимое дочерних исходных файлов — обычно это используется для подключения заголовочных файлов.

      Для простоты можно рассмотреть надуманный пример исходников некоторой компьютерной игры, состоящей из исходных файлов
      • menu.cpp
      • gameplay.cpp
      , каждый из который include-ит заголовочные файлы:
      • video.h
      • audio.h


      Если заголовочные файлы содержат в себе не только объявления (типов, макросов), но и реализацию функций, то при компиляции исходников OBJ-файле символы в таблице символов будут присутствовать в следующем порядке:
      Код: Выделить всё
      [ТИП 1] упоминание файла menu.cpp
      [ТИП 2] Адрес 0000 = строки с номерами 10, 11, 12, 13
      [ТИП 3] Адрес 0000 = имя DisplayGameMenu
      [ТИП 2] Адрес 0015 = строки с номерами 15, 16, 18, 20
      [ТИП 3] Адрес 0015 = имя HideGameMenu
      [ТИП 2] Адрес 0021 = строки с номерами 724, 725
      [ТИП 1] упоминание файла audio.h
      [ТИП 3] Адрес 0021 = имя IsAudioSupported
      [ТИП 1] упоминание файла menu.cpp
      [ТИП 2] Адрес 0047 = строки с номерами 22, 23, 24, 25, 26, 27, 28
      [ТИП 3] Адрес 0047 = имя DrawMenuItems


      Код: Выделить всё
      [ТИП 1] упоминание файла gameplay.cpp
      [ТИП 2] Адрес 0000 = строки с номерами 16, 17, 18, 19, 20, 21
      [ТИП 3] Адрес 0000 = имя RenderScene
      [ТИП 2] Адрес 0084 = строки с номерами 30, 32, 33
      [ТИП 3] Адрес 0084 = имя PrintFPS
      [ТИП 2] Адрес 0093 = строки с номерами 327, 328, 329
      [ТИП 1] упоминание файла video.h
      [ТИП 3] Адрес 0093 = имя CheckMonitorIsWidescreen
      [ТИП 1] упоминание файла audio.h
      [ТИП 2] Адрес 0098 = строки 932, 940
      [ТИП 3] Адрес 0098 = имя Check3DSoundSupported
      [ТИП 1] упоминание файла gameplay.cpp
      [ТИП 2] Адрес 0102 = строки с номерами 35, 36, 37, 38
      [ТИП 3] Адрес 0102 = имя DrawExplosion

      В таком виде, проанализировав таблицу символов первого OBJ-файла, можно определить, что функции DisplayGameMenu, HideGameMenu и DrawMenuItems находятся в файле menu.cpp на соответствующих строках, в то время как IsAudioSupported родом из audio.h, поскольку в её случае записи третьего типа предшествует запись первого типа с указанием именно этого файла.

      Проанализировав таблицу символов второго OBJ-файла, можно определить, что функции RenderScene, PrintFPS и DrawExplosion реализованы в gameplay.cpp, тогда как CheckMonitorIsWidescreen родом из video.h, а Check3DSoundSupported — из audio.h.

      Однако после линовки этих двух OBJ-файлов вместе с образованием исполняемого файла с одной стороны и DBG-файла с другой стороны, таблица символов, попавшая в DBG-файле оказывается сильно искажена:
      Код: Выделить всё
      [ТИП 1] упоминание файла menu.cpp
      [ТИП 2] Адрес 1000 = строки с номерами 10, 11, 12, 13
      [ТИП 2] Адрес 1015 = строки с номерами 15, 16, 18, 20
      [ТИП 2] Адрес 1021 = строки с номерами 724, 725
      [ТИП 1] упоминание файла audio.h
      [ТИП 1] упоминание файла menu.cpp
      [ТИП 2] Адрес 1047 = строки с номерами 22, 23, 24, 25, 26, 27, 28
      [ТИП 1] упоминание файла gameplay.cpp
      [ТИП 2] Адрес 1072 = строки с номерами 16, 17, 18, 19, 20, 21
      [ТИП 2] Адрес 1156 = строки с номерами 30, 32, 33
      [ТИП 2] Адрес 1165 = строки с номерами 327, 328, 329
      [ТИП 1] упоминание файла video.h
      [ТИП 1] упоминание файла audio.h
      [ТИП 2] Адрес 1170 = строки 932, 940
      [ТИП 1] упоминание файла gameplay.cpp
      [ТИП 2] Адрес 1174 = строки с номерами 35, 36, 37, 38
      [ТИП 3] Адрес 1015 = имя HideGameMenu
      [ТИП 3] Адрес 1021 = имя IsAudioSupported
      [ТИП 3] Адрес 1047 = имя DrawMenuItems
      [ТИП 3] Адрес 1156 = имя PrintFPS
      [ТИП 3] Адрес 1072 = имя RenderScene
      [ТИП 3] Адрес 1165 = имя CheckMonitorIsWidescreen
      [ТИП 3] Адрес 1170 = имя Check3DSoundSupported
      [ТИП 3] Адрес 1174 = имя DrawExplosion

      Здесь записи третьего типа переехали в конец таблицы. При этом хотя и сохраняется информация о том, что файлы video.h и audio.h использовались при компиляции menu.cpp и gameplay.cpp, информация о том, какие именно функции берут своё начало из этих файлов — потеряна. Более того, можно подумать, что функция по адресу 1165 (CheckMonitorIsWidescreen) находится в файле menu.cpp, потому что запись второго типа для её адреса идёт до записи первого типа для файла video.h. Но на самом деле порядок следования имеет значение только для типа 1 и 3: записи типа 3, содержащей имя сущности для адреса, будет всегда предшествовать запись типа 1, содержащая имя файла, в то время как запись типа 2, содержащая информацию о номерах строк, может идти как до, так и после записи типа 1. Записи типа 2 отражают принадлежность сущностей к файлам только приблизительно (с погрешностью на одну позицию), а записи типа 3, которые могут сообщить принадлежность сущностей к разным файлам достаточно достоверно, вынесены в конец таблицы с потерей ассоциативной связи с записями первого типа.

      В сложившихся обстоятельствах в таблице COFF-символов DBG-файла оказывается некоторое число записей первого типа, анонсирующих участие заголовочных файлов в процессе компиляции, для которых или сложно, или же вообще невозможно определить перечень содержащихся в них процедур. Ряд процедур, если придерживаться строго позиционного суждения, оказываются ошибочно ассоциированы не с теми файлами, в которых они реально располагались, и лишь анализ прогрессии номеров строк позволяет сделать необходимые корректировки, потому как записи типа 2 сохраняют свой относительный порядок в пределах группы записей, соответствующей одному входному OBJ-файлу, и этот порядок соответствует порядку появления функций в исходном файле, то есть порядку, в котором их встречал компилятор. В норме эта прогрессия должна быть строго возрастающей, и наличие в ней резких скачков с последующими возвратами позволяет предположить, что процедура, номера строк которой выбиваются из общей канвы, принадлежит не тому файлу, который был анонсирован в непосредственно предшествующей записи первого типа, а файлу, который либо будет анонсирован непосредственно после записи второго типа для данной процедуры, либо был анонсирован в одной из тех записей первого типа, вслед за которой сразу же шла ещё одна запись первого типа, и таким образом, более ранний из этих двух файлов оказался «холостым».

    Помимо этого, записи типа 2 присутствуют в таблице символов (как в OBJ, так и в DBG) лишь для процедур. Переменные снабжаются лишь записями типа 3 (сопоставляющих адресу имя переменной), и поскольку все записи типа 3 переносятся в конец таблицы и теряют всякую ассоциацию с записями типа 1 (анонсирующими имена исходных файлов), сопоставить переменные с именами исходных файлов становится напрямую невозможно. Этот факт, однако, хоть и сам по себе сильно ухудшает информативность DBG-файлов, для целей анализа объёмности исходных файлов особого время не приносит, потому что обычно переменные объявлены в исходниках в самом начале файла, либо в середине, а судить о размере файла можно по последней процедуре (процедуре с максимальным номером строки из списка строк, составляющих код этой процедуры).

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

    Однако, для исполняемых файлов, порождённых на свет в недрах компании Microsoft, эти утверждения не верны без дополнительных уточнений. Для обычных условий компиляции и линковки можно сказать, что выполняются следующие утверждения:
    • Порядок следования функций в OBJ-файле после компиляции соответствует порядку функций в исходнике.
    • В исполняемом файле функции расположены группами в таком порядке, что порядок следования функций внутри групп соответствует порядку следования функций в отдельно взятых исходных файлах, а порядок следования групп соответствует порядку, в котором OBJ-файлы были перечислены при вызове линкера.
    • При этом, если исходный файл был скомпилирован без ключа /Gy (в случае C/C++), в группе процедур, соответствующей этому исходному файлу, будут присутствовать все без исключения функции, реализованные в исходном файле, и именно в том порядке, в котором они реализованы в исходном файле. Если же исходный файл был скомпилирован с ключом /Gy, некоторые процедуры, не использованные в программе, либо же те, которые в своём воплощении в машинном коде совпадают с какими-то другими процедурами, могут быть вырезаны, но в остальном линкер не изменяет порядок следования процедур и вшитых в исполняемый файл структур данных, оставляя тот порядок, в котором входные файлы были поданы на вход линкеру, и в котором сущности располагались в каждом из файлов. Однако порядок следования может быть переопределён тем, кто запускает линкер, при помощи ключа /ORDER:...
    • Компилятор C/C++ никогда не дробит процедуры на куски и не перемешивает куски между собой так, что куски одной процедуры оказывались бы не смежными друг с другом. В объектном файле порядок следования участков машинного кода разных процедур соответствует порядку следования исходного кода этих процедур в исходном файле, и подобные участки не фрагментированы и не сменяют друг друга. Фрагмент одной процедуры в объектном файле (содержащем машинный код) не может появиться между фрагментами других процедур. Множество машинных команд, соответствующих одной процедуре, будет располагаться в объектном файле смежно друг с другом — одной непрерывной группой машинных команд, внутри которой не могут появляться инструкции процессора (машинные команды), принадлежащие другой процедуре.
    • Линкер, в свою очередь, хоть и может выбрасывать или переставлять местами COMDAT-секции, содержащие процедуры, переставляет их как цельные неизменные блоки (за исключением коррекции адресов) и никогда не меняет порядок следования инструкций внутри процедур, поэтому принцип смежности кода процедур, соблюдаемый компилятором, не нарушается и линкером и сохраняется после линковки.

    Однако Microsoft в процессе сборки своих собственных продуктов (а VB6, без сомнения, относится к их числу), помимо типичной стадии компиляции исходных файлов в OBJ-файлы и последующей стадии линковки множества OBJ-файлов в исполняемый файл (EXE или DLL), производит ещё одну очень специфичную стадию.

    Я назвал эту секретную и практически нигде и никем не упоминаемую пост-обработку исполняемых файлов термином «легофикация», поскольку есть основания полагать, что именно так это мероприятие называется в недрах Microsoft (по видимому, название пошло от название детского конструктора Lego). Суть легофикации состоит в том, что после линковки исполняемый файл разрезается на огромное число мелких кусочков и эти кусочки тотально перемешиваются определённым образом (с сохранением валидности абсолютных и относительных адресов в составе файла, разумеется). Доподлинно неизвестно, выполняется ли легофикация отдельной утилитой, вызываемой вслед за командой LINK, либо же легофикация является частью неофициальной, никогда не попадавшей в руки широкой публике внутренней версией линкера (что тоже похоже на правду, потому что позже похожее мероприятие появилось в Microsoft-овском линкере под названием Profile-Guided Optimization).

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

    Тотальной перегруппировке подлежат не только целые процедуры и их фрагменты: перегруппировываютя также и вшитые в исполняемый файл структуры данных и переменные, а также служебные структуры PE-файла, такие как таблицы импорта и экспорта.

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

    Типичное проявление последствий легофикации — во всех процедурах все проверки на предмет ошибки переделываются так, что все участки кода, отвечающие за обработку ошибок, выносятся далеко за пределы основной части процедур, поскольку ошибки происходят редко. Процедуры, которые часто вызывают друг-друга, после легофикации оказываются рядом, в пределах одной страницы памяти.

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

    К счастью, утилита, производящая легофикацию, добавляет в DBG-файл две дополнительные директории отладочной информации (которые никогда нельзя встретить в DBG-файле, полученном в результате линковки штатным образом, т.е. вне Microsoft) — так называемые директории OMAP_FROM_SRC и OMAP_TO_SRC, содержащими таблицы преобразования адресов до легофикация в адреса после легофикации и наоборот.

    Однако, нужно учитывать, что одному фрагменту образа до легофикации может соответствовать несколько фрагментов после легофикации (порождение дублей), либо же напротив легофикация подобно процедуре COMDAT Folding линкера может множеству фрагментов до легофикации сопоставить один фрагмент после легофикации. Большие фрагменты легофикация может вырезать. Некоторые фрагменты легофикация может наоборот породить, например череду машинных команд (инструкций процессора), идущих в оригинальном (пре-лего) образе подряд, легофикатор может разрезать на фрагмент, разнести эти фрагменты по образу в разные места, и вставить jmp-инструкции, передающие управление между фрагментами. Таким образом, не стоит ждать от легофикации сохранения соответствия фрагментов по принципу один-к-одному. В большинстве случаев это так, но в некоторых случаях фрагменты могут соответствовать по принципу один-ко-многим, многие-к-одному, либо фрагменту из образа до легофикации может вообще ничего не соответствовать после легофикации.

    Все эти факторы делают анализ DBG-файлов максимально неудобным и неприятным занятием. Тем не менее, аккуратный подход и применение эвристических методик позволяет выжать из него огромный объём полезной информации.

    Примечания:

    1. Говоря о том, что файлы формата COFF — это именно те файлы с расширением .obj, которые производит на выходе компилятор и которые ожидает на входе линкер, имеются в виду компилятор и линкер производства Microsoft, которые внутри Microsoft использовались для компиляции и сборки исполняемых файлов, образующих VB6. Другие компиляторы и другие линкеры могут производить и ожидать объектные файлы совершенно другого формата, например формата ELF или DCU.
    2. При описании особенностей работы компилятора, линкера, особенностей формирования DBG-файлов и особенностей процесса легофикации использовались детали, актуальные именно для тех версий инструментов (компилятора, лиинкера), которые использовались при компиляции компонентов VB6.
    3. Подробности представления ссылок на внешние сущности и механизм COMDAT-секций изложены с некоторыми упрощениями.


    Методика подсчёта или какой код учитывался?
    Доминирующая доля VB6 написана на языках C и C++ (остальная часть написана на ассемблере). Языки C и C++ чрезвычайно гибки и либеральны в отношении организации файла-исходника: объявления переменных и типов могут располагаться не только перед кодом процедур (как в VB6, например), но и вообще в любом месте файла-исходника, в том числе и между функциями, после функций, а также внутри функций. То же касается и директивы препроцессора #include (включающей другой файл-исходник в данный файл-исходник), которая может располагаться в любом месте файла-исходника — в том числе и внутри функций. А к подключаемому файлу не предъявляется абсолютно никаких требований (он может содержать что угодно).

    Однако на практике программистами соблюдается ряд ограничений (чисто формальных и стилистических):
    • Директивы #include располагаются, как правило, в самом начале файла-исходника одним блоком.
    • Затем идут объявления типов и глобальных переменных, протоптипов функций и типов.
    • Затем идёт код самих процедур.
    • Подключаемые с помощью #include файлы являются заголовочными, и, во-первых, сами в свою очередь следуют этим же правилам, а во-вторых, как правило, не содержат в себе реализации функций, а содержат только объявления типов и прототипы функций, а также макроопределения (#define), шаблоны.

    Особенность компилятора C/C++ (использованного при компиляции Microsoft Visual Basic), обусловленная архитектурой COFF-символов, такова, что информацией о номерах строк исходного кода снабжаются только процедуры — только исполняемый код (машинный код, Native Code). Эта информация попадает сначала в объектные файлы (.obj-файлы формата COFF), а после линковки остаётся в таблице COFF-символов в отладочном файле формата DBG. Сохраняется лишь таблица соответствия номеров строк и адресов блочков инструкций (кусочков процедур), соответствующих данным строкам, что необходимо отладчикам для возможности ставить точки останова (breakpoints) во время отладки прямо на строках исходного кода. Для объявлений переменных, типов (структур и классов), шаблонов, макросов никакой информации в COFF-символы не попадает ещё на этапе компиляции. Тем более неоткуда им взяться и при последующей линковке, поэтому не будет их и в DBG-файлах.

    При этом, когда компилятор C/C++ компилиирует эпилог процедуры, например:
    Код: Выделить всё
    leave
    retn XXX

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

    Зная для каждого отдельного файла-исходника, какая функция является последней (что можно понять по максимальному номеру строки), можно для каждого файла установить номер последней непустой строки — при этом делается предположение, что следом за этой строкой нет блока комментариев, объявлений типов (что лишено всякого смысла) или переменных (что бывает крайне редко) или директив препроцессора (#include или #define).

    Таким образов, приняв за правду, что в реальном проекте все исходные файлы можно разделить на файлы с реализацией (имеющие расширение .c, .cpp, .cxx) функций и заголовочные файлы (имеющие расширение .h, .hpp, .hxx), содержащие в основном лишь объявление типов, классов, шаблонов, макроконстант, и что файлы первой категории имеют внутреннюю организацию, соответствующую вышеперечисленным правилами (то есть что последние строки кода в каждом таком файле являются кодом функции), можно для каждого такого файла определить его размер (число строк в нём), определив номер строки с максимальным числовым значением, которая будет соответствовать строке с закрывающей фигурной скобки последней функции этого файла.

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

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

    Однако существует ряд проблем такой методики, о которых следует знать. Большинство заголовочных файлов на практике содержат немалое число объявлений и макросов, однако не содержат выполнимого кода, то есть кода с реализацией функций и методов. Объявления типов, переменных и макросов, содержащиеся в заголовочных файлах, при компиляции других исходников, включивших в себя эти заголовочные файлы, как уже было сказано, не порождают на выходе никаких COFF-символов, следы которых можно было бы обнаружить в DBG-файлах.

    Поэтому заголовочные файлы, содержащие только объявления, но не содержащие реализации (т.е. какого-либо исполнимого кода), остаются вне нашего видения: не известен не только размер таких заголовочных файлов, но и даже их имена. Однако на практике заголовочные файлы могут содержать inline-функции, перегрузку операторов, а также объявление классов с определением конструкторов и деструкторов непосредственно в теле класса, либо любые шаблонные классы. Достаточно хотя бы одной из перечисленных сущностей, чтобы полное имя заголовочного файла всё-таки попало в DBG-файл, а номера строк, на которые приходится код такой сущности, был зафиксирован. Как правило inline-функции и методы классов, объявляемые непосредственно в классе, редко находятся в заголовочных файлах в самом начале — заголовочные файлы тоже стремятся поддерживать вышеобозначенную организацию кода. Поэтому если, к примеру, в заголовочном файле находится единственная крохотная (однострочная) inline-функция, заканчивающаяся на 800-й строке, заголовочный файл будет учтён и его размер будет принят за как минимум 800 строк, пусть даже inline-функция занимает в нём только одну строку.

    В следствие этого в отношении заголовочных файлов возникает неизбежный недоучёт: ряд заголовочных файлов вообще не учитывается, а для тех, что учитываются, за размер файла (в строчках) принимается номер последней строки с исполняемым кодом, а не просто номер последней строки файла. Однако для незаголовочных файлов, которые следуют общепринятой схеме, размер файла определяется достаточно точно и безошибочно.

    Другая проблема при предугадывании размера файла-исходника состоит в том, что компоненты Microsoft Visual Basic (IDE, рантайм) скомпилированы с использованием механизма COMDAT folding: при работе которого не только неиспользованный машинный код (или данные) не попадает в итоговый исполняемый файл, но и соответствующие COFF-символы не попадают в файл отладочной информации. Поэтому если последней функцией какого-то файла-исходника была функция, воплощение которой в машинном коде не попало (на этапе линковки) в исполняемый файл, размер файла-исходника будет недооценен из-за неучёта этой функции.

    Для исходников Ruby это исключительно редкая ситуация: любая функция в рамках Ruby должна попасть либо в исходники VB6.EXE, либо в исходники MSVBVM60.DLL, а скорее всего попадёт и в тот, и в другой файл. Функции же, которые в после сборки не попадают ни туда, ни туда, могут считаться бесполезными или ненужными, поэтому их недоучёт не рассматривается как проблема — к тому же недоучёт размера файла происходит только тогда, когда подобная функция является в нём самой последней (по порядку следования). Само по себе существование процедур, не пригодившихся в итоговом продукте, является довольно редкой ситуацией, а совпадение, при котором такие процедуры оказываются ещё и самыми последними в файле-исходнике — ещё большая редкость.

    Для исходников EB ситуация более печальная: существование большого числа процедур, воплощение которых в машинном коде попадает лишь в VBA6.DLL, для которого отладочная информация отсутствует, означает, что для ряда файлов размер будет недооценен — в качестве нижней границы размера будет принят номер строки, принадлежащий последней функции, попавшей (на нашу радость) в MSVBVM60.DLL.

    Что касается тактики оценки вклада исходников в тот или иной компонент (Ruby или EB) или файл (VB6.EXE или MSVBVM60.DLL) — применялась следующая тактика:
    • Для файлов-исходников, покрытых двумя файлами отладочной информации (VB6.DBG и MSVBVM60.DBG), выбирался максимальный номер строки из двух номеров, полученных в результате анализа каждого из файлов.
    • Для файлов-исходников, являющихся частью исходников EB, за неимением второго файла отладочной информации брался просто максимальный номер строки.
    • При подсчёте вкладка исходников в формирование исполняемого файла весь файл целиком принимался за вклад, даже если в исполняемый файл в результате линковки попала одна единственная однострочная процедура из этого файла, находящаяся пусть даже в самом начале файла — то есть подсчёт вклада вёлся на уровне файлов-исходников, а не на уровне отдельных процедур. Количество кода, необходимое для сборки VB6.EXE или MSVBVM60.DLL в данной статье подсчитано как сумма размеров участвующих в компиляции и линковки файлов-исходников, а не как сумма размеров только тех процедур (из этих файлов), которые реально пригодились при линковке и вошли в состав исполняемого файла. Полученные числа при этом оказываются несколько большими по сравнению с числами, получающимися при подсчёте и суммировании на уровне отдельных процедур.

    Что касается малой доли файлов-исходников, написанных на ассемблере и скомпилированных с помощью компилятора MASM, то в отношении них справедливы большинство вышеперечисленных особенностей, фактов и принципов, которые распространялись на C и C++.

    Также важно иметь в виду, что в состав Microsoft Visual Basic 6 входят два исполняемых файла: линкер LINK.EXE и компилятор C2.EXE — эти компоненты являются урезанными модификациями линкера Visual Studio/Platform SDK и компилятора Visual C++. Их исходники являются составной частью других продуктов Microsoft, и не являются самостоятельной частью исходников Microsoft Visual Basic, поэтому при оценке размера исходников эти два компонента не учитывались вообще (к тому же, для них также нет файлов с отладочной информацией).

    Подводя итоги:
    • Оценка не полная, или же попросту «минимальная», она позволяет оценить нижний порог, но фактические числа могут быть больше, чем приведённые в статье.
    • Большинство заголовочных не учтены при оценке. Все заголовочные файлы, содержащие только объявления типов и макросов, но без явной или неявной реализации исполняемых функций — не учтены вообще. Заголовочные файлы, содержащие помимо объявления типов и макросов хотя бы одну функцию, шаблонный класс или класс с реализацией метода внутри объявления класса — индексируются, причём не только в объёме кода подобных сущностей, но также и в объёме всего кода, предшествующего этим сущностям в исходном коде.
    • Файлы, содержащие функции, индексируется по максимальному номеру строки, получившей своё воплощение в машинном коде в процессе компиляции, при условии, что это воплощение не было отброшено и при линковке VB6.EXE, и при линковке MSVBVM60.DLL. Для функций на языке С/С++ последней строкой, дающей своё воплощение в машинном коде, является строка с последней закрывающей фигурной скобкой, что с учётом общеприятного стиля, предписывающего ставить закрывающую фигурную скобку на обособленной строке, означает что максимальный обнаруженный номер строки будет соответствовать общему числу строк в файле в большинстве из случаев.
    • При оценке количества кода, составляющего VBA/EB, учтены только те файлы, чей код попал в MSVBVM60.DLL хотя бы в минимальном объёме. Значительная часть кода VBA/EB, отвечающая за режим отладки, кодогенерацию P-кода, компиляцию в Native-код из P-кода — в MSVBVM60.DLL при линковке не попала, а попала лишь в VBA6.DLL, и из-за отсутствия отладочных символов для этой библиотеки, весь этот код оказывается полностью неучтённым.

    The trick
    Постоялец
    Постоялец
     
    Сообщения: 690
    Зарегистрирован: 26.06.2010 (Сб) 23:08

    Re: § 12. Сколько строк кода и файлов в исходниках самого VB

    Сообщение The trick » 02.12.2020 (Ср) 18:26

    Спасибо за статью, очень интересно!
    UA6527P

    Diamock
    Постоялец
    Постоялец
    Аватара пользователя
     
    Сообщения: 362
    Зарегистрирован: 26.10.2009 (Пн) 4:19
    Откуда: Кемерово

    Re: § 12. Сколько строк кода и файлов в исходниках самого VB

    Сообщение Diamock » 07.12.2020 (Пн) 17:02

    Очень интересно. Спасибо!
    In der Beschrankung zeigt sich erst der Meister
    Графоманю...

    ger_kar
    Продвинутый гуру
    Продвинутый гуру
    Аватара пользователя
     
    Сообщения: 1952
    Зарегистрирован: 19.05.2011 (Чт) 19:23
    Откуда: Кыргызстан, Иссык-Куль, г. Каракол

    Re: § 12. Сколько строк кода и файлов в исходниках самого VB

    Сообщение ger_kar » 08.12.2020 (Вт) 18:03

    А какие средства использовались для анализа отладочных файлов?
    Бороться и искать, найти и перепрятать

    Хакер
    Телепат
    Телепат
    Аватара пользователя
     
    Сообщения: 16324
    Зарегистрирован: 13.11.2005 (Вс) 2:43
    Откуда: Казахстан, Петропавловск

    Re: § 12. Сколько строк кода и файлов в исходниках самого VB

    Сообщение Хакер » 09.12.2020 (Ср) 16:40

    ger_kar писал(а):А какие средства использовались для анализа отладочных файлов?

    Использовались утилиты из комплекса средств, специально созданных (мною) для анализа и изучения внутреннего устройства VB.

    Основной при этом была утилита с названием RFNG, что расшифровывается как Raw File Name Grabber или Rough File Name Grabber, то есть «сырой» или «грубый» добыватель имён файлов. С каких-то пор название утилиты совершенно не отражает её сути: когда-то давно эта утилита просто добывала из DBG-файла неупорядоченный список имён исходных файлов, отыскивая их по сигнатуре .file, и совершенно не вникая в формат и структуру DBG-файла. Поэтому и raw/rough.

    Затем утилита эволюционировала так, что от этого подхода с поиском по сигнатуре (которая фактически являлась содержимым одного из полей записей таблицы COFF-символов) не осталось и следа, а утилита стала полностью разбирать и анализировать DBG-файл, не полагаясь на dbghelp.dll или imagehlp.dll. Поэтому утилита стала не просто добывать список файлов, а восстанавливать ассоциативные связи между разными сущностями и строить структурированное представление о файлах, о содержащихся в них функциях (и других сущностях — методах классов, процедурах-инициализаторах) и т.д. Но название RFNG осталось.
    —We separate their smiling faces from the rest of their body, Captain.
    —That's right! We decapitate them.


    Вернуться в VB Internals

    Кто сейчас на конференции

    Сейчас этот форум просматривают: нет зарегистрированных пользователей и гости: 1

        TopList