Как сделать из amxx в sma декомпилятор

Обновлено: 05.07.2024

Если вы нажмёте кнопку и напишете и нажмёте на автор будет знать, что тратил время не зря !

Если вы нажмёте кнопку и напишете и нажмёте на автор будет знать, что тратил время не зря !

Если вы нажмёте кнопку и напишете и нажмёте на автор будет знать, что тратил время не зря !

Если вы нажмёте кнопку и напишете и нажмёте на автор будет знать, что тратил время не зря !

Если вы нажмёте кнопку и напишете и нажмёте на автор будет знать, что тратил время не зря !

Если вы нажмёте кнопку и напишете и нажмёте на автор будет знать, что тратил время не зря !

Если вы нажмёте кнопку и напишете и нажмёте на автор будет знать, что тратил время не зря !

Если вы нажмёте кнопку и напишете и нажмёте на автор будет знать, что тратил время не зря !

Теперь мы поговорим о том как получить из байт-кода исходный код. Для примера я написал маленький плагин с показательными моментами, который мы и будем разбирать как пример.
Байт-код плагина имеет вид:

Байт-код такого вида я получаю от своего декомпилятора, в начале строки обозначен адрес инструкции, а комментариями показаны адреса инструкций с переходами по данному адресу.
Впрочем smxviewer или SPEdit дают похожую картинку, иногда уже заранее преобразовав адреса в имена, что делает код более похожим на ассемблер.

Первым делом мы находим proc инструкции и разбиваем байт-код на 4 функции.

Теперь будем по очереди разбирать эти функции.

Получение прототипа функции
Первым делом для первой функции мы ищем как можно больше информации о ней что бы получить её прототип:
По адресу функции (3192) находим в .dbg.symbols секции описание функции

По tagid смотрим секцию .tags и по ссылки на имя типа в секции .names находим, что tagid 6 соответствует void.
ident 9 позволяет нам убедится что мы нашли правильный символ, т.к. 9 - IDENT_FUNCTION.
По name ищем в .dbg.strings название функции, и для 209 в данном случае получаем "OnPluginStart".
В результате имеем:

Теперь надо получить информацию о аргументах функции.
Делаем поиск в .dbg.symbols по codestart равным адресу нашей функции (аргументы создаются в начале вызова и уничтожаются в конце вызова, имея время жизни и видимость на протяжении всей функции.
В данном случае символов с codestart равным 3192 не найдено (кроме символа самой функции), это значит что аргументов эта функция не имеет (о чем вы и так уже догадались по её названию).

Теперь определим квалификаторы функции.
Смотрим в .public секцию в поиске адреса нашей функции и находим

Теперь важная ремарка связанная с разными версиями компилятора. Ранее квалификатор на уровне smx имели только обозначены public в исходном коде функции. Позднее при компиляции стали все функции приравнивать к public, даже те, что не имеют в объявлении public, для того чтобы иметь возможность найти их с помощью GetFunctionByName и вызвать их через Call_StartFunction, имена функций, которые не имели квалификатора public будут с припиской адреса в начале. К примеру если бы OnPluginStart не имел бы public, то его имя в .names секции было записано как ".3192.OnPluginStart".
Таким образом чтобы понять, надо ли добавлять public, надо заглянуть не только в .publics секцию, но и посмотреть её имя в .names секции.
В итоге мы получаем, что функция имеет прототип:

Первые две инструкции proc и break мы уже преобразовали в тело самой фукнции, отбрасываем их.
Далее надо выделить структурные блоки кода. В этом нам очень помогают break инструкции, в работе кода они не участвуют и нужны для работы отладчика, но при этом подсказывают нам где начинается новая строка и новый кусок кода. (т.е. образно break нам показывает где были ';' в исходном коде и как делится код.
Заглянем в начале в конец функции: Если вы внимательно читали прошлые уроки и помните о том как преобразуется "return" конструкция в байт-код, то по отсутствию break инструкции перед "3248: zero.pri" можно понять, что это функция, которая не имела return в конце её тела.
Таким образом, делим код на блоки и отбрасываем уже разобранные нами, получим:


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

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

Здесь несколько вариантов приводящих к одному результату. Мы можем найти описание всем нам известной функции в интернете:

Здесь же вы сразу же можете заметить, что const квалификаторы используются только на стадии компиляции и информацию о них найти в smx нельзя. Собственно так же, как если бы их убрать до компиляции, результат компиляции получился бы тот же.

Из прототипа мы видим, что первый и третий параметры являются ссылками (адресами), второй параметр является ссылкой на функцию, а четвертый просто значением. Приступим к обработки аргументов вызова:
Начнем со 2-го аргумента, ссылки на функцию. Эти ссылки указывают на номер public функции внутри smx. .public секция имеет следующее содержание:

Данная система нужна, чтобы INVALID_FUNCTION имел значение 0. А ссылки будут иметь вид 1, 3, 5, 7, 9 и т.д.
В нашем вызове второй параметр имеет значение 5, значит это public с индексом 2 - Cmd_Menu.

Теперь разберемся с адресами (ссылками).
Пояснение: правильно называть это передачей аргумента по ссылке (англ. by reference), т.к. мы не можем с ним работать как с просто числом, т.е. адресом.
Первый аргумент, 2224, может обозначать адрес глобальной или статической (static) переменной, или просто адрес в .data секции, откуда берется константное значение.
Ищем в .dbg.symbols переменную с адресом 2224, и ничего не находим. Следовательно это константное значение, которое надо достать из .data секции. Смотрим туда по адресу 2224:

По этой вырезки из .data секции видно, что по адресу 2224, текст "sm_menu".
Проделываем аналогичный поиск со вторым адресом (2232), и не найдя глобальных переменных видимо по этому адресу только NUL символы. Это обозначает, что там передается пустая строка.
Пояснение: Вся память должна быть выровнена по ячейкам (4 байта). Когда в память добавляться строка, к кол-во символов в строке прибавляется один NUL символ как символ окончания строки и потом дополняется NUL символами до кол-во кратного 4. Соответственно при пустой строке будет один NUL символ, дополненный до 4-х байт, что и видно по адресу 2232.

Соединив все наши выяснения получим завершенный код функции:

Если вы видите блок оканчивающийся на инструкцию условного перехода, то это или if, или while или for. Но for и while могут быть опознаны наличием далее по коду безусловного перехода (jump) в обратном чтению кода направлении и своеобразному началу (об этом вы должны были прочитать в прошлом уроке). Резюмируем: первый блок, это if со сложным условием (составным).
Разберемся в составе условия. В конце условия:

Предыдущие операторы перехода ведут на const.pri 1, это признак того, что здесь ИЛИ условие. (В случае срабатывания перехода, устанавливается значение 1 и вызывается jzer, который как можно понять не производит перехода и выполняется код идущий после jzer.)
Теперь разберем 2 условия:

Теперь приведем псевдокод к исходному коду. 3 явно должно иметь Action тип, можем сделать или view_as(3) или используем эквивалентное значение из перечисления Plugin_Handled. И так же найдем имя функции с адресом 3404.

Блок 1:
stack -4 говорит нам о объявлении переменной размером 4 байта. Указатель вершины равен указателю кадра в начале работы функции, поэтому адрес этой локальной переменной будет "0 + -4 = -4". Найти описание локальной переменной в .dbg.symbols легче всего по codestart, он равен адресу инструкции stack - 3416.

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

Далее мы видим stor.s.pri -4, что дает нам понять, что это инициализация объявленной переменной.
Смотрим информацию о нативе 7.

По наличию точки в названии функции можно распознать метод methodmap объявления. Т.к. название методмапа и самой функции одинаковы - это конструктор, а т.к. мы знаем, что Menu производный от Handle, должны использовать new.
Теперь преобразуем первый параметр в индекс паблик функции (7 / 2 = 3) и найдем её имя (Menu_Handler). Второй параметр просто обернём в преобразователь типа. Итог

Параметры передаваемые в vararg (те что, в параметре с именем . и далее) передаются только по адресу.
С первым параметром все понятно, переходим ко второму. Найти глобальную переменную с адресом 2236 мы не смогли, поэтому обращаемся к .data секции по указному адресу.

По адресу 2236 мы видим строку "%T", значит два переданных параметра должны быть строкой с обозначением имени фразы перевода и индекс клиента. Как видим 4-ый параметр как раз и есть индекс клиента, а значит 2240 должна быть строкой. Глобальной переменной с таким адресом мы опять не находим, а значит обращаемся опять в .data и получаем "MENU_TITLE".
Т.к. это функция часть methodmap, и должна вызывать относительно объекта, то мы обрезаем первую часть имени и переносим первый параметр на её место, в итоге получая:


Блок 3:
Надеюсь к этому моменту основная мысль стала уже понятно, поэтому тут в сокращенном режиме сначала получаем

Здесь мы видим в конце неявный return, значит у самой функции он отсутствовал. Так же мы видим автоматически генерируемое освобождение стека (stack 4) от созданных ранее локальных переменных. Получается что сам блок будет

Начав анализировать байт-код, мы замечаем switch, значит здесь будет switch конструкция. Адрес после switch отсылает нас к casetbl в котором содержится описание нашего switch. Первая 2 обозначает что здесь две case конструкции. Значение 3852 это адрес default кейса, и он указывает на следующую после casetbl инструкцию, а это значит что здесь нет default случая. Первый кейс имеет число 4 и адрес его кода 3712, второй имеет число 16 и адрес его начала 3668. Как видно, адрес начала второго кейса ранее по коду, это из-за того, что в casetable кейсы записываются в обратном порядке их объявления в исходном коде, а в самом байт-коде они расположены логически правильно, поэтому первым будет кейс 16, а вторым 4. Структурно я специально разбил кейсы, но не разбивал их содержимое на блоки. Как видно, в конце каждого кейса добавляется jump инструкция указывающая на конец switch конструкции. Так же видно по load.s.pri 16 видно, что значением передаваемым в switch является второй аргумент функции.
Так же можно заметить, что между casetbl и zero.pri ничего нет, а значит return здесь отсутствует. Преобразим switch в исходный код:

Как мы видим, мы должны как-то после вызова функции в той же инструкции присвоить значение для параметра 0. Мы вполне можем сами разделить это на два блока:

Те кто хорошо читали прошлые уроки и так все знают, те кто догадался молодцы, а для остальных отвечу, то в исходном коде на этом месте была конструкция delete. Т.е. наш код:

Мы вполне могли оставить и наш прошлый вариант, т.к. данный код выполняет точно те же действия, но его байт-код будет немного отличаться.

Один case разобрали, теперь второй.
В первом блоке мы сразу же замечаем jnz 3764, и по всем признакам понимает что тут простой if со значением четвертого аргумента. Адрес нас ведет к 3му блоку, а значит в самом if только второй блок.

Изучим второй блок

По адресу 2920 находится функция PrintToChatAll. Мы могли бы её декомпилировать, но в этом нет смысла, т.к. её исходный код есть в инклудах SM. И значит 2276 - строка. Переменной с таким адресом нет, идем в .data.

Но думаю у вас уже возник вопрос "Что там делает jump 3788?" Если вы читали уроки, то знаете конечно же, но для остальных: наличие jump в конце if блока обозначает наличие else блока, и судя по адресу 3788 код с 3764 по 3788 будет внутри else.

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

Автоматизация процесса декомпиляции
Теперь, когда стало видно как происходит процесс декомпиляции и порядок действий выполняемых для этого, не сложно догадаться, что все это надо автоматизировать и запрограммировать. Большинство действий переносятся в алгоритмы, а потом в функции "как есть", к примеру получение имени натива по его индексу. Но какие-то алгоритмы, легко понятные нам людям, придется преобразовать в более простые для понимания машиной алгоритмы. В итоге весь процесс работы декомпилятора будет разделятся:
1) Предварительный парсинг байт-кода для определения сложных конструкций, таких как for, while циклы, и д.р.
2) Имитация работы виртуальной машины для преобразования кода во вложенные структуры псевдокода.
3) Трансляция псевдокода в исходный код с использованием всей сопутствующей информации сохраненной в smx (.data, .natives, .publics, .pubvars, .tags, .dbg.*).

