зазвичай main
Ідея написати цю статтю прийшла мені в голову, коли одного з моїх колег змусили пройти початковий курс по CS в моєму університеті. Ми з ним шукали спосіб написати коректну програму так, щоб вона проходила тести, але жоден з іспитом не міг зрозуміти, як і чому вона працює. Так я почав згадувати різні трюки в C, які бачив колись в старому коді, і один з них був дуже цікавим. Ідея цього трюку прийшла мені в голову через назву блогу «main is usually a function». Я подумав тоді: «А в яких випадках main може не бути функцією?» Давайте це з'ясуємо!
Якщо ви хочете скачати вихідний код до цієї статті, я виклав його тут. Зверніть увагу, що я писав його під 64-бітний Linux, і вам, можливо, доведеться поправити його під свою платформу.
Я, як і, гадаю, багато розробників, шукаю відповідь на питання наступним чином. Крок 1: пошук в Google. Крок 2: переходжу по кожній релевантної посиланням на першій сторінці. Якщо це не допомогло, я змінюю запит і повторюю все заново. Цього разу мені пощастило, і відповідь знайшовся при першому ж пошуку на Stackoverflow. У 1984 році коротка програма виграла IOCCC. main в ній була оголошена так: short main [] =. і це якимось чином працювало! На жаль, вона була написана під абсолютно іншу архітектуру і компілятор, і я не зміг її зібрати, щоб подивитися, що вона робить, але, судячи з того, що це був просто набір чисел, я міг припустити, що в масиві були байти скомпільованої коду, який просто містився в пам'ять на місце main.
Прийнявши версію, що код цієї програми - скомпільована функція main. представлена у вигляді масиву, давайте подивимося, чи зможемо ми написавши маленьку програму і повторити цей трюк.
Відмінно! Це спрацювало! Майже ... Отже, наша наступна мета - надрукувати що-небудь на екран. Наскільки я пам'ятав тоді асемблер, в скомпільованому коді є секція команд і секція даних. При цьому в секції команд міститься виконуваний, але не змінюваний код, а в секції даних лежать змінювані дані, які не можна виконати. У нашому випадку, ми можемо тільки заповнити код функції main. тому все, що ми покладемо в секцію даних, буде нам не бачити. Нам потрібно знайти спосіб покласти рядок «Hello world!» В функцію main і послатися на неї.
Я став думати, що можна написати за якомога меншу кількість рядків. Оскільки я знав, що збираю програму під 64-бітний Linux, я міг викликати системну команду write. яка виведе що-небудь на екран. Зараз я, звичайно, розумію, що міг тоді і не використовувати асемблер, але, в той же час, я радий, що отримав такий досвід. Починати з вбудованого асемблера в GCC було непросто, але коли я більш-менш звик, справи пішли швидше.
Спочатку було дуже важко. Виявилося, що все, що я міг дізнатися про асемблері через пошуковик, це старий Intel-івський синтаксис, причому для 32-бітної архітектури. Мені ж треба було скомпілювати код під 64-бітну систему без будь-яких спеціальних прапорів компілятора. Це означає ніяких прапорів і опцій компілятора, ніяких додаткових кроків лінковщік і вбудований в GCC синтаксис ATT. Велику частину часу я витратив на пошук інформації про асемблері для 64-бітових систем! Можливо, погано шукав. Тут я користувався здебільшого методом проб і помилок. Я всього лише хотів вивести рядок «Hello world!» На екран за допомогою вбудованого асемблера, чому це так складно? Для тих, хто хоче дізнатися, як це зробити, рекомендую поглянути на ці сайти: Linux syscall list. Intro to Inline Asm. Differences between Intel and ATT Syntax.
Зрештою, у мене почав вийдуть більш-менш виразний asm-код, який навіть працював. Згадаймо, моя мета в тому, щоб написати таку main, яка вдає із себе масив з asm-командами, які виводять на екран «Hello World».
>>> hex_string = "554889E5B801000000BB01000000BE10054000BA0D0000000F05B83C00000031DB0F0548656C6C6F20576F726C64210A5D". decode ( "hex")
>>> array. array ( 'B'. hex_string)
array ( 'B'. [85. 72. 137. 229. 184. 1. 0. 0. 0. 187. 1. 0. 0. 0. 190. 16. 5. 64. 0. 186. 13. 0 . 0. 0. 15. 5. 184. 60. 0. 0. 0. 49. 219. 15. 5. 72. 101. 108. 108. 111. 32. 87. 111. 114. 108. 100. 33 . 10. 93])
Я вважаю, якщо б мої знання bash і unix були на більш високому рівні, я б знайшов спосіб зробити це дещо простіше, але гугл у відповідь на що-небудь на зразок «hex dump of compiled function» видає кілька питань про те, як надрукувати 16 -річний дамп на різних мовах. Проте, у нас тепер є масив розділених комами чисел, що представляють нашу функцію. Спробуємо записати його в новий файл і перевіримо, чи спрацює трюк. Ось що вийшло.