В информатике самомодифицирующийся код ( SMC или SMoC ) — это код , который изменяет свои собственные инструкции во время выполнения — обычно для уменьшения длины пути инструкций и повышения производительности или просто для сокращения повторяющегося аналогичного кода , тем самым упрощая обслуживание . Этот термин обычно применяется только к коду, в котором самомодификация является преднамеренной, а не в ситуациях, когда код случайно изменяет себя из-за ошибки, такой как переполнение буфера .
Самомодифицирующийся код может включать перезапись существующих инструкций или создание нового кода во время выполнения и передачу управления этому коду.
Самомодификацию можно использовать как альтернативу методу «установки флагов» и условному ветвлению программы, используемому в первую очередь для уменьшения количества проверок условия.
Этот метод часто используется для условного вызова кода тестирования/отладки без необходимости дополнительных вычислительных затрат для каждого цикла ввода/вывода .
Модификации могут быть выполнены:
В любом случае модификации могут быть выполнены непосредственно в самих инструкциях машинного кода путем наложения новых инструкций на существующие (например: изменение сравнения и перехода на безусловный переход или, альтернативно, на « NOP »).
В архитектуре IBM System/360 и ее преемниках до z/Architecture инструкция EXECUTE (EX) логически накладывает второй байт целевой инструкции на младшие 8 бит регистра 1. Это обеспечивает эффект самовосстановления. модификация, хотя фактическая инструкция в памяти не изменяется.
Самомодификация может быть выполнена различными способами в зависимости от языка программирования и его поддержки указателей и/или доступа к динамическим компиляторам или интерпретаторам «движков»:
Самомодифицирующийся код довольно просто реализовать при использовании языка ассемблера . Инструкции могут динамически создаваться в памяти (или накладываться на существующий код в незащищенном хранилище программ) [1] в последовательности, эквивалентной той, которую стандартный компилятор может генерировать как объектный код . В современных процессорах могут возникнуть непредвиденные побочные эффекты для кэша ЦП , которые необходимо учитывать. Этот метод часто использовался для тестирования условий «первого раза», как в этом хорошо прокомментированном примере ассемблера IBM/360 . Он использует наложение инструкций для уменьшения длины пути инструкции на (N×1)-1, где N — количество записей в файле (-1 — затраты на выполнение наложения).
SUBRTN NOP ОТКРЫТ В ПЕРВЫЙ РАЗ ЗДЕСЬ?* NOP — x'4700'<Адрес_открытого>. OI SUBRTN+1,X'F0' ДА, ИЗМЕНИТЬ NOP НА БЕЗУСЛОВНУЮ ВЕТВЬ (47F0...) ОТКРЫТЬ ВВОД И ОТКРЫТЬ ВХОДНОЙ ФАЙЛ, ПОСКОЛЬКУ ЭТО ПЕРВЫЙ РАЗОТКРЫТО ПОЛУЧИТЬ ВВОД НОРМАЛЬНАЯ ОБРАБОТКА ВОЗОБНОВЛЯЕТСЯ ЗДЕСЬ ...
Альтернативный код может включать в себя проверку «флага» каждый раз. Безусловный переход немного быстрее, чем инструкция сравнения, а также уменьшает общую длину пути. В более поздних операционных системах для программ, находящихся в защищенном хранилище, этот метод нельзя было использовать, поэтому вместо этого использовалось изменение указателя на подпрограмму . Указатель будет находиться в динамической памяти и может быть изменен по желанию после первого прохода, чтобы обойти OPEN (необходимость сначала загрузить указатель вместо прямой ветви и ссылки на подпрограмму добавит N инструкций к длине пути, но это приведет к увеличению длины пути). будет соответствующим сокращением N для безусловного перехода, который больше не потребуется).
Ниже приведен пример на языке ассемблера Zilog Z80 . Код увеличивает регистр «B» в диапазоне [0,5]. Инструкция сравнения «CP» модифицируется в каждом цикле.
;========== ORG 0H CALL FUNC00 HALT ;========== FUNC00: LD A , 6 LD HL , label01 + 1 LD B ,( HL ) label00: INC B LD ( HL ), B label01: CP $ 0 JP NZ , label00 RET ;==========
Самомодифицирующийся код иногда используется для преодоления ограничений в наборе команд машины. Например, в наборе команд Intel 8080 невозможно ввести байт из входного порта, указанного в регистре. Входной порт статически закодирован в самой инструкции как второй байт двухбайтовой инструкции. Используя самомодифицирующийся код, можно сохранить содержимое регистра во втором байте инструкции, а затем выполнить измененную команду для достижения желаемого эффекта.
Некоторые компилируемые языки явно допускают самомодифицирующийся код. Например, команда ALTER в COBOL может быть реализована как инструкция ветвления, которая модифицируется во время выполнения. [2] Некоторые методы пакетного программирования предполагают использование самомодифицирующегося кода. Clipper и SPITBOL также предоставляют возможности для явного самомодификации. Компилятор Algol в системах B6700 предлагал интерфейс с операционной системой, посредством которого исполняемый код мог передавать текстовую строку или именованный дисковый файл компилятору Algol, а затем иметь возможность вызвать новую версию процедуры.
В интерпретируемых языках «машинный код» представляет собой исходный текст и может быть доступен для редактирования «на лету»: в SNOBOL выполняемые исходные операторы являются элементами текстового массива. Другие языки, такие как Perl и Python , позволяют программам создавать новый код во время выполнения и выполнять его с помощью функции оценки , но не позволяют изменять существующий код. Иллюзия модификации (хотя на самом деле машинный код не перезаписывается) достигается за счет изменения указателей функций, как в этом примере JavaScript:
вар f = функция ( x ) { return x + 1 }; // присваиваем f новое определение: f = new Function ( 'x' , 'return x + 2' );
Макросы Lisp также позволяют генерировать код во время выполнения без анализа строки, содержащей программный код.
Язык программирования Push — это генетическая система программирования , специально предназначенная для создания самомодифицирующихся программ. Хотя это и не язык высокого уровня, но и не такой низкий уровень, как язык ассемблера. [3]
До появления нескольких окон системы командной строки могли предлагать систему меню, включающую модификацию запущенного командного сценария. Предположим, что файл сценария DOS (или «пакетный») MENU.BAT содержит следующее: [4] [nb 1]
:начинать ШОУМЕНУ.EXE
При запуске MENU.BAT из командной строки SHOWMENU отображает экранное меню с возможной справочной информацией, примерами использования и т. д. В конце концов пользователь делает выбор, который требует выполнения команды SOMENAME : SHOWMENU завершает работу после перезаписи файла MENU.BAT, чтобы он содержал
:начинать ШОУМЕНУ.EXE ПОЗВОНИТЕ КАКИМ-ТО ИМЯ .BAT ПЕРЕЙТИ к началу
Поскольку интерпретатор команд DOS не компилирует файл сценария и затем не выполняет его, не считывает весь файл в память перед началом выполнения и не полагается на содержимое буфера записи, при выходе из SHOWMENU интерпретатор команд находит новый команду для выполнения (это вызов файла сценария SOMENAME в каталоге и по протоколу, известному SHOWMENU), и после завершения этой команды он возвращается к началу файла сценария и повторно активирует SHOWMENU, готовый к следующему выбору. . Если в меню будет выбран выход, файл будет перезаписан обратно в исходное состояние. Хотя в этом начальном состоянии метка не используется, она или эквивалентный объем текста требуется, поскольку интерпретатор команд DOS запоминает позицию байта следующей команды, когда он должен запустить следующую команду, поэтому файл перезаписывается. должен поддерживать выравнивание, чтобы начальная точка следующей команды действительно была началом следующей команды.
Помимо удобства системы меню (и возможных вспомогательных функций), эта схема означает, что система SHOWMENU.EXE не находится в памяти при активации выбранной команды, что является значительным преимуществом, когда память ограничена. [4] [5]
Интерпретаторы управляющих таблиц можно считать, в каком-то смысле, «самомодифицирующими» значениями данных, извлеченными из записей таблицы (а не специально закодированными вручную в условных операторах формы «IF inputx = 'yyy'»).
В некоторых методах доступа IBM традиционно использовались самомодифицирующиеся канальные программы , где значение, например адрес диска, считывается в область, на которую ссылается канальная программа, где оно используется более поздней командой канала для доступа к диску.
IBM SSEC , продемонстрированный в январе 1948 года, имел возможность изменять свои инструкции или иным образом обращаться с ними точно так же, как с данными. Однако на практике эта возможность использовалась редко. [6] На заре появления компьютеров самомодифицирующийся код часто использовался для уменьшения использования ограниченной памяти или повышения производительности, или того и другого. Иногда его также использовали для реализации вызовов подпрограмм и возвратов, когда набор инструкций предусматривал только простые инструкции ветвления или пропуска для изменения потока управления . [7] [8] Такое использование по-прежнему актуально в некоторых ультра- RISC- архитектурах, по крайней мере теоретически; см., например , компьютер с набором одной команды . Архитектура MIX Дональда Кнута также использовала самомодифицирующийся код для реализации вызовов подпрограмм. [9]
Самомодифицирующийся код можно использовать для различных целей:
Пример псевдокода :
повторить N раз { если СОСТОЯНИЕ равно 1 увеличить А на единицу еще уменьшить А на единицу сделай что-нибудь с А}
Самомодифицирующийся код в этом случае будет просто переписывать цикл следующим образом:
повторить N раз { увеличить A на единицу сделай что-нибудь с А когда STATE должен переключиться { замените код операции «увеличение» выше кодом операции для уменьшения или наоборот }}
Обратите внимание, что замену кода операции в двух состояниях можно легко записать как «xor var по адресу со значением «opcodeOf(Inc) xor opcodeOf(dec)».
Выбор этого решения должен зависеть от значения N и частоты изменения состояния.
Предположим, для некоторого большого набора данных необходимо рассчитать набор статистических данных, таких как среднее значение, экстремумы, расположение экстремумов, стандартное отклонение и т. д. В общей ситуации может быть возможность связать веса с данными, поэтому каждый x i связан с aw i , и вместо проверки наличия весов для каждого значения индекса могут быть две версии расчета: одна для использования с гирями и без, с одним испытанием в начале. Теперь рассмотрим еще один вариант: с каждым значением может быть связано логическое значение, указывающее, следует ли это значение пропускать или нет. Это можно решить, создав четыре пакета кода, по одному для каждой перестановки и результатов раздувания кода. В качестве альтернативы, массивы веса и пропуска можно объединить во временный массив (с нулевыми весами для пропущенных значений) за счет затрат на обработку, но все равно будет раздуваться. Однако при модификации кода к шаблону расчета статистики можно при необходимости добавить код пропуска нежелательных значений и применения весов. Не будет повторного тестирования опций, и доступ к массиву данных будет осуществляться один раз, как и к массивам весов и пропусков, если они задействованы.
Самомодифицирующийся код сложнее анализировать, чем стандартный код, и поэтому его можно использовать в качестве защиты от обратного проектирования и взлома программного обеспечения . Самомодифицирующийся код использовался для сокрытия инструкций по защите от копирования в дисковых программах 1980-х годов для таких платформ, как IBM PC и Apple II . Например, на IBM PC (или совместимом с ним ) инструкция доступа к дисководу гибких дисковint 0x13
не будет отображаться в образе исполняемой программы, но будет записана в образ памяти исполняемого файла после того, как программа начнет выполняться.
Самомодифицирующийся код также иногда используется программами, которые не хотят раскрывать свое присутствие, например компьютерными вирусами и некоторыми шеллкодами . Вирусы и шеллкоды, использующие самомодифицирующийся код, чаще всего делают это в сочетании с полиморфным кодом . Модификация фрагмента работающего кода также используется в некоторых атаках, таких как переполнение буфера .
Традиционные системы машинного обучения имеют фиксированный, заранее запрограммированный алгоритм обучения для корректировки своих параметров . Однако с 1980-х годов Юрген Шмидхубер опубликовал несколько самомодифицирующихся систем с возможностью изменения собственного алгоритма обучения. Они избегают опасности катастрофических самоперезаписей, гарантируя, что самомодификации выживут только в том случае, если они будут полезны в соответствии с заданной пользователем функцией приспособленности , ошибки или вознаграждения . [14]
Ядро Linux, в частности, широко использует самомодифицирующийся код; это делается для того, чтобы иметь возможность распространять один двоичный образ для каждой основной архитектуры (например, IA-32 , x86-64 , 32-битный ARM , ARM64 ...), одновременно адаптируя код ядра в памяти во время загрузки в зависимости от конкретного процессора. обнаружена модель, например, чтобы иметь возможность воспользоваться новыми инструкциями ЦП или обойти аппаратные ошибки. [15] [16] В меньшей степени ядро DR-DOS также оптимизирует критичные к скорости разделы самого себя во время загрузки в зависимости от базового поколения процессора. [10] [11] [количество 2]
Тем не менее, на мета-уровне программы все равно могут изменять свое поведение, изменяя данные, хранящиеся где-то еще (см. метапрограммирование ), или используя полиморфизм .
Ядро Synthesis представлено в докторской диссертации Алексии Массалин. Thesis [17] [18] представляет собой крошечное ядро Unix , использующее структурированный или даже объектно-ориентированный подход к самомодифицирующемуся коду, где код создается для отдельных объектов , таких как дескрипторы файлов. Генерация кода для конкретных задач позволяет ядру Synthesis (как мог бы JIT-интерпретатор) применять ряд оптимизаций , таких как свертывание констант или исключение общих подвыражений .
Ядро Synthesis было очень быстрым, но было полностью написано на ассемблере. В результате отсутствие переносимости не позволило идеям оптимизации Массалина быть внедрены в любое производственное ядро. Однако структура методов предполагает, что их можно охватить языком более высокого уровня , хотя и более сложным, чем существующие языки среднего уровня. Такой язык и компилятор могли бы позволить разрабатывать более быстрые операционные системы и приложения.
Пол Хэберли и Брюс Карш возражали против «маргинализации» самомодифицирующегося кода и оптимизации в целом в пользу снижения затрат на разработку. [19]
В архитектурах без связанного кеша данных и инструкций (например, некоторые ядра SPARC , ARM и MIPS ) синхронизация кеша должна выполняться явно модифицирующим кодом (очистить кеш данных и сделать недействительным кеш инструкций для измененной области памяти).
В некоторых случаях короткие участки самомодифицирующегося кода на современных процессорах выполняются медленнее. Это связано с тем, что современный процессор обычно пытается хранить блоки кода в своей кэш-памяти. Каждый раз, когда программа перезаписывает часть себя, переписанная часть должна быть снова загружена в кеш, что приводит к небольшой задержке, если модифицированный кодлет использует одну и ту же строку кэша с модифицирующим кодом, как в случае, когда измененная память Адрес находится в пределах нескольких байтов от адреса модифицирующего кода.
Проблема аннулирования кэша на современных процессорах обычно означает, что самомодифицирующийся код будет работать быстрее только тогда, когда модификация будет происходить редко, например, в случае переключения состояния внутри внутреннего цикла. [ нужна цитата ]
Большинство современных процессоров загружают машинный код перед его выполнением, а это означает, что если будет изменена инструкция, находящаяся слишком близко к указателю инструкции , процессор не заметит этого, а вместо этого выполнит код так, как он был до изменения. См. очередь ввода предварительной выборки (PIQ). Процессоры ПК должны правильно обрабатывать самомодифицирующийся код по соображениям обратной совместимости, но они далеко не эффективны в этом. [ нужна цитата ]
Из-за последствий самомодифицирующегося кода для безопасности все основные операционные системы стараются удалять такие уязвимости по мере их появления. Обычно проблема заключается не в том, что программы намеренно изменяют себя, а в том, что они могут быть злонамеренно изменены с помощью эксплойта .
Одним из механизмов предотвращения вредоносной модификации кода является функция операционной системы под названием W^X (что означает «записать или выполнить»). Этот механизм запрещает программе делать любую страницу памяти доступной для записи и выполнения. Некоторые системы предотвращают изменение страницы, доступной для записи, на исполняемую, даже если разрешение на запись удалено. [ нужна цитация ] Другие системы предоставляют своего рода « черный ход », позволяющий нескольким сопоставлениям страниц памяти иметь разные разрешения. Относительно портативный способ обойти W^X — создать файл со всеми разрешениями, а затем дважды сопоставить его с памятью. В Linux можно использовать недокументированный флаг общей памяти SysV, чтобы получить исполняемую общую память без необходимости создания файла. [ нужна цитата ]
Самомодифицирующийся код труднее читать и поддерживать, поскольку инструкции в листинге исходной программы не обязательно являются инструкциями, которые будут выполняться. Самомодификация, заключающаяся в замене указателей функций, может быть не такой загадочной, если ясно, что имена вызываемых функций являются заполнителями для функций, которые будут идентифицированы позже.
Самомодифицирующийся код можно переписать как код, который проверяет флаг и переходит к альтернативным последовательностям в зависимости от результата теста, но самомодифицирующийся код обычно работает быстрее.
Самомодифицирующийся код конфликтует с аутентификацией кода и может потребовать исключений из политик, требующих, чтобы весь код, работающий в системе, был подписан.
Модифицированный код должен храниться отдельно от его исходной формы, что противоречит решениям по управлению памятью, которые обычно удаляют код из ОЗУ и при необходимости перезагружают его из исполняемого файла.
На современных процессорах с конвейером инструкций код, который часто изменяет себя, может работать медленнее, если он изменяет инструкции, которые процессор уже прочитал из памяти в конвейер. На некоторых таких процессорах единственный способ гарантировать правильное выполнение измененных инструкций — это очистить конвейер и перечитать множество инструкций.
Самомодифицирующийся код вообще нельзя использовать в некоторых средах, например в следующих:
REP MOVSW
инструкций по умолчанию («копировать слова») в рабочем образе ядра на 32-битные REP MOVSD
инструкции («копирование двойных слов») при копировании данных из одной ячейки памяти в другую (и вдвое меньше необходимых повторений) для ускорения передачи данных на диске. Пограничные случаи , такие как нечетное количество, учтены. [10] [11]SSEC был первым действующим компьютером, способным обрабатывать собственные хранимые инструкции точно так же, как данные, модифицировать их и действовать в зависимости от результата.
[…] Первоначально
двоичная перезапись
была мотивирована необходимостью изменять части программы во время выполнения (например, исправление во время выполнения
PDP-1
в 1960-х годах) […](36 страниц)
[…] Помимо получения инструкции,
Z80
использует половину цикла для
обновления
динамического
ОЗУ
.
[…] поскольку Z80 должен тратить половину каждого цикла
выборки инструкций
на выполнение других задач, у него не так много времени для выборки
байта инструкции
, как для байта данных.
Если одна из
микросхем ОЗУ
в той области памяти, к которой осуществляется доступ, работает немного медленно, Z80 может получить неверную битовую комбинацию при получении инструкции, но получить правильную при чтении данных.
[…] встроенный тест памяти не выявляет проблемы такого типа […] это строго тест чтения/записи данных.
Во время теста все инструкции извлекаются из
ПЗУ
, а не из ОЗУ […], в результате чего
H89
проходит тест памяти, но все еще работает хаотично в некоторых программах.
[…] Это программа, которая проверяет память, перемещаясь по ОЗУ.
При этом ЦП печатает текущий адрес программы на
ЭЛТ
, а затем извлекает инструкцию по этому адресу.
Если микросхемы ОЗУ по этому адресу в порядке, ЦП перемещает тестовую программу в следующую ячейку памяти, печатает новый адрес и повторяет процедуру.
Но если одна из микросхем ОЗУ достаточно медленная, чтобы возвращать неверную битовую комбинацию, ЦП неправильно интерпретирует инструкцию и ведет себя непредсказуемо.
Однако вполне вероятно, что дисплей заблокируется, показывая адрес неисправной микросхемы.
Это сужает проблему до восьми микросхем, что является улучшением по сравнению с необходимостью проверять целых 32. […] Программа […] выполнит проверку на наличие червей, отправив инструкцию RST 7 (RESTART 7) из нижнего конца памяти. до последнего рабочего адреса.
Остальная часть программы остается неподвижной и занимается отображением текущего местоположения команды RST 7 и ее
перемещением
.
Кстати, программа называется тестом
на червяков
, потому что по мере продвижения инструкции RST 7 по памяти она оставляет после себя
скользкий след
NOP
(
НЕТ ОПЕРАЦИИ).
[…]