Предыстория
За последние два года мне несколько раз пришлось восстанавливать завёрнутый код Oracle версий 7.2-9.2:
один раз - в рамках рабочей задачи;
один раз - чтобы вернуть наш собственный потерянный пакет;
пару раз - по просьбе коллег из финтеха: древний код неправильно считал налог, и надо было разобраться почему.
Имевшаяся утилита REWRAP 1993 года, рассчитанная на PL/SQL 2.2, помогла несильно: код нужно было довести до уровня, пригодного для отладки. Так я погрузился в тему unwrapping-а, и эта статья - сводка того, что удалось выяснить о формате V1 в ходе создания unwrapper-а.
Введение
В Oracle 7.2–9.2 утилита WRAP не шифрует исходный текст. Она прогоняет его через фронтенд компилятора PL/SQL и записывает в файл синтаксическое дерево DIANA - промежуточное представление, которое база затем достраивает и компилирует. Поэтому формат V1 обратим: имена и литералы лежат в открытом лексиконе, а структура программы - в шести числовых таблицах.
Статья разбирает, что такое DIANA, через какие слои проходит код на пути в wrapped-файл, как устроен этот файл и что в нём теряется. Формат V1 охватывает PL/SQL 2.2–9.2, то есть Oracle 7.2, 7.3, 8.0, 8i и 9i. Формат V2 (Oracle 10g и новее) с ним не связан: там хранится сжатый текст без дерева.
Цифры в статье получены дампом таблиц эталонных юнитов - wrapped-файлов из дистрибутивов Oracle и синтетических примеров, завёрнутых WRAP разных версий: 332 юнита V1, 16 625 узлов. Внешние источники - документация Oracle 9.2, справочник DIANA и доклад Пита Финнигана на Black Hat 2006. Распаковка формата V2 (Oracle 10g и новее) реализована в репозитории oracle-plsql-unwrap.
DIANA: дерево с атрибутами
DIANA (Descriptive Intermediate Attributed Notation for Ada) - промежуточный язык компиляторов Ada: программа хранится как дерево узлов с именованными атрибутами. Её спроектировали группы Герхарда Гооса (Карлсруэ) и Уильяма Вульфа (Карнеги–Меллон) в 1981 году (FOLDOC). Структура дерева задана на метанотации IDL (Interface Description Language), а физическое представление скрыто за операциями типа (справочник DIANA, ред. 3, 1983).
PL/SQL построен на Ada и использует свой вариант DIANA, тоже описанный на IDL. По документации Oracle 9.2, база хранит и DIANA, и m-code: по DIANA компилируются зависимые модули, m-code просто исполняется. Спецификация пакета, типа, отдельная процедура или анонимный блок ограничены 2^26 узлами DIANA; при превышении компилятор выдаёт ошибку вроде PLS-00123.
Узлы
Таблица раскладок, извлечённая из IDL-дескриптора WRAP 9.2.0.4, содержит 300 кодов узлов; в 9.x атрибуты есть у 230 из них. Префикс имени говорит о семействе узла. Старые имена обрезаны до 8 символов (D_COMP_U, DI_PACKA), узлы, добавленные в 8i–9i, - длиннее (D_SQL_STMT, DI_BULK_ITER).
Префикс |
Семейство |
Кодов |
Примеры |
|---|---|---|---|
|
конструкции языка |
166 |
|
|
идентификаторы: определения и использования |
35 |
|
|
последовательности |
23 |
|
|
статический SQL |
75 |
|
Атрибуты
В исходной DIANA четыре вида атрибутов: структурные (as_), лексические (lx_), семантические (sm_) и атрибуты кода (cd_). PL/SQL сохраняет это деление со своими префиксами: в таблице раскладок 184 имени атрибутов. Последняя колонка - доля ненулевых значений в 332 юнитах сэмплов: она показывает, какие атрибуты WRAP вообще заполняет.
Префикс |
Вид |
Имён |
Примеры |
Ненулевых в wrapped-файлах |
|---|---|---|---|---|
|
структурный: ссылка на узел или флаг |
84 |
|
63% |
|
структурный: последовательность |
28 |
|
72% |
|
лексический: номер лексемы |
7 |
|
54% |
|
семантический |
47 |
|
5,1% |
|
семантическая последовательность |
10 |
|
0% |
|
кодогенерация |
8 |
|
0,2% |
Структурные и лексические атрибуты заполнены, семантические и атрибуты кода практически пусты. Из семантических встречаются лишь связи, которые парсер знает сам: S_LAYER, S_FIRST, S_T_SPEC, S_RECORD. Исходный текст можно восстановить именно из структурных атрибутов: авторы DIANA закладывали тесное соответствие между синтаксисом языка и этими атрибутами.
Где DIANA живёт в базе
Скомпилированный модуль хранится в таблицах SYS IDL_UB1$, IDL_UB2$, IDL_SB4$ и IDL_CHAR$. Суффиксы - типы элементов IDL: беззнаковые 1 и 2 байта, знаковые 4 байта, символы. Колонка PART разделяет содержимое: 0 - DIANA, 1 - переносимый p-code (m-code), 2 - машинно-зависимый p-code; так сказано в комментарии sql.bsq, на который ссылается Финниган.
Из PL/SQL дерево читают пакеты PIDL и DIANA (скрипты pipidl.sql, pidian.sql) и утилиты DIUTIL. Имена узлов и атрибутов из pidian.sql совпадают с теми, что зашиты в WRAP. По наблюдению Финнигана, DIANA тел пакетов в IDL-таблицах не сохраняется, и через эти API тела процедур не достать.
Где WRAP встраивается в компиляцию
WRAP - это лексер и парсер PL/SQL, которые останавливаются перед семантическим анализом. Файл .plb содержит то дерево, которое база построила бы сама после разбора исходника. При загрузке она пропускает разбор и продолжает с семантики.

