В информатике самомодифицирующийся код ( 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 , позволяют программам создавать новый код во время выполнения и выполнять его с помощью функции eval , но не позволяют изменять существующий код. Иллюзия изменения (даже если на самом деле никакой машинный код не перезаписывается) достигается путем изменения указателей функций, как в этом примере JavaScript:
var f = функция ( x ) { return x + 1 }; // присваиваем новое определение f: f = new Function ( 'x' , 'return x + 2' );
Макросы Lisp также позволяют генерировать код времени выполнения без анализа строки, содержащей программный код.
Язык программирования Push — это генетическая система программирования , которая специально разработана для создания самомодифицирующихся программ. Хотя это не язык высокого уровня, он не такой низкий уровень, как язык ассемблера. [3]
До появления множественных окон системы командной строки могли предлагать систему меню, включающую изменение запущенного командного скрипта. Предположим, что файл скрипта DOS (или "пакетный") MENU.BAT содержит следующее: [4] [nb 1]
:начинать SHOWMENU.EXE
При запуске MENU.BAT из командной строки SHOWMENU отображает экранное меню с возможной справочной информацией, примерами использования и т. д. В конце концов пользователь делает выбор, требующий выполнения команды SOMENAME : SHOWMENU завершает работу после перезаписи файла MENU.BAT, чтобы он содержал
:начинать SHOWMENU.EXE НАЗЫВАЙТЕ КАКОЕ-НИБУДЬ .BAT GOTO начало
Поскольку интерпретатор команд 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 на единицу сделай что-нибудь с А когда ГОСУДАРСТВО должно переключиться { замените код операции «увеличение» выше на код операции «уменьшение» или наоборот }}
Обратите внимание, что двухступенчатую замену кода операции можно легко записать как «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] [nb 2]
Несмотря на это, на метауровне программы по-прежнему могут изменять свое собственное поведение, изменяя данные, хранящиеся в другом месте (см. метапрограммирование ) или используя полиморфизм .
Ядро Synthesis, представленное в докторской диссертации Алексии Массалин [ 17] [18], представляет собой крошечное ядро Unix , которое использует структурированный или даже объектно -ориентированный подход к самомодифицирующемуся коду, где код создается для отдельных кваджектов , таких как файловые дескрипторы. Генерация кода для определенных задач позволяет ядру Synthesis (как JIT-интерпретатору) применять ряд оптимизаций, таких как свертывание констант или устранение общих подвыражений .
Ядро Synthesis было очень быстрым, но было написано полностью на ассемблере. Полученное в результате отсутствие переносимости не позволило идеям оптимизации Массалина быть принятыми каким-либо производственным ядром. Однако структура методов предполагает, что они могли бы быть реализованы языком более высокого уровня , хотя и более сложным, чем существующие языки среднего уровня. Такой язык и компилятор могли бы позволить разрабатывать более быстрые операционные системы и приложения.
Пол Хаеберли и Брюс Карш возражали против «маргинализации» самомодифицирующегося кода и оптимизации в целом в пользу снижения затрат на разработку. [19]
В архитектурах без связанного кэша данных и инструкций (например, некоторые ядра SPARC , ARM и MIPS ) синхронизация кэша должна быть явно выполнена модифицирующим кодом (очистить кэш данных и сделать недействительным кэш инструкций для измененной области памяти).
В некоторых случаях короткие разделы самомодифицирующегося кода выполняются медленнее на современных процессорах. Это происходит потому, что современный процессор обычно пытается сохранить блоки кода в своей кэш-памяти. Каждый раз, когда программа переписывает часть себя, переписанная часть должна быть загружена в кэш снова, что приводит к небольшой задержке, если измененный кодлет разделяет ту же строку кэша с модифицирующим кодом, как в случае, когда измененный адрес памяти находится в пределах нескольких байтов от адреса модифицирующего кода.
Проблема недействительности кэша на современных процессорах обычно означает, что самомодифицирующийся код будет работать быстрее только в том случае, если изменение будет происходить редко, например, в случае переключения состояний внутри внутреннего цикла. [ необходима цитата ]
Большинство современных процессоров загружают машинный код перед его выполнением, что означает, что если инструкция, которая находится слишком близко к указателю инструкций , будет изменена, процессор не заметит этого, а вместо этого выполнит код таким, каким он был до изменения. См. prefetch input queue (PIQ). Процессоры ПК должны правильно обрабатывать самомодифицирующийся код по причинам обратной совместимости, но они далеки от эффективности в этом. [ необходима цитата ]
Из-за последствий для безопасности самомодифицирующегося кода все основные операционные системы тщательно удаляют такие уязвимости по мере их обнаружения. Обычно проблема заключается не в том, что программы будут намеренно изменять себя, а в том, что они могут быть злонамеренно изменены эксплойтом .
Одним из механизмов предотвращения вредоносной модификации кода является функция операционной системы W^X (от "write xor execute"). Этот механизм запрещает программе делать любую страницу памяти как доступной для записи, так и доступной для исполнения. Некоторые системы не позволяют когда-либо изменять доступную для записи страницу, делая ее доступной для исполнения, даже если разрешение на запись удалено. [ требуется цитата ] Другие системы предоставляют своего рода " черный ход ", позволяющий нескольким отображениям страницы памяти иметь разные разрешения. Относительно переносимый способ обойти 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
(NO OPERATION). […]