Конец
Спасибо за прочтение.
Буду рад отзывам и вопросам по данному уроку.

Ничего она не работает открывается кансоль и сразу закрывается всё тупо краш.

Voice ippo - صوت ايبو

Program is good / Thanks You Mr Fox48 rus

Иван Лысенко

Код кривой получается :(

всё чётко работает спасибо )

Михаил Малышев

Виндовс выдает ошибку и закрывает консоль апликейшен1

Bogdan Guz

Liker_Gold and Woteraz

У меня есть ошибка при компелировани в амхх просто компилятор выдет ошибку что всьо неправельно
Что делать?Помоги пожалуйста

i love you so much man
thanks and thanks for million time

Дима Данилов

Блин жалко крашит, я уже обрадовался :(

Блокада игрок- Bodi

IBRAHIM OPTIMUS

Мне понравилось, супер-шикарный

sanek42

У кого прога работает отпишитесь пж, у меня вылетает. я вам кину файл скините мне текст кому не трудно

Декомпиляция плагинов

Программа декомпилятор Lysis Decompile AMXX, с помощью которой можно превратить amxx плагин в исходник sma.

КАК СДЕЛАТЬ ИЗ AMXX В SMA . ДЕКОМПИЛЯТОР AMXX ФАЙЛОВ. ДЕЛАЕМ ИСХОДНИК

Компиляция плагинов

В данном видео я покажу как превратить исходник sma файл в готовый для установки amxx плагин. Так же вы узнаете какой .

Читайте также: