Есть ли умные случаи модификации кода времени выполнения?

119

Можете ли вы представить себе какое-либо законное (умное) использование для модификации кода во время выполнения (программа, изменяющая свой собственный код во время выполнения)?

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

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

Део
источник
8
На современных архитектурах это сильно мешает кешированию и конвейеру команд: в конечном итоге самомодифицирующийся код не изменяет кеш, поэтому вам понадобятся барьеры, и это, вероятно, замедлит ваш код. И вы не можете изменять код, который уже находится в конвейере команд. Таким образом, любая оптимизация, основанная на самомодифицирующемся коде, должна выполняться задолго до запуска кода, чтобы влияние на производительность было выше, чем, например, проверка во время выполнения.
Alexandre C.
7
@Alexandre: для самомодифицирующегося кода характерно то, что модификации редко меняются (например, один или два раза), несмотря на то, что они выполняются произвольное количество раз, поэтому разовая стоимость может быть незначительной.
Тони Делрой
7
Не уверен, почему это помечено как C или C ++, поскольку ни у кого нет механизма для этого.
MSalters
4
@Alexandre: Microsoft Office, как известно, делает именно это. Как следствие (?) Все процессоры x86 имеют отличную поддержку самомодифицируемого кода. На других процессорах требуется дорогостоящая синхронизация, что делает все это менее привлекательным.
Mackie Messer
3
@Cawas: Обычно программное обеспечение с автоматическим обновлением загружает новые сборки и / или исполняемые файлы и перезаписывает существующие. Затем он перезапустит программное обеспечение. Это то, что делают firefox, adobe и т. Д. Самомодификация обычно означает, что во время выполнения код перезаписывается приложением в памяти из-за некоторых параметров и не обязательно сохраняется на диске. Например, он может оптимизировать целые пути кода, если он может разумно определить, что эти пути не будут использоваться во время этого конкретного запуска, чтобы ускорить выполнение.
NotMe 06

Ответы:

117

Есть много подходящих случаев для модификации кода. Генерация кода во время выполнения может быть полезна для:

Иногда код переводится в код во время выполнения (это называется динамической двоичной трансляцией ):

  • Эмуляторы, подобные Apple Rosetta, используют эту технику для ускорения эмуляции. Другой пример - программное обеспечение трансформации кода Transmeta .
  • Сложные отладчики и профилировщики, такие как Valgrind или Pin, используют его для инструментария вашего кода во время его выполнения.
  • До того, как в набор инструкций x86 были внесены расширения, программное обеспечение виртуализации, такое как VMWare, не могло напрямую запускать привилегированный код x86 внутри виртуальных машин. Вместо этого ему приходилось « на лету» переводить любые проблемные инструкции в более подходящий пользовательский код.

Модификация кода может использоваться для обхода ограничений набора инструкций:

  • Было время (я знаю, это было давно), когда у компьютеров не было инструкций по возврату из подпрограммы или косвенной адресации памяти. Самомодифицирующийся код был единственным способом реализовать подпрограммы, указатели и массивы .

Еще случаи модификации кода:

  • Многие отладчики заменяют инструкции для реализации точек останова .
  • Некоторые динамические компоновщики изменяют код во время выполнения. В этой статье рассказывается о перемещении библиотек DLL Windows во время выполнения, которое, по сути, является формой модификации кода.
Маки Мессер
источник
10
Этот список, кажется, смешивает примеры кода, который изменяет себя, и кода, который изменяет другой код, например, компоновщики.
AShelly
6
@AShelly: Ну, если вы считаете, что динамический компоновщик / загрузчик является частью кода, то он сам себя модифицирует. Они живут в одном адресном пространстве, поэтому я думаю, что это верная точка зрения.
Mackie Messer
1
Хорошо, теперь в списке проводится различие между программами и системным ПО. Я надеюсь это имеет смысл. В конце концов, любая классификация спорна. Все сводится к тому, что именно вы включаете в определение программы (или кода).
Mackie Messer
35

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