В документации Oracle 9.2 тело wrapped-юнита названо промежуточной формой объектного кода (Wrap Utility, 9.2), а Финниган прямо называет WRAP фронтендом компилятора PL/SQL. Из этого следуют три вещи:
WRAP не видит базу. Он не разрешает внешние ссылки, поэтому опечатка в имени таблицы проявится только при компиляции
.plb. Синтаксические ошибки он сообщает сам.Семантические атрибуты в файле пусты. Это видно по замеру в таблице атрибутов:
S_заполнены на 5,1%,SS_- на 0%,C_- на 0,2%. Имена вDI_U_NAMне связаны с определениями:S_DEFN_PRIVATEненулевой в 2 случаях из 5 107.Файл совместим вверх, но не вниз. Юнит WRAP 8.1.5 загружается в 8.1.6, обратное невозможно. Вероятная причина - раскладки узлов: в новой версии DIANA у узлов появляются атрибуты, которых старый компилятор не знает.
Формат wrapped-файла V1
Wrapped-юнит V1 - обычный текст: скопированный заголовок CREATE, служебная шапка, лексикон и шесть таблиц дерева в шестнадцатеричной записи. Схема показывает файл из разбора ниже (пакет va, 59 узлов) в порядке строк, как его читает парсер распаковщика.

