Чи є якісь розумні випадки зміни коду виконання?


119

Чи можете ви придумати будь-які законні (розумні) використання для зміни коду виконання (програма, що модифікує власний код під час виконання)?

Сучасні операційні системи, схоже, нахмурилися на програми, які роблять це, оскільки віруси використовували цю методику, щоб уникнути їх виявлення.

Все, що я можу придумати, - це якась оптимізація виконання, яка видалила б або додала якийсь код, знаючи щось під час виконання, що не може бути відоме під час компіляції.


8
У сучасних архітектурах це погано втручається в кешування і конвеєр інструкцій: самомодифікуючий код в кінцевому підсумку не змінить кеш, тому вам знадобляться бар'єри, і це, швидше за все, робить ваш код повільним. І ви не можете змінити код, який вже є в конвеєрі інструкцій. Таким чином, будь-яка оптимізація, заснована на самовиправляючому коді, повинна бути здійснена способом до запуску коду, щоб мати вплив на ефективність, який перевищує, скажімо, перевірку часу виконання.
Олександр К.

7
@Alexandre: звичайно для самовиправляючого коду внесення змін змінюється рідко (наприклад, один раз, два рази), незважаючи на те, що вони виконуються довільно кількість разів, тому одноразова вартість може бути незначною.
Тоні Делрой

7
Не впевнений, чому це позначено C або C ++, оскільки жоден механізм цього не має.
MSalters

4
@ Александр: Microsoft Office, як відомо, робить саме це. Як наслідок (?) Всі процесори x86 мають чудову підтримку для самовиправлення коду. Для інших процесорів необхідна дорога синхронізація, яка робить всю справу менш привабливою.
Mackie Messer

3
@Cawas: Зазвичай програмне забезпечення для автоматичного оновлення завантажує нові збірки та / або виконувані файли та замінює існуючі. Потім він перезапустить програмне забезпечення. Це те, що роблять firefox, adobe тощо. Самовиправлення, як правило, означає, що під час виконання програм програма переписується в пам'ять через деякі параметри і не обов'язково зберігається на диску. Наприклад, він може оптимізувати цілі шляхи коду, якщо він може інтелектуально виявити, що ці шляхи не будуть використовуватися під час цього конкретного запуску з метою прискорення виконання.
NotMe

Відповіді:


117

Існує багато дійсних випадків для зміни коду. Генерування коду під час виконання може бути корисним для:

Іноді код перекладається в код під час виконання (це називається динамічним бінарним перекладом ):

  • Емулятори, такі як Apple Rosetta, використовують цю техніку для прискорення емуляції. Інший приклад - програмне забезпечення, яке перетворює код Transmeta .
  • Складні налагоджувачі та профілі, такі як Valgrind або Pin, використовують його для інструменту коду під час його виконання.
  • До того, як розширення були зроблені до набору інструкцій x86, програмне забезпечення для віртуалізації, як VMWare, не могло безпосередньо запускати привілейований код x86 всередині віртуальних машин. Натомість їй довелося перекладати будь-які проблемні інструкції на льоту в більш відповідний спеціальний код.

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

  • Був час (давно, я знаю), коли комп'ютери не мали вказівки повертатися з підпрограми або непрямо звертатися до пам'яті. Самовиправляючий код був єдиним способом реалізації підпрограм, покажчиків та масивів .

Більше випадків зміни коду:

  • Багато налагоджувачів замінюють інструкції щодо впровадження точок прориву .
  • Деякі динамічні лінкери змінюють код під час виконання. Ця стаття надає деяку інформацію про переїзд DLL-файлів Windows, що фактично є формою модифікації коду.

10
Цей список, як видається, змішує приклади коду, який змінює сам себе, і коду, який модифікує інший код, наприклад, лінкери.
AShelly

6
@AShelly: Ну, якщо ви вважаєте, що динамічний лінкер / завантажувач є частиною коду, то він сам себе модифікує. Вони живуть в одному адресному просторі, тому я думаю, що це коректна точка зору.
Mackie Messer

1
Ок, зараз у списку розрізняють програми та системне програмне забезпечення. Сподіваюся, це має сенс. Зрештою, будь-яка класифікація є дискусійною. Все зводиться до того, що саме ви включаєте у визначення програми (або коду).
Маккі Мессер

35

Це було зроблено в комп'ютерній графіці, зокрема програмному забезпеченні для оптимізації. Під час виконання проаналізується стан багатьох параметрів і створюється оптимізована версія коду растерізатора (потенційно виключаючи багато умовних умов), що дозволяє візуалізувати графічні примітиви, наприклад трикутники, набагато швидше.


5
Цікавим є читання 3-х частинних піксематичних статей Майкла Абраша про DDJ: drdobbs.com/architecture-and-design/184405765 , drdobbs.com/184405807 , drdobbs.com/184405848 . Друга ланка (Part2) розповідає про Pixomatic зварника коду для піксельного конвеєра.
typo.pl

1
Дуже приємна стаття на цю тему. З 1984 року, але все ще добре читайте: Роб Пайк і Барт Локанті і Джон Рейзер. Компенсації апаратного програмного забезпечення для графіки Bitmap на блиску .
Mackie Messer

5
Чарльз Петцольд пояснює один подібний приклад у книзі під назвою "Красивий код": amazon.com/Beautiful-Code-Leading-Programmers-Practice/dp/…
Наваз

3
Ця відповідь говорить про генерацію коду, але питання про зміну коду ...
Timwi

3
@Timwi - він змінив код. Замість того, щоб обробляти велику ланцюжок, якщо вона розібрала форму один раз і переписала візуалізатор, щоб вона була налаштована на правильний тип форми, не перевіряючи кожного разу. Цікаво, що це зараз звичайно з кодом opencl - оскільки він складається на льоту, ви можете переписати його для конкретного випадку під час виконання
Martin Beckett

23

Однією з дійсних причин є те, що в наборі інструкцій ASM відсутні деякі необхідні інструкції, які ви могли б скласти самостійно. Приклад: На x86 немає способу створити переривання до змінної в регістрі (наприклад, зробити переривання з номером переривання в ax). Дозволені лише номери const, кодовані в коді. За допомогою самомодифікуючого коду можна наслідувати цю поведінку.


Досить справедливо. Чи є використання цієї методики? Це здається небезпечним.
Олександр К.

4
@Alexandre C.: Якщо я добре пам'ятаю, багатьом бібліотекам часу виконання (C, Pascal, ...) довелося DOS разів функціонувати, щоб виконувати переривання дзвінків. Оскільки такі функції отримують номер переривання як параметр, вам довелося надати таку функцію (звичайно, якщо число було постійним, ви могли б створити правильний код, але це не було гарантовано). І всі бібліотеки реалізували це за допомогою самомодифікуючого коду.
флоло

Ви можете використовувати корпус комутатора, щоб зробити це без зміни коду. Недоліком є ​​те, що вихідний код буде більшим
phuclv

17

Деякі компілятори використовували його для ініціалізації статичної змінної, уникаючи вартості умовного для наступного доступу. Іншими словами, вони реалізують "виконати цей код лише один раз", замінивши цей код без відключення під час першого виконання.


1
Дуже приємно, особливо якщо уникнути блокування / розблокування мютексу.
Тоні Делрой

2
Дійсно? Як це робиться для коду на основі ROM або для коду, виконаного в кодовому сегменті, захищеному від запису?
Іра Бакстер

1
@Ira Baxter: будь-який компілятор, який видає код, що пересувається, знає, що сегмент коду можна записати, принаймні під час запуску. Тож твердження "деякі компілятори використали це" все ж можливе.
MSalters

17

Є багато випадків:

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

Деякі моделі безпеки ОС означають, що самовиправляючий код не може працювати без привілеїв root / admin, що робить його недоцільним для загального використання.

З Вікіпедії:

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

У таких ОС навіть такі програми, як Java VM, потребують привілеїв root / admin для виконання свого JIT-коду. (Докладніші відомості див. На веб-сайті http://en.wikipedia.org/wiki/W%5EX )


2
Для самостійного зміни коду вам не потрібні кореневі привілеї. Ні Java VM.
Mackie Messer

Я не знав, що деякі ОС такі суворі. Але це, безумовно, має сенс у деяких додатках. Мені все ж цікаво, якщо виконання Java з привілеями root фактично підвищує безпеку ...
Mackie Messer

@Mackie: Я думаю, це має зменшити, але, можливо, він може встановити деякі дозволи на пам'ять, а потім змінити ефективний uid назад на якийсь обліковий запис користувача ...?
Тоні Делрой

Так, я б очікував, що у них з'явиться тонкодисперсний механізм для надання дозволів на сувору модель безпеки.
Mackie Messer

15

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

Так, це приклад оптимізації виконання.


Я не бачу сенсу. Якщо ви скажете, що системний виклик буде заборонений ОС, ви, ймовірно, отримаєте помилку, яку вам доведеться перевірити в коді, чи не так? Мені здається, що зміна виконуваного файлу замість повернення коду помилки - це своєрідне перенапруження.
Олександр К.

@Alexandre C.: таким чином ви зможете усунути нульові перевірки вказівників. Часто для абонента тривіально очевидно, що аргумент є дійсним.
MSalters

@Alexandre: Ви можете прочитати дослідження за посиланням. Я думаю, що вони отримали досить вражаючі скорочення, і це був би сенс: -}
Іра Бакстер

2
Для відносно тривіальних та не вв'язаних вводу / виводу системних викликів економія є значною. Наприклад, якщо ви пишете deamon для Unix, є купа системних викликів котла, які ви робите, щоб відключити stdio, налаштувати різні обробники сигналів і т. Д. Якщо ви знаєте, що параметри виклику є константами і що результати завжди будуть однаковими (наприклад, закриття stdin, наприклад), багато коду, який ви виконуєте в загальному випадку, непотрібне.
Марк Бессі

1
Якщо ви читаєте тезу, глава 8 містить кілька дійсно вражаючих цифр про нетривіальне введення / виведення реального часу для збору даних. Пам’ятаючи, що це теза середини 1980-х років, а машина, якою він працював, була 10? МГц 68000, він міг у програмі для збору аудіоданих з якістю CD (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 виконує самомодифікацію тексту програми. Видатним прикладом може бути перекладач сценаріїв V8 ECMA від Google.


5

Ще одна причина самовиправляючого коду (насправді "самогенеруючий" код) - це реалізація механізму компіляції Just-In-Time для продуктивності. Наприклад, програма, яка зчитує алгебричний вираз і обчислює його на діапазоні вхідних параметрів, може перетворити вираз у машинний код перед тим, як заявити обчислення.


5

Ви знаєте старий каштан, що між технічним і програмним забезпеченням немає логічної різниці ... можна також сказати, що між кодом і даними немає логічної різниці.

Що таке самозмінний код? Код, який містить значення в потоці виконання, щоб його можна було сприймати не як дані, а як команду. Звичайно, є теоретична точка зору у функціональних мовах, що насправді немає різниці. Я кажу про те, що це може зробити прямо в мовних імперативних мовах та укладачах / перекладачах без презумпції рівного статусу.

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

Тому навіть у звичайному випадку, коли компілятор створює кодовий простір і посилається на повністю окремий простір даних (купу), все одно можна змінювати дані, щоб явно змінити шлях виконання.


4
Немає логічної різниці, правда. Хоча я не бачив занадто багато інтегруючих мікросхем, що самозмінюються.
Іра Бакстер

@Mitch, IMO, що змінює шлях exec, не має нічого спільного з (само-) модифікацією коду. Крім того, ви плутайте дані з інформацією. Я не можу відповісти вашим коментарем на мою відповідь в LSE б / с. Мені заборонено форму там, оскільки Feabruary, протягом 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?


1

Ядро Linux має Офлайн модулі ядра , які роблять саме це.

Emacs також має цю здатність, і я її постійно використовую.

Все, що підтримує динамічну архітектуру плагінів, істотно модифікує її код під час виконання.


4
навряд чи. наявність динамічно завантажуваної бібліотеки, яка не завжди є резидентом, має дуже мало спільного з кодом, що самозмінюється.
Дов.

1

Я запускаю статистичний аналіз на основі постійно оновлюваної бази даних. Моя статистична модель записується і переписується щоразу, коли код виконується для розміщення нових даних, які стають доступними.


0

Сценарій, за якого це можна використовувати, - це програма навчання. У відповідь на введення користувача програма вивчає новий алгоритм:

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

Виникає питання, як це зробити на Java: Які можливості для самомодифікації коду Java?


-1

Найкращою версією цього може бути Lisp Macros. На відміну від макросів C, які є лише препроцесором, Lisp дозволяє вам мати доступ до всієї мови програмування в усі часи. Це про найпотужнішу особливість у Lisp і не існує жодної іншої мови.

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


2
Це насправді створює самомодифікуючий код або це просто більш потужний препроцесор (той, який буде генерувати функції)?
Брендан Лонг

@Brendan: на самому справі, але це правильний спосіб зробити попередню обробку. Тут немає модифікації коду виконання.
Олександр К.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.