trenki
источник
5
Интересно прочитать статьи Майкла Абраша Pixomatic о DDJ из трех частей : drdobbs.com/architecture-and-design/184405765 , drdobbs.com/184405807 , drdobbs.com/184405848 . Вторая ссылка (часть 2) рассказывает о сварщике кода Pixomatic для пиксельного конвейера.
typo.pl
1
Очень хорошая статья по теме. С 1984 года, но все еще хорошо читаемый: Роб Пайк, Барт Локанти и Джон Райзер. Аппаратные и программные компромиссы для растровой графики на Blit .
Mackie Messer
5
Чарльз Петцольд объясняет один такой пример в книге под названием «Красивый код»: amazon.com/Beautiful-Code-Leading-Programmers-Practice/dp/…
Nawaz
3
В этом ответе говорится о генерации кода, но вопрос касается изменения кода ...
Тимви
3
@Timwi - он изменил код. Вместо того, чтобы обрабатывать большую цепочку if, он один раз проанализировал форму и переписал средство визуализации, чтобы оно было настроено на правильный тип формы без необходимости проверять каждый раз. Интересно, что теперь это обычное дело с кодом opencl - поскольку он компилируется на лету, вы можете переписать его для конкретного случая во время выполнения
Мартин Беккет,
23

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

flolo
источник
Справедливо. Есть ли польза от этой техники? Это кажется опасным.
Alexandre C.
4
@Alexandre C .: Если я правильно помню, многие библиотеки времени выполнения (C, Pascal, ...) должны были DOS раз использовать функцию для выполнения вызовов прерывания. Поскольку такая функция получает номер прерывания в качестве параметра, вы должны были предоставить такую ​​функцию (конечно, если бы номер был постоянным, вы могли бы сгенерировать правильный код, но это не было гарантировано). И все библиотеки реализовали это с помощью самомодифицирующегося кода.
flolo
Вы можете использовать корпус переключателя, чтобы сделать это без модификации кода. Уменьшение состоит в том, что выходной код будет больше
phuclv
17

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

JoeG
источник
1
Очень хорошо, особенно если это позволяет избежать блокировок / разблокировок мьютексов.
Тони Делрой
2
В самом деле? Как это работает для кода на основе ПЗУ или для кода, выполняемого в защищенном от записи сегменте кода?
Ира Бакстер
1
@Ira Baxter: любой компилятор, который генерирует перемещаемый код, знает, что сегмент кода доступен для записи, по крайней мере, во время запуска. Так что утверждение «некоторые компиляторы использовали это» все еще возможно.
MSalters
17

Случаев много:

  • Вирусы обычно использовали самомодифицирующийся код для «деобфускации» своего кода перед выполнением, но этот метод также может быть полезен для предотвращения обратного проектирования, взлома и нежелательного взлома.
  • В некоторых случаях может быть определенная точка во время выполнения (например, сразу после чтения файла конфигурации), когда известно, что - для остальной части времени жизни процесса - определенная ветвь всегда или никогда не будет использоваться: а не без нужды проверяя некоторую переменную, чтобы определить, в какую сторону перейти, сама инструкция ветвления может быть соответственно изменена
    • например, может стать известно, что будет обрабатываться только один из возможных производных типов, так что виртуальная отправка может быть заменена конкретным вызовом.
    • Обнаружив, какое оборудование доступно, можно жестко запрограммировать использование соответствующего кода.
  • Ненужный код можно заменить инструкциями без операции или перепрыгнуть через него, либо следующий бит кода можно сдвинуть прямо на место (проще, если использовать независимые от позиции коды операций)
  • Код, написанный для облегчения собственной отладки, может вводить инструкцию прерывания / сигнала / прерывания, ожидаемую отладчиком, в стратегическом месте.
  • Некоторые выражения предиката, основанные на вводе данных пользователем, могут быть скомпилированы в собственный код библиотекой.
  • Встраивание некоторых простых операций, которые не видны до выполнения (например, из динамически загружаемой библиотеки) ...
  • Условное добавление шагов самоинструментации / профилирования
  • Взломщики могут быть реализованы в виде библиотек, которые изменяют код, который их загружает (не "само" изменение в точности, но требует тех же методов и разрешений).
  • ...

Некоторые модели безопасности ОС означают, что самомодифицирующийся код не может работать без прав root / администратора, что делает его непрактичным для общего использования.

Из Википедии:

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

