
Полная версия
Грамматическая машина. Том 23. От философской онтологии к исполнимому языку
Детерминированная система говорит: мир предсказуем, одинаковые входы дают одинаковые выходы. Это онтология необходимости, где всё подчинено законам, и законы эти неизменны. Вероятностная система говорит: мир неопределён, одинаковые входы могут давать разные выходы. Это онтология случайности, где неопределённость является фундаментальной, а не временной, и система должна работать с этой неопределённостью, а не устранять её. Переход от детерминизма к вероятностности — это смена типа рациональности: от формально-логической и картезианской к такой, которая требует удержания неопределённости как валидного состояния.
Архитектура кода как конституирование реальности
Архитектура кода не просто описывает мир — она конституирует его. Когда мы проектируем систему, мы создаём не просто программу, а реальность, в которой эта программа работает. Мы определяем, какие сущности существуют, как они связаны, какие операции допустимы, какие события могут произойти. Каждая строка архитектурного решения — это акт онтологического творчества.
Эта реальность имеет свою онтологию, и она может быть описана в терминах ГМ. Субстанции в архитектурной реальности — это те сущности, которые имеют самостоятельное бытие: сервисы, базы данных, очереди сообщений, пользователи, сессии. Каждая субстанция обладает идентичностью, которая сохраняется при изменении её состояний. Сервис может быть перезапущен, база данных может мигрировать, пользователь может изменить пароль — но идентичность сохраняется. Модусы — это свойства и состояния субстанций: данные, которые хранятся в базе, конфигурации сервисов, права доступа пользователей, статусы сессий. Модусы могут меняться, но они всегда принадлежат своей субстанции. Границы — это условия, при которых сущности могут существовать и взаимодействовать: API, контракты, политики безопасности, соглашения об уровне обслуживания. Границы определяют, что допустимо, а что является нарушением.
И когда система работает, она не просто выполняет вычисления — она воспроизводит эту реальность, поддерживает её существование, реагирует на изменения. Каждый запрос, каждая транзакция, каждое событие — это акт конституирования. Система постоянно создаёт и пересоздаёт свою онтологию. Сбой сервиса — это не просто техническая ошибка, а онтологическое событие: субстанция временно перестаёт существовать, и вся система должна перестроить свою реальность с учётом этого отсутствия. Восстановление после сбоя — это возобновление существования.
Реверс-инжиниринг как восстановление архитектурной онтологии
Здесь мы подходим к ключевому пониманию: реверс-инжиниринг — это не восстановление кода. Это восстановление онтологии системы. Мы пытаемся понять, какую реальность конституировала эта программа, какой мир она создавала. Какие сущности существовали в её мире? Как они были связаны? Какие операции были допустимы? Каковы были границы существования? Какие архитектурные решения были приняты и какую онтологию они породили?
В книге «ИИ в реверс-инжиниринге» эта проблема описана как «семантическая эрозия»: при компиляции онтология программы разрушается, превращаясь в «серую слизь» машинных инструкций. Функции теряют имена, структуры теряют организацию, типы теряют границы. Но на архитектурном уровне происходит нечто ещё более драматичное: теряются отношения между компонентами, теряется знание о том, какие части системы были субстанциями, а какие — модусами, теряются границы, которые определяли допустимые взаимодействия. Всё, что остаётся — это последовательности инструкций, которые выполняют операции, но не несут в себе знания о том, какую реальность эти операции конституировали.
Почему статический анализ так труден? Потому что мы пытаемся восстановить онтологию из того, что осталось после компиляции — из безымянных функций, бестиповых данных, разрушенных структур. Это как пытаться восстановить архитектуру здания по груде кирпичей. Кирпичи есть, но мы не знаем, как они были организованы, какие стены они образовывали, какие комнаты они ограничивали, где были двери, а где — несущие конструкции.
ГМ предлагает подход к этой проблеме. Она даёт нам язык для описания архитектурной онтологии, операторы для её анализа и инструменты для её восстановления. Мы используем расщепление (split), чтобы выделить разные уровни архитектуры: от инструкций к функциям, от функций к модулям, от модулей к сервисам, от сервисов к системе в целом. Мы используем удержание (hold), чтобы фиксировать противоречия между разными гипотезами о том, как устроена система, не разрешая их преждевременно. Мы используем переход (transition), чтобы двигаться между уровнями абстракции — от ассемблера к псевдокоду, от псевдокода к архитектурной диаграмме, от диаграммы к онтологической модели.
Онтологическое восстановление архитектуры в действии
Рассмотрим, как это работает на практике — на примере анализа прошивки IoT-устройства. Стандартный подход: найти функции, понять, что они делают. Онтологический подход ГМ: восстановить архитектурный мир, который конституирует эта прошивка.
Мы начинаем с расщепления — выделяем разные уровни анализа. На уровне инструкций мы видим операции с памятью и регистрами, вызовы и переходы. Это сырой материал, лишённый архитектурного смысла. На уровне функций мы видим, как эти операции организованы в осмысленные блоки: функция читает из сокета, функция применяет преобразование к данным, функция отправляет ответ. На уровне модулей мы видим, как функции группируются в логические единицы с общей ответственностью: модуль сетевого взаимодействия, модуль криптографии, модуль управления конфигурацией. На уровне архитектуры мы видим, как эти модули взаимодействуют: кто кого вызывает, какие данные передаются, где проходят границы. На уровне онтологии мы реконструируем субстанции, их модусы и границы: устройство, сессия, пользователь, конфигурация.
Мы применяем удержание, когда обнаруживаем противоречие между разными уровнями. Функция, которая по инструкциям выглядит как шифрование, но по месту в архитектуре является частью протокола аутентификации, — мы не разрешаем это противоречие немедленно. Мы фиксируем его как узел напряжения: возможно, это не шифрование, а хеширование; возможно, это не аутентификация, а проверка целостности; возможно, мы имеем дело с аутентифицированным шифрованием, где обе операции являются модусами одной субстанции — защищённого канала.
Мы применяем переход, поднимаясь от уровня инструкций к уровню онтологии. Мы спрашиваем: какие субстанции существуют в этой системе? Мы обнаруживаем устройство как фундаментальную субстанцию, сессию как временную субстанцию, пользователя как субстанцию с правами, конфигурацию как субстанцию, определяющую параметры. Какие модусы они имеют? Устройство: состояние подключения, уровень заряда, версия прошивки. Сессия: идентификатор, таймаут, ключ шифрования. Пользователь: уровень доступа, история действий. Конфигурация: сетевые настройки, параметры безопасности. Каковы границы их существования? Таймауты сессий, проверки прав доступа, валидация конфигурационных данных, лимиты на количество подключений.
И только после того, как мы восстановили эту архитектурную онтологию, мы можем ответить на практические вопросы безопасности и анализа. Где уязвимости? Не просто «где переполнение буфера», а в каких местах архитектурная онтология разрывается — где границы нарушаются без генерации исключения, где модусы входят в противоречие друг с другом, где субстанции теряют свою идентичность. Где архитектурные решения создают онтологические напряжения, которые могут быть использованы атакующим?
Это и есть реверс-инжиниринг как восстановление архитектурной онтологии. И это то, что делает ГМ возможным: переход от «серой слизи» инструкций к пониманию того, какую реальность конституировала программа, к восстановлению мира, который был потерян при компиляции. Не просто мира кода, но мира архитектурных решений, онтологических выборов и фундаментальных напряжений, которые формировали этот код.
2.5. Проклятие реверс-инженера: потеря онтологии при компиляции
Реверс-инжиниринг — это не просто сложная задача. Это онтологически проблематичная задача. Проблема не в том, что ассемблер трудно читать — мнемоники можно выучить за несколько недель. Проблема в том, что между исходным кодом и бинарным файлом лежит пропасть, которую невозможно преодолеть простым чтением инструкций. Эта пропасть — потеря онтологии при компиляции. И пока мы не поймём эту потерю как онтологическую катастрофу, мы не сможем построить инструменты, которые действительно помогают реверс-инженеру.
Компиляция как семантическая эрозия
Исходный код — это текст с богатой онтологией. В нём есть имена переменных, которые несут смысл: user_password, session_timeout, encryption_key — каждое имя указывает на роль сущности в мире программы. В нём есть типы данных, которые определяют границы существования: uint32_t задаёт диапазон допустимых значений, struct User определяет, из каких компонентов состоит пользователь, enum Status фиксирует конечное множество возможных состояний. В нём есть структуры, которые организуют данные в осмысленные целые. В нём есть абстракции — классы, интерфейсы, модули, — которые скрывают сложность за простыми контрактами.
Компиляция — это процесс, который уничтожает эту онтологию. Я называю этот процесс семантической эрозией: исходный код теряет свою семантическую насыщенность и превращается в «серую слизь» машинных инструкций. Это не побочный эффект оптимизации — это суть компиляции. Компилятор не обязан сохранять онтологию. Его задача — производить эффективный машинный код. И для этого он безжалостно уничтожает всё, что не нужно для выполнения. Компилятор действует как совершенная машина онтологического забвения: он помнит только то, что требуется процессору — байты, адреса, операции.
Семантическая эрозия происходит на нескольких уровнях, и каждый из них представляет собой отдельную онтологическую катастрофу. Эти катастрофы соответствуют тем слоям онтологии, которые я описал в разделе 2.2: компиляция последовательно уничтожает субстанции, модусы, границы и архитектурные отношения, оставляя лишь вычислительный скелет.
Первая катастрофа: смерть типов данных
В исходном коде uint32_t, float, struct User имеют чёткие границы. Это не просто наборы байт — это сущности с определённым смыслом, с допустимыми операциями, с онтологическим статусом. uint32_t — это беззнаковое 32-битное целое, для которого определены арифметические операции и операции сравнения, но не определено, например, деление на ноль. struct User — это составная субстанция, объединяющая имя, идентификатор и права доступа в одно онтологическое целое.
В бинарном коде почти всё превращается в безликие машинные слова — dword, qword, word. Инструкция mov eax, [rcx+8] не содержит никакой информации о том, что именно мы загружаем: указатель на виртуальную таблицу, счётчик цикла, часть строки или поле age внутри структуры User. Тип умер — остались только байты определённого размера. Это не просто потеря информации. Это потеря онтологической структуры. Мир, который был организован в осмысленные типы со своими границами и допустимыми операциями, превращается в мир, где есть только байты и адреса, и ничто не указывает на то, чем одни байты отличаются от других в онтологическом смысле. Реверс-инженер вынужден восстанавливать эту структуру заново — гадать, что означают эти байты, какие операции над ними допустимы, как они организованы в более крупные целые. Ошибка в восстановлении типа — это не просто техническая неточность, это приписывание сущности неверного онтологического статуса.
Вторая катастрофа: исчезновение границ переменных
В исходном коде переменные имеют чёткие границы существования во времени и пространстве. Переменная int x существует от точки объявления до конца области видимости. В разное время x может хранить разные значения, но её идентичность сохраняется: это та же самая переменная x, даже если её значение изменилось. Это субстанция с определённым жизненным циклом.
Компилятор агрессивно переиспользует регистры и участки стека. Одна и та же ячейка памяти [rsp+0x20] в начале функции может хранить дескриптор файла, в середине — временный результат арифметического выражения, в конце — счётчик цикла. Переменные не просто теряют имена — они теряют свои границы, свою идентичность. Одна и та же память последовательно воплощает разные сущности, и границы между ними не отмечены ничем, кроме инструкций, которые читают и пишут в эту память. Это не просто оптимизация. Это онтологическое смешение: в исходном коде разные переменные — это разные субстанции с разными модусами и разными жизненными циклами. В бинарном коде они становятся одной и той же памятью, которая последовательно воплощает разные, онтологически не связанные друг с другом сущности. Реверс-инженер вынужден отслеживать эту «жизнь после смерти» переменных — понимать, в какой момент ячейка памяти перестаёт быть одной сущностью и становится другой, и где проходят невидимые границы между ними.
Третья катастрофа: разрушение абстракций
Объектно-ориентированный код с классами, наследованием и методами превращается в плоский спагетти-код из инструкций call и манипуляций с указателями. Виртуальные таблицы видны лишь как статические массивы указателей. Иерархия наследования исчезает — остаются только смещения в памяти. Отношение «является» (AdminUser является User) не сохраняется ни в какой явной форме.
Это не просто потеря структуры. Это разрушение всего того, что делало код осмысленным на онтологическом уровне. В исходном коде вызов метода — это осмысленное действие в контексте объекта: user.authenticate() означает, что мы просим конкретного пользователя выполнить действие, определённое его классом или унаследованное от родительского класса. В бинарном коде это просто call [rax+0x38] — прыжок по адресу, который вычисляется как значение по смещению 0x38 от указателя, хранящегося в rax. Абстракции, которые делали код понятным, уничтожены. Осталась только голая машинерия вызовов и переходов.
Четвёртая катастрофа: потеря архитектурных отношений
К трём описанным катастрофам я добавляю четвёртую, которая особенно важна для реверс-инжиниринга крупных систем. В исходном коде модули, сервисы и компоненты связаны архитектурными отношениями, которые не сводятся к вызовам функций. Модуль A зависит от модуля B. Сервис C является клиентом сервиса D. Компонент E реализует интерфейс F. Эти отношения конституируют архитектурную онтологию: они определяют, какие части системы являются субстанциями, как они связаны, где проходят границы.
Компиляция уничтожает и эти отношения. В бинарном коде нет модулей — есть только линейное или почти линейное адресное пространство. Нет сервисов — есть только код, который что-то делает с данными. Нет интерфейсов — есть только вызовы по адресам. Архитектурная онтология исчезает полностью, и реверс-инженер вынужден восстанавливать её по косвенным признакам: по тому, какие функции вызывают друг друга, по тому, как данные организованы в памяти, по тому, какие строки и константы используются.
Восстановление онтологии как обратный перевод
Восстановление онтологии из бинарного кода — это задача «обратного перевода» с бедного языка на богатый. Это похоже на перевод с мёртвого языка, для которого не сохранилось ни словаря, ни грамматики, ни параллельных текстов. У нас есть текст на языке, который мы не знаем (ассемблер), и мы пытаемся восстановить текст на языке, который мы знаем (исходный код), но у нас нет ни словаря соответствий, ни грамматических правил перехода, ни примеров, где один и тот же смысл выражен на обоих языках.
Это онтологическая проблема, а не просто лингвистическая. Мы пытаемся восстановить не просто слова, а мир, который эти слова описывали и конституировали. Мы пытаемся понять, какие субстанции существовали, какие модусы они имели, каковы были их границы, как они были связаны архитектурными отношениями. Но у нас есть только следы этого мира — инструкции, которые работают с байтами и адресами, и никакой явной информации о том, как эти байты и адреса были организованы в онтологическое целое.
Традиционные методы реверс-инжиниринга пытаются решить эту проблему через эвристики и паттерны. IDA Pro ищет сигнатуры известных функций — «вот так выглядит strcpy, вот так выглядит malloc». Ghidra пытается восстановить типы по тому, как используются данные — «если к этому значению применяются арифметические операции, вероятно, это целое число». Но все эти методы сталкиваются с фундаментальным ограничением: они работают на уровне синтаксиса, а не онтологии. Они могут сказать, что функция похожа на strcpy, но не могут сказать, какую роль эта функция играет в мире программы — копирует ли она имя пользователя, часть сетевого пакета или временный буфер для форматирования строки. Они восстанавливают синтаксические формы, но не онтологическое содержание.
ГМ как решение: операторное восстановление онтологии
ГМ предлагает иной подход. Вместо того чтобы пытаться восстановить код — синтаксические формы, — мы пытаемся восстановить онтологию, мир, который конституировала программа. И мы делаем это не через эвристики, а через систематическое применение операторов ГМ.
Мы используем расщепление (split), чтобы разделить бинарный файл на смысловые блоки. Мы не смотрим на отдельные инструкции как на изолированные единицы — мы с самого начала группируем их в функции, функции в модули, модули в архитектурные компоненты. Мы разделяем анализ на уровни — инструкции, функции, модули, архитектура, онтология — и на каждом уровне задаём свои вопросы. На уровне инструкций: какие данные читаются и пишутся? На уровне функций: какую логику реализует эта группа инструкций? На уровне модулей: как функции группируются в единицы с общей ответственностью? На уровне архитектуры: как модули взаимодействуют друг с другом? На уровне онтологии: какие субстанции существуют, каковы их модусы и границы?
Мы используем удержание (hold), чтобы фиксировать противоречия между предполагаемыми онтологиями. Когда мы видим, что функция по одним признакам похожа на шифрование (использует XOR с ключом), а по другим — на хеширование (результат фиксированной длины, необратимое преобразование), мы не разрешаем это противоречие преждевременно. Мы фиксируем его как узел напряжения — TensionNode, который содержит обе гипотезы вместе с их основаниями. Это позволяет нам удерживать множественность интерпретаций, не сворачивая их в одну до того, как у нас будет достаточно информации с других уровней анализа. Возможно, на уровне архитектуры обнаружится, что эта функция является частью протокола аутентификации, и тогда напряжение разрешится в пользу одной из гипотез. А возможно, она является частью аутентифицированного шифрования, и тогда напряжение укажет на более сложную онтологическую структуру.
Мы используем переход (transition), чтобы двигаться между уровнями абстракции, переформулируя понимание на каждом новом уровне. Мы начинаем с уровня инструкций — что делает этот код на уровне процессора: читает из памяти, применяет арифметическую операцию, записывает обратно. Затем мы переходим к уровню алгоритмов — какую логику реализует этот код: это цикл, это ветвление, это конечный автомат. Затем к уровню архитектуры — какую роль этот код играет в системе: это часть модуля аутентификации, это обработчик сетевого протокола, это функция логирования. Затем к уровню онтологии — какую реальность конституирует эта система: пользователь проходит аутентификацию, создаётся сессия, сессия имеет таймаут, после таймаута сессия уничтожается.
Каждый переход — это акт интерпретации, который не может быть полностью автоматизирован. Мы не просто переходим — мы трансформируем данные, переформулируем понимание, изменяем контекст интерпретации. И на каждом уровне мы проверяем согласованность: не противоречит ли наше понимание на этом уровне тому, что мы видели на предыдущих уровнях? Если противоречит — мы возвращаемся, удерживаем противоречие и ищем более глубокую структуру, которая могла бы его разрешить, не уничтожая.
От серой слизи к восстановленному миру
Конечная цель этого процесса — не просто «понять код». Конечная цель — восстановить онтологию программы, мир, который был потерян при компиляции. Понять, какие субстанции существовали в этом мире, какие сущности имели самостоятельное бытие и сохраняли идентичность во времени. Понять, какие модусы они имели, какие свойства и состояния их характеризовали и как эти модусы изменялись. Понять, каковы были границы их существования, какие условия должны были быть выполнены для корректной работы и какие нарушения приводили к исключениям. Понять архитектурные отношения между ними, как они были связаны в целостную систему.
Это — восстановление мира. Мира, который существовал в сознании разработчиков, когда они проектировали программу. Мира, который был зафиксирован в структурах данных, в архитектурных решениях, в интерфейсах и контрактах. Мира, который был уничтожен компиляцией и который мы должны восстановить, если хотим не просто читать инструкции, а войти в ту реальность, которую они конституировали.
Именно это делает ГМ возможной как инструмент реверс-инжиниринга. Операторы split, hold и transition — это не просто аналитические инструменты в ряду других. Это способы систематического восстановления онтологии из того, что осталось после семантической эрозии. Это способы перехода от «серой слизи» инструкций к пониманию того, какую реальность конституировала программа. Это способы снятия проклятия реверс-инженера — не через магию, а через операторное мышление.
И в следующей главе я покажу, как эти же операторы, применённые к LLM, превращают вероятностного попугая в инструмент, способный удерживать онтологическую сложность и участвовать в восстановлении миров, потерянных при компиляции.
3.1. Ограничения текущих LLM: галлюцинации, потеря контекста, непонимание онтологии
Большие языковые модели — это технологическое чудо. Они пишут стихи, отвечают на вопросы, генерируют код, ведут диалоги. Но их блеск скрывает фундаментальные ограничения, которые становятся особенно опасными, когда мы пытаемся использовать их для глубокого анализа — философских текстов, программного кода, архитектурных решений. Эти ограничения не случайны. Они вытекают из самой природы LLM как вероятностных машин.
LLM как «вероятностные попугаи»
В основе LLM лежит простая, но мощная идея: предсказывать следующее слово в последовательности на основе предыдущих. Модель не «понимает» в человеческом смысле. Она не строит модель мира, не различает истину и ложь, не имеет онтологических предпосылок. Она вычисляет вероятности: какое слово наиболее вероятно после данной последовательности слов. Это делает её «вероятностным попугаем» — она воспроизводит паттерны, увиденные в обучающих данных, но не знает, что эти паттерны означают.
Это не проблема для многих задач. Для генерации текста, для перевода, для суммаризации — вероятностное предсказание работает удивительно хорошо. Но для глубокого анализа, где требуется понимание структуры, онтологии, причинности, вероятностное предсказание становится препятствием. Модель не анализирует — она имитирует анализ. Она не понимает — она воспроизводит паттерны понимания. И в этой имитации кроются три фундаментальных ограничения, каждое из которых имеет онтологический корень и каждое из которых требует не просто улучшения модели, а введения внешней структуры — экзоскелета, которым и является Грамматическая машина.
Галлюцинации: когда правдоподобие заменяет истину
Галлюцинации — это, пожалуй, самое известное ограничение LLM. Модель выдумывает факты, создаёт несуществующие цитаты, придумывает имена функций, которых никогда не было. Это происходит потому, что LLM не отличает истину от правдоподобия. Для неё «звучит правильно» и «является истиной» — это одно и то же. Если паттерн правдоподобен, модель его воспроизведёт, даже если он не соответствует реальности.
Онтологический корень галлюцинаций — в отсутствии у модели механизма верификации. LLM не проверяет свои утверждения на соответствие чему-либо вне себя. У неё нет процедуры, которая говорила бы: «это правдоподобно, но я не знаю, правда ли это, поэтому я должен зафиксировать неопределённость, а не маскировать её выдумкой». Модель работает в режиме картезианской машины без картезианской дисциплины: она производит правдоподобные высказывания, но не проверяет их на ясность и отчётливость.












