Самомодифицирующийся код

Эта статья находится на начальном уровне проработки, в одной из её версий выборочно используется текст из источника, распространяемого под свободной лицензией
Материал из энциклопедии Руниверсалис

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

По времени проведения модификации метод делится на:

  • Модификация при инициализации — проводится один раз перед запуском изменяемого кода
  • Модификация на лету (on-the-fly) — изменение состояния программы во время исполнения

В обоих случаях изменение проходит непосредственно в машинном коде, когда новые инструкции перезаписывают старые (напр. условный переход JZ, JNZ, JE, JNE и т.п. заменяются на безусловный переход JMP или NOP). В наборе инструкций IBM/360 и Z/Architecture имеется инструкция EXECUTE (EX), которая перезаписывает целевую инструкцию (записанную во втором байте команды EX) самыми младшими 8 битами регистра 1. На указанных архитектурах с её помощью реализуется стандартный, законный метод временного изменения инструкций.

Назначение

Основные применения самомодифицирующегося кода:

  • В критичных к безопасности местах для усложнения исследования кода (полиморфные вирусы, некоторые типы защиты от копирования, упаковщики и т. д.).
  • В критичных к скорости местах для ускорения работы. Так, например, во время исполнения можно уменьшить длину критического пути исполнения. Вместо установки и последующей многократной проверкой флагов с условными переходами можно всего лишь изменить адрес и тип перехода в машинном коде. Многие порты движка Doom устанавливали прямо в машинном коде ширину экрана, это ускоряло отрисовку столбца[1].
  • Иногда используется для включения/отключения во время исполнения некоторой функциональности для тестирования или отладки. Так, в ОС Linux и Solaris при использовании отладочных инструментов Kprobes и DTrace в некоторые места кода ядра или программ вставляются последовательности инструкций nop. При включении инструмента некоторые из этих последовательностей заменяются на безусловный переход на процедуру отладки. Использование СМК позволяет расставить значительное количество точек, в которых возможна отладка, слабо при этом влияя на скорость исполнения с отключенной отладкой.
  • В ядре Linux и, возможно, других ОС, используются для отключения частей ядра, не нужных в данном окружении. При загрузке Linux определяет, исполняется ли он на SMP или на однопроцессорной машине. Во втором случае часть примитивов синхронизации удаляется из кода ядра.

Применимость к процессорам с гарвардской архитектурой

В гарвардской архитектуре память для кода и память для данных разделены. Соответственно, в них сильно усложняется работа самомодифицирующегося кода. Хотя архитектура x86 определена как фон-неймановская (с единой памятью кода и данных), большинство современных процессоров имеет раздельные области кэша для кода и для данных. При этом кэш кода не поддерживает запись, и при изменении закэшированного участка памяти может потребоваться либо аппаратно проведенный частичный или полный сброс кэша кода (x86), либо явная инструкция процессору на сброс кэша кода (SPARC). Из-за этого только что измененный код может исполняться медленнее либо потребовать дополнительных команд для правильной работы. Также изменение кода сбрасывает конвейер процессора.[2]

Также, некоторые идеи гарвардской архитектуры реализуются в ОС (например, Data Execution Prevention в ОС Windows, W^X в OpenBSD) и в процессорах (для x86 — бит NX и подобные). В этих реализациях отдельные фрагменты памяти могут быть помечены как неисполняемые (то есть данные) или как исполняемые, но немодифицируемые (то есть код без права на изменение). Использование самомодифицирующегося кода в таких программных окружениях усложняется, так как его приходится располагать либо в незащищенной области памяти (иногда такой областью является стэк), либо явно отключать защиту для подлежащего изменению кода.

Использование

  • JIT (Just in time — компиляция)
  • Динамическая трансляция
  • Динамическая рекомпиляция — при которой двоичный транслятор следит за частотой исполнения региона, и, если регион выполняется часто, проводится рекомпиляция этого региона с изменением его кода во время исполнения. В наиболее совершенных двоичных трансляторах может иметься до 4-5 последовательных уровней оптимизации региона.

Интерпретируемые языки

Языки Perl, PHP и Python позволяют программе создавать новый код во время выполнения и выполнять его, используя функцию eval, но не позволяют самомодифицироваться существующему коду (interactive python shell):

>>> x = 1
>>> eval('x + 1')
2
>>> eval('x')
1

Иллюзия модификации (при том, что никакой машинный код в действительности не изменяется) достигается путём изменения указателя функции, как в этом JavaScript-примере:

var f = function (x) {return x + 1};
alert(f(0)); //1

f = new Function('x', 'return x + 2'); // assign a new definition to f
alert(f(0)); //2

См. также

Примечания

  1. См., например, исходный код Doom Legacy, функция ASM_PatchRowBytes.
  2. Касперски, абзац с "Процессоры семейства Pentium .."

Ссылки