В таких ОС даже программам, таким как Java VM, требуются права root / admin для выполнения своего JIT-кода. (Подробнее см. Http://en.wikipedia.org/wiki/W%5EX )

Тони Делрой
источник
2
Для самомодифицирующегося кода вам не нужны права root. Виртуальная машина Java тоже.
Mackie Messer
Я не знал, что некоторые ОС настолько строги. Но в некоторых приложениях это определенно имеет смысл. Однако мне интересно, действительно ли выполнение Java с привилегиями root повышает безопасность ...
Mackie Messer
@Mackie: Я думаю, это должно уменьшить его, но, может быть, он может установить некоторые разрешения на память, а затем изменить эффективный uid на какую-то учетную запись пользователя ...?
Тони Делрой
Да, я бы ожидал, что у них будет мелкозернистый механизм предоставления разрешений, сопровождающий строгую модель безопасности.
Mackie Messer
15

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

Да, это пример оптимизации времени выполнения.

Ира Бакстер
источник
Я не понимаю сути. Если вы скажете, что системный вызов будет запрещен ОС, вы, скорее всего, получите обратно ошибку, которую вам придется проверить в коде, не так ли? Мне кажется, что изменение исполняемого файла вместо возврата кода ошибки - это своего рода излишняя инженерия.
Alexandre C.
@Alexandre C.: таким образом вы сможете исключить проверки нулевого указателя. Часто для вызывающего абонента очевидно, что аргумент действителен.
MSalters
@Alexandre: Вы можете прочитать исследование по ссылке. Я думаю, что они получили довольно впечатляющие ускорения, и это было бы сутью: -}
Ира Бакстер
2
Для относительно тривиальных системных вызовов, не связанных с вводом-выводом, экономия значительна. Например, если вы пишете демон для Unix, вы выполняете множество стандартных системных вызовов, чтобы отключить stdio, настроить различные обработчики сигналов и т. Д. Если вы знаете, что параметры вызова являются константами, а результаты всегда будут одинаковыми (например, закрытие стандартного ввода-вывода), большая часть кода, который вы выполняете в общем случае, не требуется.
Марк Бесси
1
Если вы читаете диссертацию, то в главе 8 содержатся действительно впечатляющие цифры о нетривиальном вводе-выводе в реальном времени для сбора данных. Помните, что это тезис середины 1980-х, а машина, на которой он работал, была 10? Mhz 68000, он мог программно захватывать аудиоданные с качеством компакт-диска (44000 отсчетов в секунду) с помощью простого старого программного обеспечения. Он утверждал, что рабочие станции Sun (классический Unix) могут набирать только 1/5 от этого показателя. Я старый программист на ассемблере из тех дней, и это довольно впечатляюще.
Ира Бакстер
9

Много лет назад я провел утро, пытаясь отладить некоторый самомодифицирующийся код, одна инструкция изменила целевой адрес следующей инструкции, то есть я вычислял адрес ветвления. Он был написан на ассемблере и отлично работал, когда я выполнял программу по одной инструкции за раз. Но когда я запустил программу, она не удалась. В конце концов, я понял, что машина получает 2 инструкции из памяти и (поскольку инструкции были размещены в памяти) изменяемая мной инструкция уже была получена, и, таким образом, машина выполняла неизмененную (неправильную) версию инструкции. Конечно, когда я отлаживал, он выполнял только одну инструкцию за раз.

Я хочу сказать, что самомодифицирующийся код может быть чрезвычайно неприятным для тестирования / отладки и часто имеет скрытые предположения относительно поведения машины (будь то аппаратная или виртуальная). Более того, система никогда не могла совместно использовать кодовые страницы между различными потоками / процессами, выполняемыми на (теперь) многоядерных машинах. Это сводит на нет многие преимущества виртуальной памяти и т. Д. Это также аннулирует оптимизацию ветвей, выполненную на аппаратном уровне.

(Примечание - я не включаю JIT в категорию самомодифицирующегося кода. JIT переводит из одного представления кода в альтернативное представление, это не модифицирует код)

В общем, это просто плохая идея - действительно изящная, очень непонятная, но очень плохая.

конечно - если все, что у вас есть, это 8080 и ~ 512 байт памяти, вам, возможно, придется прибегнуть к таким методам.

сойка
источник
1
Я не знаю, хорошее и плохое - неподходящие категории, чтобы думать об этом. Конечно, вы должны действительно знать, что вы делаете, а также почему вы это делаете. Но программист, написавший этот код, вероятно, не хотел, чтобы вы видели, что делает программа. Конечно, неприятно отлаживать подобный код. Но этот код, скорее всего, должен был быть таким.
Mackie Messer
Современные процессоры x86 имеют более сильное обнаружение SMC, чем требуется на бумаге: наблюдение за выборкой устаревших инструкций на x86 с самомодифицирующимся кодом . А на большинстве процессоров, отличных от x86 (например, ARM), кэш инструкций не согласуется с кешами данных, поэтому требуется ручная очистка / синхронизация, прежде чем вновь сохраненные байты могут быть надежно выполнены как инструкции. community.arm.com/processors/b/blog/posts/… . В любом случае, производительность SMC на современных процессорах ужасна , если вы не измените один раз и не запустите много раз.
Питер Кордес
7

