Самопереробний код (англ. Self-modifying code) — програмний прийом, при якому програма створює або змінює частину свого програмного коду під час виконання. Такий код зазвичай застосовують у програмах, написаних під процесор з фон-нейманівською організацією пам'яті.

За часом проведення модифікації метод поділяється на:

  • Модифікація при ініціалізації проводиться один раз, перед запуском змінюваного коду
  • Модифікація на льоту (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). Через це тільки змінений код може виконуватися повільніше або вимагати додаткових команд для правильної роботи. Також зміна коду скидає конвейєр процесора.

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

Використання ред.

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

Інтерпретовані мови ред.

Мови Perl і 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.

Посилання ред.