Шапка
WRAP копирует оператор CREATE до конца имени юнита как есть - с регистром, схемой, кавычками, переводами строк и комментариями - и дописывает wrapped. У тела типа копия идёт до IS или AS, поэтому пробелы и комментарии перед ними тоже попадают в заголовок. Дерево заголовка не хранит: CREATE и CREATE OR REPLACE, имя со схемой и без дают одинаковые таблицы.
Дальше идут строка 0 и строки abcd: 16 штук в PL/SQL 2.x–8.1.5 и 15 начиная с 8.1.6, где на месте шестнадцатой стоит число (в сэмплах 1 или 3). Следом - код вида юнита в hex, он совпадает с типом объекта в словаре: 7 - процедура, 8 - функция, 9 - пакет, b - тело пакета, d - тип, e - тело типа, 16 - библиотека.
Затем идёт версия DIANA - семь шестнадцатеричных цифр. Это версия PL/SQL, а не Oracle (PL/SQL 2.2 - это Oracle 7.2, 2.3 - 7.3, дальше номера совпадают). И это не релиз самого WRAP: утилита пишет последнюю версию DIANA, которую знает.
WRAP (релиз PL/SQL) |
Строка версии в юните |
|---|---|
9.2.0.4 |
|
9.0.1 |
|
8.1.6, 8.1.7 |
|
8.1.5 (Oracle8i для Windows NT) |
|
8.0.5 |
|
2.3.3.0.0, 2.3.4 |
|
2.3.2.3.0 для Windows NT |
|
2.2.2.3.1 для Windows NT |
|
Слова на 25ff - не версия, а след ошибки импорта. WRAP для Windows PL/SQL 2.2 и 2.3.2 импортирует переменную версии как функцию и читает первые четыре байта переходника jmp [slot]: FF 25 и младшие байты адреса слота. Настоящая версия этих сборок - 200f000, и раскладка атрибутов у них та же, что у 2.3.4. После версии идут постоянные строки 1, 4, 0.
Лексикон
Лексикон - список всех различных слов юнита: имён, литералов, знаков операций и ключевых слов, которые лексер читает как идентификаторы (PACKAGE, TYPE, VARRAY). Каждое слово записано один раз, в порядке первого появления; номер 0 - пустая лексема. Первая строка - число лексем в hex, последняя - 0.
PL/SQL 2.x и 8.0: строка
<длина в hex> <текст>:; длинная лексема продолжается на следующих строках, пока не наберётся длина.PL/SQL 8.1.5 и новее: маркер
2 :e:, затем строки1<текст>:; строка, оканчивающаяся на+, продолжается следующей.Экранирование: перевод строки внутри лексемы -
:n, двоеточие -::, возврат каретки записан как есть.
Пример строк лексикона: PL/SQL 2.3.4 и 9.2
2.3.4: 12 SELECT_DELETE_DEMO: ← 0x12 = 18 символов 4 P_ID: 9.2: 1SELECT empno, ename:n from emp:n where deptno = p:n order by ena+ 1me: ← продолжение лексемы после +
Шесть таблиц дерева
Каждая таблица начинается с числа позиций и строки-маркера 2 или 4, затем идут значения в hex через пробел. Запись :N v означает N повторов значения v. Позиция 0 значит «нет значения», поэтому узлы, атрибуты и элементы списков нумеруются с 1.
№ |
Таблица |
Индекс |
Значение |
Маркер: 2.x–8.1.5 / 8.1.6 / 9.x |
|---|---|---|---|---|
1 |
Node Types |
номер узла |
код типа узла ( |
2 / 2 / 2 |
2 |
Attribute References |
номер узла |
позиция первого атрибута в таблице 5 |
2 / 2 / 2 |
3 |
Columns |
номер узла |
колонка в исходном тексте |
2 / 2 / 2 |
4 |
Lines |
номер узла |
строка в исходном тексте |
2 / 2 / 4 |
5 |
Attributes |
позиция |
значения атрибутов всех узлов блоками |
2 / 4 / 4 |
6 |
AS Lists |
позиция |
список: длина, затем номера узлов |
2 / 4 / 4 |
Маркер, по всей видимости, - ширина элемента в байтах. Парсер проверяет только, что это 2 или 4, а во всех 332 юнитах сэмплов значения зависят только от версии.
Узел - это номер, по которому из таблиц 1–4 берутся тип, начало блока атрибутов и позиция в тексте. Атрибут k узла лежит в таблице 5 по позиции «ссылка + k − 1». Список в таблице 6 записан как длина и столько же номеров узлов вслед за ней.
Число атрибутов узла в файле не записано. Его знает только IDL-описание фронтенда той версии, что писала файл; лишний атрибут окажется атрибутом соседнего узла. Блоки лежат не в порядке узлов, а между ними бывают ничьи слоты со случайными значениями.
До 8.1.6 ссылка на блок атрибутов 16-битная, а её старшие разряды спрятаны в коде типа узла. Если код ≥ 0x1000, то тип - это код mod 0x1000, а к ссылке добавляется (код div 0x1000) × 0x10000.
Эпилог и хвост
После таблиц снова идут 1, 4, 0 и номер корневого узла; корень обязан иметь тип D_COMP_U. Дальше WRAP пишет хвост, который распаковщик не читает: строки 0, 1, 14, два счётчика, 0x14 = 20 чисел и тройки чисел. Первое число тройки в сэмплах - номер определяющего идентификатора: пакета, подпрограммы, параметра, переменной, курсора или индекса цикла. Юнит заканчивается строкой /.
Слои преобразования
На пути в .plb текст проходит семь шагов, и каждый что-то теряет. Самое важное для восстановления: семантический шаг пропущен, поэтому в файле стоит дерево, близкое к синтаксису, а не откомпилированная программа. То, что теряется на шагах 3–5, часто оставляет косвенные следы, и распаковщик их ищет.

Лексер: что остаётся от текста
Регистр. Имена без кавычек попадают в лексикон в верхнем регистре: пакет
vaзаписан какVA. Имя в кавычках сохраняет регистр, а сами кавычки становятся флагом узла-идентификатора (четвёртый атрибут, значения 1 или 5).Единственность и порядок. Каждая лексема записана один раз в порядке первого появления:
NUMBERвстречается в примере трижды, а в лексиконе стоит однажды, под номером 8. Потому даже порядок слов в восстановленном тексте влияет на совпадение таблиц.Ключевые слова. Зарезервированные слова (
BEGIN,END,IS,AS,WITH,IN) следа не оставляют. Слова, которые лексер читает как идентификаторы (PACKAGE,TYPE,VARRAY,EXTERNAL,LANGUAGE,NAME), становятся узламиDI_U_NAM, на которые не ссылается ни один атрибут. Эти «сироты» - главный источник косвенных следов.Комментарии. Исчезают, кроме трёх случаев: комментарий в заголовке, хинт оптимизатора (лексема
L_Q_HINT) и SQL, сохранённый текстом. До 9.0 WRAP теряет символ сразу после/*+, и распаковщик ставит там пробел.
Парсер: дерево и его нумерация
Узлы нумеруются в порядке создания: парсер сначала строит детей, потом родителя. В примере корень D_COMP_U - узел 58 из 59. Каждый узел хранит строку и колонку своего токена, поэтому wrapped-код может сообщать номера строк исходника.
Блоки атрибутов выделяются не в порядке узлов, и это тоже след. WRAP до PL/SQL 9.0 после IS читает один токен вперёд, а после AS - нет. Узел забегающего токена получает блок раньше имени пакета, и по этому следу распаковщик выбирает IS или AS.
Нормализация: сахар становится каноном
Фронтенд пишет в дерево не слова исходника, а их каноническую форму, и для этого иногда добавляет свои лексемы. Распаковщик должен узнать канон и вернуть исходную запись.
В исходнике |
В дереве |
По чему видна форма |
|---|---|---|
|
|
|
|
|
флаг 1; у |
|
ветка |
имя исключения помечено флагом 64 «создано парсером» |
|
вызов |
имя с пробелом нельзя написать в тексте; |
|
вызов |
то же имя с пробелом |
|
вызов функции-оператора; имена |
|
Статический SQL
По умолчанию WRAP разбирает каждый SQL-оператор на узлы под Q_SQL_ST, и распаковщик собирает его заново в своей раскладке. WRAP 9.2 с ключом edebug=wrap_new_sql хранит оператор узлом D_SQL_STMT с исходным текстом в лексиконе, вместе с комментариями. Операторы курсора OPEN, FETCH, CLOSE в обоих режимах остаются узлами.
Ключ описан в руководстве PL/SQL 9.2: он включает поддержку всего синтаксиса SQL и поэтому оставляет SQL открытым текстом. В корпусе из 4017 юнитов таких 7; ещё 1429 содержат разобранный SQL, 2581 обходятся без статического SQL.
Сериализация
Дерево целиком раскладывается по таблицам из предыдущего раздела, и новых потерь тут нет. Но число атрибутов узла становится неявным, а между блоками появляются ничьи слоты: за узлом без атрибутов их всегда два. WRAP оставляет в них произвольные значения, так что два файла с одним деревом могут различаться байтами.
Разбор примера: пакет va
Спецификация пакета из тестового набора (WRAP 9.2.0.4) занимает семь строк, а в .plb превращается в 18 лексем, 59 узлов, 251 слот атрибутов и 20 позиций списков - 146 строк. Распаковщик возвращает эталонный текст байт в байт.
Текст
Исходный текст пакета va (8 строк)
CREATE OR REPLACE PACKAGE va IS TYPE V1 IS VARRAY( 10 ) OF NUMBER; TYPE V2 IS VARYING ARRAY( 5 ) OF VARCHAR2( 10 ); TYPE V3 IS VARRAY( 3 ) OF NUMBER NOT NULL; TYPE T1 IS TABLE OF NUMBER; X V1; END; /
Это эталон сэмпла и одновременно вывод распаковщика. Раскладка в нём каноническая - отступ в три пробела, пробелы внутри скобок, - а WRAP той же версии строит из него то же дерево DIANA.
Wrapped-файл
Wrapped-файл пакета va целиком (146 строк)
CREATE OR REPLACE PACKAGE va wrapped 0 abcd abcd abcd abcd abcd abcd abcd abcd abcd abcd abcd abcd abcd abcd abcd 3 9 9200000 1 4 0 12 2 :e: 1PACKAGE: 1VA: 1TYPE: 1V1: 1VARRAY: 11: 110: 1NUMBER: 1V2: 1VARYING: 1ARRAY: 15: 1VARCHAR2: 1V3: 13: 1T1: 1BINARY_INTEGER: 1X: 0 0 0 3c 2 0 a0 97 a0 9d a0 :2 51 63 a8 a0 1c c 77 a0 9d :2 a0 :2 51 63 a8 a0 51 a5 1c c 77 a0 9d a0 :2 51 63 a8 a0 1c c 77 a0 9d a0 1c a0 40 a8 c 77 a3 a0 1c 81 b0 :2 aa 59 58 1d 17 b5 3c 2 0 3 7 11 39 19 1d 20 23 27 28 2c 34 15 40 70 48 4c 50 53 56 5a 5b 5f 62 63 6b 44 77 9f 7f 83 86 89 8d 8e 92 9a 7b a6 c5 ae b2 ba be bf c0 aa e1 d0 d4 dc cf cc e8 ea ed f0 f1 fa 3c 2 0 1 9 3 8 e :4 14 :2 1c e :2 3 8 e 16 :4 1b 22 2b 2a 22 e :2 3 8 e :4 14 :2 1b e :2 3 8 :2 17 :4 e :2 3 :3 5 3 :7 1 3c 4 0 :2 1 :b 2 :e 3 :b 4 :9 5 :5 6 :7 1 fc 4 :3 0 1 :3 0 2 :6 0 1 :2 0 3 :3 0 4 0 c 35 5 :3 0 6 :2 0 7 :2 0 6 7 :2 0 3 8 :3 0 a :7 0 9 b :2 0 2 4 c 4 :4 0 3 :3 0 f 0 1a 35 a :3 0 b :3 0 6 :2 0 c :2 0 12 13 :2 0 5 d :3 0 7 :2 0 7 16 18 :6 0 15 19 :2 0 2 9 1a f :4 0 3 :3 0 1d 0 25 35 5 :3 0 6 :2 0 f :2 0 1f 20 :2 0 9 8 :3 0 23 0 2 :5 0 22 24 :2 0 2 e 25 1d :4 0 3 :3 0 28 0 2e 35 8 :3 0 29 :7 0 11 :3 0 2b b 2d 2a :2 0 1 10 2e 28 :4 0 f 37 0 d 4 :3 0 31 :7 0 34 32 0 35 0 12 :7 0 37 35 36 38 2 37 3a 0 39 38 3b :8 0 15 4 :3 0 1 8 1 14 1 17 1 21 1 2c 1 30 5 d 1b 26 2f 33 1 4 0 3a 0 1 14 1 6 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 28 1 0 4 1 0 f 1 0 1d 1 0 2 0 1 30 1 0 0 /
Вторая команда файла, CREATE OR REPLACE TYPE vt AS VARRAY(20) OF NUMBER;, осталась в .plb открытой. По документации 9.2 WRAP заворачивает из типов только объектные (CREATE TYPE … OBJECT) и тела типов, а остальные команды передаёт как есть.
Дерево

Нумерация идёт снизу вверх: литералы диапазона получили номера 6 и 7, сам диапазон - 8, корень - 58. Позиции взяты из исходника, который видел WRAP: там было VARRAY(10) без пробелов, поэтому у NUMBER записана колонка 28, а в каноническом тексте он стоит правее.
Один узел вручную
Расшифруем узел 8 - диапазон 1..10 из VARRAY(10):
Типы узлов, позиция 8:
63= 99, то естьD_RANGE. В строке0 a0 97 a0 9d a0 :2 51 63запись:2 51занимает позиции 6 и 7.Ссылки на атрибуты, позиция 8:
23= 35.Колонки и строки, позиция 8: колонка 20, строка 2.
Раскладка
D_RANGEдля 9.x - четыре атрибута:A_EXP1,A_EXP2,S_BASE_T,S_LENGTH_SEMANTICS. В таблице атрибутов на позициях 35–38 стоят 6, 7, 0, 0: границы - узлы 6 и 7, семантика пуста.Узлы 6 и 7 -
D_NUMERIсL_NUMREP, равным лексемам 6 и 7, то есть1и10.Родитель - узел 9
DS_D_RANсAS_LIST= 3. В таблице списков на позиции 3 стоят длина 1 и элемент 8.
Второй пример: процедура с SELECT INTO, IF и циклом
На пакете va видны только объявления. Вот процедура с SELECT … INTO, IF и циклом:
Исходный текст процедуры bonus (11 строк)
CREATE OR REPLACE PROCEDURE bonus(p_empno IN NUMBER) AS v_sal NUMBER; BEGIN SELECT sal INTO v_sal FROM emp WHERE empno = p_empno; IF v_sal < 1000 THEN FOR i IN 1 .. 3 LOOP v_sal := v_sal * 2; END LOOP; END IF; END; /
После WRAP 9.2.0.4 от процедуры остаются заголовок, словарь лексем (16 штук) и плоские таблицы дерева на 62 узла. Исходного текста там нет:
Фрагмент wrapped-файла процедуры bonus
CREATE OR REPLACE PROCEDURE bonus wrapped … 9200000 … 1BONUS: 1P_EMPNO: 1NUMBER: 1V_SAL: 1SAL: 1EMP: 1EMPNO: 1=: 1<: 11000: 1I: 11: 13: 1LOOP: 1*: 12:
Дерево процедуры
Дерево хранится плоскими таблицами. Это таблицы типов узлов, ссылок на блоки атрибутов, самих атрибутов, списков и пара таблиц строк и колонок. Номер узла - порядок его создания при разборе, то есть снизу вверх. Поэтому листья идут первыми, а корень D_COMP_U получает номер 61 из 62. Ниже тот же массив, развёрнутый в дерево (в скобках номер узла; служебные атрибуты S_*, C_* в 9.2 нулевые, их не показываю):
Дерево DIANA процедуры bonus (31 строка)
D_COMP_U (61) └ A_UNIT_B → D_S_BODY (58) тело процедуры BONUS ├ A_HEADER → D_P_ (7) → DS_PARAM → D_IN (5) P_EMPNO IN NUMBER └ A_BLOCK_ → D_BLOCK (57) ├ AS_ITEM → DS_ITEM [D_VAR (12)] V_SAL NUMBER ├ AS_ALTER → DS_ALTER [] раздела EXCEPTION нет └ AS_STM → DS_STM (54) = [29, 53] ├ Q_SQL_ST (29) ── SELECT … INTO ── │ └ A_STM → Q_SELECT (26) │ ├ A_EXP → Q_EXP (27) │ │ ├ AS_EXP → DS_EXP [DI_U_NAM SAL] │ │ └ A_EXP → Q_TBL_EX (19) │ │ ├ AS_FROM → DS_NAME [DI_U_NAM EMP] │ │ └ A_WHERE → D_F_CALL (24) │ │ ├ A_NAME → D_USED_O "=" │ │ └ AS_P_ASS → DS_PARAM [EMPNO, P_EMPNO] │ └ AS_INTO_ → DS_NAME [DI_U_NAM V_SAL] ← INTO лежит рядом с запросом └ D_IF (53) ── IF ── └ AS_LIST → [D_COND_C (52)] одна ветка условия ├ A_EXP_VO → D_F_CALL (34) V_SAL < 1000 │ ├ A_NAME → D_USED_O "<" │ └ AS_P_ASS → DS_PARAM [DI_U_NAM V_SAL, D_NUMERI "1000"] └ AS_STM → DS_STM [D_LOOP (50)] ── цикл ── ├ A_ITERAT → D_FOR (40) │ ├ A_ID → DI_ITERA I │ └ A_D_R_ → D_RANGE (39) A_EXP1 = D_NUMERI "1", A_EXP2 = D_NUMERI "3" └ AS_STM → DS_STM [D_ASSIGN (47)] ├ A_NAME → DI_U_NAM V_SAL └ A_EXP → D_F_CALL (46) V_SAL * 2 ├ A_NAME → D_USED_O "*" └ AS_P_ASS → DS_PARAM [DI_U_NAM V_SAL, D_NUMERI "2"]
Что видно по дереву
Нет узлов для служебных слов. Ни
END IF, ниEND LOOP, ни;в дереве нет. Рендерер сам пишетEND IF;после списка веток иLOOP … END LOOP;вокруг тела.IF- это список веток. Каждая ветка -D_COND_Cс условием и телом. Первая ветка пишется какIF … THEN, следующие какELSIF, а ветка без условия какELSE.Цикл - это заголовок плюс тело. Заголовок лежит в
A_ITERAT. ЭтоD_FOR,D_FOR_REVERSE,WHILEилиFORALL. Если заголовка нет, получается простойLOOP. Диапазон выводится какнижняя..верхняя.INTOхранится отдельно от запроса. Это атрибутAS_INTO_узлаQ_SELECT, а не частьQ_EXP. Распаковщик сначала собирает текст запроса (список выборки,FROM,WHERE) и потом вставляет строкуINTOпосле второй строки.Операторов в дереве нет, есть вызовы.
<,*и=- этоD_F_CALLс именемD_USED_Oи списком операндов. Распаковщик собирает из них инфиксную запись. Цепочки видаa + b + cон разворачивает циклом по первому операнду.Числа хранятся лексемами.
1000лежит в словаре как строка"1000", значениеS_VALUEне вычисляется.Каждый узел проверяется. Рендерер сверяет типы ссылок и обратную ссылку
A_UPна родителя. Если структура не сходится, он сообщает об ошибке структуры с номером узла, а не выдаёт мусор.
Восстановленный текст
Точный вывод распаковщика:
Восстановленный текст процедуры bonus (17 строк)
CREATE OR REPLACE PROCEDURE bonus( P_EMPNO IN NUMBER ) IS V_SAL NUMBER; BEGIN SELECT SAL INTO V_SAL FROM EMP WHERE EMPNO = P_EMPNO; IF V_SAL < 1000 THEN FOR I IN 1..3 LOOP V_SAL := V_SAL * 2; END LOOP; END IF; END; /
Текст совпадает с исходным по смыслу, но не по форме. В wrapped нет ни регистра, ни пробелов, ни комментариев, поэтому:
Имена стали заглавными. Заголовок
bonusостался как был, потому что он хранится текстом до словаwrapped.ASпревратилось вIS. В 9.x дерево для них одинаковое, и распаковщик выбираетIS.1 .. 3превратилось в1..3, аLOOPушёл на отдельную строку. Это канонический формат, а не восстановленное оформление автора.
Проверка обратным wrap
Результат я проверил обратным wrap той же версией:
wrap(восстановленный текст)даёт дерево, идентичное исходному по типам узлов, атрибутам, спискам и словарю. Различаются только таблицы строк и колонок, потому что раскладка текста другая.unwrap(wrap(восстановленный текст))совпадает с восстановленным текстом побайтно.Если исходный текст уже в каноническом формате, как у образцов из дистрибутивов Oracle и синтетических тестов,
wrapвозвращает исходный wrapped-файл побайтно. Это основной критерий проверки.
Версии DIANA и раскладки атрибутов
Одна и та же конструкция в разных версиях даёт узлы с разным числом атрибутов, а файл этого числа не хранит. Распаковщик берёт его из таблицы раскладок: она извлечена из IDL-дескриптора WRAP 9.2.0.4, где записаны все пять версий DIANA. Новая версия только дописывает атрибуты в конец списка, поэтому раскладка старой версии - префикс новой.
Версии DIANA: релизы, число типов и слотов, что появилось
Версия DIANA |
Релизы WRAP |
Группа |
Типов с атрибутами |
Слотов в раскладке |
Что появилось в дереве |
|---|---|---|---|---|---|
|
2.2–2.3.4, слова |
2.x |
183 |
664 |
базовая раскладка PL/SQL 2 |
|
8.0.5 |
8.0 |
202 |
759 |
объектные типы, внешние процедуры и библиотеки, |
|
8.1.5 для Windows NT |
8.1.5 |
216 |
819 |
|
|
8.1.6, 8.1.7 |
8.1.6 |
218 |
826 |
|
|
9.0.1; 9.2.0.4 пишет |
9.0, 9.2 |
230 |
893 |
выражения |
Последняя колонка - новые узлы и атрибуты из таблицы раскладок (например, D_FORALL и Q_EXEC_IMMEDIATE в 8.1.5, D_CASE_EXP и D_PIPE в 9.x). Юнит незнакомой версии читается по ближайшей младшей раскладке: 9200000 - как 9000000.
Откуда взялась таблица
IDL-описание - это дескриптор, на который в Linux-сборках WRAP 8.1.6–9.2 указывает символ plsdiana. В PLS234.DLL и PLS805.DLL символов нет, и дескриптор находится по ссылке на массив имён узлов. Структура дескриптора во всех релизах одинакова: номера атрибутов всех узлов, начало списка атрибутов каждого типа, их число, базовые типы атрибутов, имена и таблицы чисел по версиям.
Раскладки сверены с другими релизами и проверены на 1431 юните и 3,2 млн узлов: ни у одного узла блок в файле не короче раскладки. Пороги вроде «с Oracle 8i», подобранные по сэмплам, с ней не сходятся: UPDATE … RETURNING есть с 8.0.3, FETCH … LIMIT - с 8.1.6, AUTHID - с 8.1.5, а AGENT у LIBRARY - только с 9.0.
Релиз и форма SQL по дереву
Релиз нужен, чтобы выбрать WRAP для проверки круговым прогоном, а строка версии его не всегда называет. Поэтому распаковщик сообщает три значения: строку как в юните, релиз с точностью, которую позволяет юнит, и группу совместимости, например 2.2.2.3.1 [952825ff, compatible 2.x].
PL/SQL 2.2 или 2.3. Строку
200f000пишут оба семейства, но деревья тел у них разные: 2.3 оставляет узлыD_NULL_Sбез родителя после каждогоD_P_BODYиD_S_BODY, 2.2 таких узлов не создаёт. Признак проверен на 490 юнитах PL/SQL 2 с дисков Oracle 7.3; спецификации пакетов у 2.2 и 2.3 совпадают и получают релиз2.x.Форма статического SQL.
Её видно по типам узлов:D_SQL_STMTзначит текст,Q_SQL_STс оператором кромеOPEN,FETCH,CLOSE- разобранный SQL, иначе статического SQL нет. Для кругового прогона юнита с текстом нужен WRAP 9.2.0.4 сedebug=wrap_new_sql.
Обратное преобразование: как устроен распаковщик
Распаковщик проходит путь WRAP в обратную сторону. Парсер читает таблицы в неизменяемую модель юнита, рендерер обходит дерево от корня и пишет текст в канонической раскладке, а потерянные детали добирает по следам в дереве. Публичный API один для V1 и V2, модель DIANA наружу не видна.

Правила рендерера
Один источник данных. Всё читается из модели юнита, таблицы не копируются. Атрибут адресуется номером в узле, а атрибут за пределами раскладки версии читается как 0, поэтому чужие атрибуты не попадают в текст.
Проверки в одном месте.
Каждая ссылка сверяется со схемой узла: атрибут должен указывать на узел, список или лексему нужного вида. Испорченное дерево даёт ошибку структуры с номером и типом узла, а циклы в цепочках узлов обнаруживаются.Разделение по областям. Обработчики объявлений, операторов, выражений и SQL - по несколько сотен строк каждый; обработчик выбирается по коду типа узла. Выражения возвращают текст, составные операторы сами пишут строки.
Раскладка как у эталона. SQL раскладывается как в выводе исходной программы REWRAP: переносы, отступы, выравнивание вложенных запросов. Для этого обработчики SQL передают вложенным узлам колонку выравнивания и режим.
Потерянное - по следам
Что восстановить |
Откуда берётся |
|---|---|
Заголовок: схема, |
строка заголовка wrapped-юнита, а не дерево |
|
след забегающего токена: порядок узлов и блоков атрибутов |
Порядок предложений внешней процедуры, |
номера узлов-сирот ключевых слов |
Перевод строки |
перевод строки заголовка юнита во входном тексте: CRLF, LF или CR |
Релиз PL/SQL и форма SQL |
строка версии и признаки дерева |
Проверка идёт на трёх уровнях. Тесты сравнивают вывод с эталонами байт в байт; круговой прогон требует, чтобы WRAP той же версии построил из восстановленного текста то же дерево; на wrapped-файлах из дистрибутивов Oracle вывод сверяется с REWRAP. Круговой прогон строже смысловой эквивалентности: из-за лексикона и нумерации узлов важен даже порядок слов.
Проблемы и подводные камни
Главная проблема V1 - дерево хранит смысл программы, но не её запись. Часть записи потеряна насовсем, часть остаётся лишь в косвенных следах, а сам формат содержит ловушки, на которых наивный распаковщик выдаёт неверный текст.
Теряется безвозвратно
Комментарии внутри юнита. Уцелевают только комментарии заголовка, хинты и SQL, сохранённый текстом.
Раскладка. Позиции узлов в файле есть, но пробелы и переносы между токенами по ним однозначно не восстанавливаются. Распаковщик пишет каноническую раскладку REWRAP.
Регистр имён без кавычек и ключевых слов: в лексиконе всё в верхнем регистре.
ISилиASв PL/SQL 9.x. WRAP 9.x строит для них одно дерево, поэтому пишетсяIS, как у REWRAP.
Неоднозначности, которые снимаются по следам
Проблема |
Масштаб |
Как снимается |
|---|---|---|
|
в wrapped-файлах Oracle 2374 заголовка 871 юнита восстанавливаются как |
след забегающего токена |
Порядок предложений внешней процедуры |
3351 тело в файлах Oracle 8.0–9.2, 27 разных порядков; фиксированный порядок совпадает с исходником у 1678 |
номера узлов ключевых слов |
Необязательные слова |
смысл не меняют, но добавляют лексему и узел |
те же узлы и номера узлов |
|
один узел |
сирота |
Канонизированные формы: |
парсер переписал конструкцию |
служебные имена |
Ловушки формата
Восемь ловушек формата V1
Неявное число атрибутов. Без раскладки версии чтение уходит в атрибуты соседнего узла. Длину блока нельзя вычислить по ссылке на следующий узел: блоки не идут по порядку, а между ними бывают ничьи слоты.
Версия - не релиз. WRAP пишет последнюю известную ему версию DIANA, а WRAP для Windows PL/SQL 2.2 и 2.3.2 - слово
…25ff. Семизначное слово вроде92025ffлегко принять за версию 9.2, если проверять его не первым.16-битные ссылки до 8.1.6. Старшие разряды ссылки на блок атрибутов лежат в коде типа узла; без декодирования большой юнит читается неверно.
Два формата лексикона и сырой CR. Возврат каретки из литерала записан без экранирования, поэтому строку лексикона завершает только LF или CRLF.
Хинты. До PL/SQL 9.0 WRAP теряет символ сразу после
/*+.Переводы строк. WRAP для Windows пишет CRLF и хранит перевод строки в литерале как LF. WRAP для Linux пишет LF и отвергает CR вне литералов ошибкой
PSU(103,…), поэтому текст с CRLF ему не скормить.Заголовок тела типа. WRAP копирует его до
ISилиASвместе с пробелами. Если распаковщик добавит свой пробел, каждый круговой прогон добавит ещё один.CREATE TYPE … AUTHID. WRAP 8.1.5–9.2.0.4 оставляет такой оператор незавёрнутым, и местоAUTHIDв его дереве неизвестно.
Ограничения самого WRAP
Нет семантической проверки: ошибки имён и типов видны только при загрузке
.plb.Совместимость только вверх: файл WRAP 8.1.6 не загрузится в 8.1.5.
Файл заметно больше исходника: в примере семь строк превратились в 146.
Часть синтаксиса SQL в 9.2 по умолчанию не поддержана;
edebug=wrap_new_sqlвключает его ценой открытого SQL.Заворачиваются только подпрограммы, пакеты и объектные типы (в сэмплах есть ещё
LIBRARY); остальные команды идут в.plbкак есть.Есть и предел компилятора: 2^26 узлов DIANA на модуль, при превышении - ошибка вроде PLS-00123.
Что мешает при разборе реальных wrapped-файлов
Wrapped-файлы из живых систем редко выглядят так, как их описывает документация: исходники переносили между Windows и Unix, а файлы собирали разными релизами WRAP. Ниже - проблемы, на которые натыкаешься именно на таких файлах.
CR на границе блока в 512 байт
Попадаются wrapped-пакеты, исходник которых был в Windows-формате с CRLF, а заворачивался в Unix или Linux, где строка кончается LF. CR перед LF убирается не везде: исходник читается блоками по 512 байт от начала файла, и пара CR LF распознаётся только внутри блока.
Где остаётся CR. Если CR стоит последним байтом блока - на смещении 511, 1023 и так далее, - а LF открывает следующий блок, CR переживает обработку. В юните с LF появляются единичные CRLF, в том числе внутри строковых литералов.
От чего зависит. Набор уцелевших CR определяется тем, какие строки исходного файла случайно закончились на границе блока. В восстановленном скрипте строки стоят на других смещениях, поэтому повторный WRAP убирает другие CR и даёт другие байты.
Что делать при разборе. Парсер не должен считать одиночный CR концом строки и не должен «чистить» такие CR в литералах: это часть значения, которое видела база. Восстановленный текст при этом смешивает LF и CRLF, и это точное воспроизведение исходника, а не ошибка.
В формате V2 это заметнее всего: так работают WRAP 10.2.0.1, 10.2.0.5, 11.1.0.6 и 11.2.0.1, а текст там сжат, и перекодировка файла до него не дотягивается. Даже в дистрибутиве Oracle 12.2.0.1 есть такие юниты: ords_migrate и upg_oauth с 3 и 1 оставшимся CRLF.
8.1.6 и 8.1.7: один формат, разный вывод
WRAP 8.1.6 и 8.1.7 генерируют код одного формата: строка версии 8106000 и одна раскладка атрибутов. Но вывод для одного и того же исходника у них отличается.
По строке версии релиз не определить: распаковщик сообщает диапазон
8.1.6-8.1.7.Для побайтового кругового прогона нужен WRAP именно того релиза, что собирал файл: совпадение версии DIANA этого не гарантирует.
Сравнивать файлы 8.1.6 и 8.1.7 по байтам бессмысленно - сравнивается восстановленный текст.
2.2 и 2.3.2: адрес вместо версии
WRAP для Windows PL/SQL 2.2 и 2.3.2 пишет на месте версии не 200f000, а адрес переменной с версией. Точнее, первые четыре байта переходника импорта jmp [slot]: FF 25 и младшие байты адреса, отсюда слова вроде 952825ff, a54025ff, 95a025ff. На самом деле это 200f000: дерево и раскладка атрибутов те же, что у 2.3.4.
Слово на
25ffнадо проверять до разбора версии, иначе семизначный вариант вроде92025ffлегко принять за 9.2 и прочитать дерево чужой раскладкой.Адрес зависит от сборки, поэтому слово заодно указывает на релиз:
952825ffиa54025ff- 2.2.2.3.1,95a025ff- 2.3.2.3.0, остальные известные - семейство 2.3.Незнакомое слово на
25ffчитается как PL/SQL 2.x, а 2.2 или 2.3 определяется по дереву.
Несовместимые изменения внутри одной версии DIANA
При переходе от версии к версии PL/SQL иногда появлялись «несовместимые» изменения, даже когда строка версии в файле оставалась прежней. Самый наглядный пример - переход от PL/SQL 2.2 к 2.3: оба враппера относятся к версии 200f000, но в таблице атрибутов между ними произведено переименование.
Что изменилось. Атрибут на той же позиции в 2.2 и 2.3.2 называется
S_NORMAL, а в 2.3.3 и 2.3.4 -S_NORMARGLIST. Кроме того, 2.3 оставляет в телах узлыD_NULL_Sбез родителя, а 2.2 - нет, то есть деревья одной и той же версии DIANA различаются.Как это видно в дистрибутивах. Из-за переименования Oracle сохранил в ранних дистрибутивах два разных враппера - для Oracle 7.2 и для Oracle 7.3.
Совместимость спрятана в базе. База 7.3 понимает атрибутику от 7.2: где-то внутри транслятора совместимость сохранена. Но на уровне враппера и файла этого не видно: строка версии одинакова, и кто именно завернул юнит, приходится определять по дереву.
Что это значит для распаковщика. Раскладку атрибутов нельзя считать однозначно заданной строкой версии, а для кругового прогона нужен враппер именно того семейства - 2.2 или 2.3, - которым файл был собран.
Насколько wrap V1 защищает код
Слабо: V1 - сериализованное дерево с открытым лексиконом, а не шифр. Документация Oracle 9.2 сама предупреждает: литералы и имена переменных, таблиц и столбцов остаются открытым текстом, а пароли и имена таблиц так не спрячешь.
Лексикон читается без инструментов. По Финнигану, видимая таблица символов выдаёт назначение процедуры и облегчает поиск SQL-инъекций. Он же показал правку wrapped-кода без распаковки.
Структура известна. Имена узлов и атрибутов лежат в
pidian.sqlдистрибутива, раскладки - в дескрипторе самого WRAP. Позиции узлов связывают дерево со строками исходника.Восстановление точное. Проект возвращает текст, из которого WRAP той же версии строит то же дерево, а с эталонами сэмплов совпадает байт в байт.
В 10g схему сменили. Формат V2 хранит сжатый текст без DIANA; его распаковка реализована в репозитории oracle-plsql-unwrap.
Итог: wrap V1 защищает от случайного взгляда, но не от целенаправленного чтения.
Что есть из готовых инструментов
Для формата V1 готовых инструментов немного, и у каждого есть ограничения:
REWRAP.EXE - DOS-утилита на Clipper 1993 года. Совместима только с WRAP 2.2 и частично с 2.3 и работает очень медленно - единицы строк в секунду. Её вывод задаёт каноническую раскладку, которую повторяет распаковщик из этой статьи.
rstenet/9i-unwrapper - proof of concept на PL/SQL для 9i и, вероятно, более ранних версий. По README реализовано около 40 из примерно 300 узлов DIANA: такие конструкции, как
LOOP,CURSOR,IFиEXCEPTION, не поддерживаются и помечаются в выводе, часть результата приходится править вручную, чтобы он скомпилировался, а исходная раскладка не восстанавливается.Jimmy45M/PL-SQL-Unwrapper - пакет PL/SQL, который работает только внутри базы Oracle 10.2 и новее и читает код из
DBA_SOURCE, то есть юнит сначала надо загрузить в базу. Понимает V1 и V2, но код Oracle 7 не поддерживает, а вывод V1 автор советует перепроверять. Для V1 в 21c нужен параметрPERMIT_92_WRAP_FORMAT, в 23ai формат V1 уже не загружается.Распаковщик Пита Финнигана на PL/SQL. В блоге он показывает восстановление кода WRAP 7.2–9.2 с проверкой обратным wrap, но в открытом доступе инструмента я не нашёл.
Демонстрация на пакетах DIANA и PIDL (Финниган,
unwrap_r.sql) показывает только сигнатуры пакетов, тела процедур через этот API не достать.Инструменты для 10g и новее - RemmyLee/Oracle-SQL-Unwrapper (Python/Flask),
unwrap.py, расширение Unwrapper для SQL Developer, онлайн-сервисы - рассчитаны на формат V2. Wrapped-код до 10g они не принимают.
Что удаётся сейчас
На данный момент мне удаётся развернуть любой код Oracle от версии 7.2 до 26ai с полным совпадением по исполнению после обратного wrapping-а. Расхождения допустимы только в двух случаях:
в комментариях
REM, если они были добавлены в итоговый документ;в лишних CR, если файл заворачивался при смешении CRLF и LF в исходнике и результате.
На логику и возможность компиляции эти расхождения не влияют.
Оговорка: речь о восстановлении собственного кода или кода, на который у вас есть права. Лицензионные соглашения Oracle, как правило, запрещают декомпиляцию и обратную разработку поставляемых программ, так что разворачивать чужие пакеты без таких прав не стоит.
Если столкнулись с похожей задачей - восстановить потерянный код или перенести старую систему, - пишите на ashindarev@gmail.com или в LinkedIn: ashindarev, обсудим.