С точки зрения ядра операционной системы каждый Just In Time Compiler и Linker Runtime выполняет самомодификацию текста программы. Ярким примером может служить интерпретатор скриптов Google V8 ECMA.

datenwolf
источник
5

Другая причина самомодифицируемого кода (на самом деле «самогенерируемого» кода) заключается в реализации механизма своевременной компиляции для повышения производительности. Например, программа, которая считывает алгебраическое выражение и вычисляет его по диапазону входных параметров, может преобразовать выражение в машинный код до того, как объявить вычисление.

Джузеппе Геррини
источник
5

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

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

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

Таким образом, даже в обычном случае, когда компилятор создает пространство кода и ссылается на полностью отдельное пространство данных (кучу), можно по-прежнему изменять данные, чтобы явно изменить путь выполнения.

Митч
источник
4
Никакой логической разницы, правда. Однако еще не видел слишком много самомодифицирующихся интегральных схем.
Ира Бакстер
@Mitch, IMO изменение пути exec не имеет ничего общего с (само) модификацией кода. Кроме того, вы путаете данные с информацией. Я не могу ответить на годовой комментарий к моему ответу в LSE , т.к. меня забанили там с февраля на 3 года (1000 дней) за то, что я выражаю в мета-LSE мое мнение о том, что американцы и британцы не владеют английским языком.
Геннадий Ванин Геннадий Ванин
4

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

Дэвид
источник
2

Одним из вариантов использования является тестовый файл EICAR, который представляет собой законный исполняемый COM-файл DOS для тестирования антивирусных программ.

X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*

Он должен использовать самостоятельную модификацию кода, потому что исполняемый файл должен содержать только печатные / печатные символы ASCII в диапазоне [21h-60h, 7Bh-7Dh], что значительно ограничивает количество кодируемых инструкций.

Подробности объяснены здесь


Он также используется для диспетчеризации операций с плавающей запятой в DOS.

Некоторые компиляторы будут CD xxвыдавать с xx в диапазоне от 0x34-0x3B вместо инструкций с плавающей запятой x87. Поскольку CDэто код операции для intинструкции, он перейдет к прерыванию 34h-3Bh и эмулирует эту инструкцию в программном обеспечении, если сопроцессор x87 недоступен. В противном случае обработчик прерывания заменит эти 2 байта на, 9B Dxтак что последующие исполнения будут обрабатываться напрямую x87 без эмуляции.

Какой протокол эмуляции с плавающей запятой x87 в MS-DOS?

phuclv
источник
1

Ядро Linux имеет Загружаемые модули ядра , которые делают именно это.

Emacs также имеет эту возможность, и я использую ее постоянно.

Все, что поддерживает динамическую архитектуру плагинов, существенно изменяет свой код во время выполнения.

dietbuddha
источник
4
едва. наличие динамически загружаемой библиотеки, которая не всегда резидентная, имеет мало общего с самомодифицирующимся кодом.
Дов
1

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

Дэвид Лебауэр
источник
0

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

  1. он ищет в существующей кодовой базе аналогичный алгоритм
  2. если в базе кода нет аналогичного алгоритма, программа просто добавляет новый алгоритм
  3. если подобный алгоритм существует, программа (возможно, с некоторой помощью со стороны пользователя) изменяет существующий алгоритм, чтобы он мог служить как старой цели, так и новой цели

Возникает вопрос, как это сделать в Java: каковы возможности для самостоятельной модификации кода Java?

Серж Рогач
источник
-1

Лучшей версией этого могут быть макросы Лиспа. В отличие от макросов C, которые представляют собой всего лишь препроцессор, Lisp позволяет вам всегда иметь доступ ко всему языку программирования. Это самая мощная функция в lisp, которой нет ни в одном другом языке.

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

Захари К.
источник
2
Создает ли это самомодифицирующийся код или это просто более мощный препроцессор (тот, который будет генерировать функции)?
Брендан Лонг,
@Brendan: на самом деле, но это правильный способ сделать предварительную обработку. Здесь нет модификации кода времени выполнения.
Александр С.