Читайте также секцию комментариев – там много интересных дополнений!
Представьте, что вы написали программу, которая может изменить сама себя и стать другой программой. Это возможно или нет?
Давайте быстро вспомним, как работает программа. Во-первых, она загружается в память компьютера. Это значит, что начиная с определенного адреса в памяти располагаются машинные инструкции, из которых состоит программа:
Во-вторых, чтобы программа начала работать, нужно "натравить" на неё процессор. У процессора есть специальный указатель, который всегда указывает на какой-то адрес. Из этого адреса процессор читает и исполняет инструкцию. Указатель сдвигается на следующий адрес, процессор читает следующую инструкцию, и т.д. Значит, тот адрес, с которого начинается программа, нужно записать в указатель процессора. Тогда процессор начнет читать инструкции и исполнять их, начиная с этого адреса. То есть программа фактически начнёт работать.
Но программа также работает с какими-то своими переменными. Скажем, если мы напишем в коде
a = 5
То будет выделен адрес памяти для переменной a, и в этот адрес будет записано число 5.
Значит, и сама программа находится в памяти, и её данные находятся в памяти. И очевидно, что данные можно читать и перезаписывать.
Чем отличаются данные, лежащие в памяти, от машинных инструкций, лежащих в памяти? Абсолютно ничем. Значит, данные можно трактовать как машинные инструкции, а машинные инструкции можно трактовать как данные?
Да, можно.
Например, число 12058880 – это машинный код, который обозначает "записать число 1 в первый регистр процессора" (неважно, это просто пример).
Стало быть, если мы напишем:
a = 12058880
То мы поместим в память, с одной стороны, число 12058880, а с другой стороны – машинную инструкцию.
Значит, код программы вполне может сгенерировать код другой программы и записать его в память. Останется только его выполнить. Если мы знаем адрес переменной a, то нужно записать её адрес в указатель инструкций процессора, и тогда процессор прочитает число 12058880 из этого адреса и выполнит его как инструкцию.
А если программа знает собственный адрес? Тогда она может записать какие-то данные прямо поверх своих инструкций, тем самым изменив саму себя.
Получается, что мы загружаем программу в память, запускаем её, потом смотрим в память – а там лежит уже другая программа.
Но не тут-то было
Раньше программы действительно могли так делать. Можно было отправить процессор выполнять созданные программой данные, или записать инструкции поверх других инструкций. Но это приводило к огромным дырам в безопасности и в частности, к разгулу вирусных программ.
Кроме того, даже если программа не была вредоносной, какая-нибудь ошибка в ней вполне могла привести к тому, что процессор попадал на область данных и начинал выполнять их как инструкции, что естественно приводило к полному краху и непредсказуемым последствиям.
В результате была придумана защита. Память была поделена на сегменты (не физические, а условные). В сегмент кода загружался и исполнялся только код, а в сегмент данных помещались только данные, и они никогда не пересекались. Программа могла делать запись только в сегмент данных. Если делалась попытка записать что-то в сегмент кода или выполнить что-то в сегменте данных – то процессор это сразу перехватывал, и выбрасывал ошибку, которая знакома всем: "Программа совершила недопустимую операцию и будет закрыта".
Тем не менее, и в такой защите время от времени находят бреши. То баги в самом процессоре, то хитроумные и непредусмотренные механизмы изменения защиты.
Но для обычных людей времена самомодифицирующихся программ давно прошли :)
Когда-то я написал игру на ассемблере, и мне нужен был генератор случайных чисел. Чтобы не возиться с ним, я сделал так, чтобы программа читала собственные машинные коды как случайные числа. Они, конечно, не были случайными, но были достаточно похожи на них.