Як працювати з JIT

У деяких внутрішніх системах для швидкого пошуку великого бітового масиву ми в Badoo використовуємо JIT. Це дуже цікава і не найвідоміша тема. І щоб виправити таку прикру ситуацію, я переклав корисну статтю Елая Бендерскі про те, що таке JIT і як його використовувати.
Раніше я вже публікував вступну статтю з libjit для програмістів, які вже знайомі з JIT. Хоча б трохи. У тому пості я дуже коротко описав JIT, а в цьому зроблю повний огляд JIT і доповню прикладами, код яких не вимагає ніяких додаткових бібліотек.
Визначення JIT
JIT – це акронім від “Just In Time” або, якщо перекладати українською, “на льоту”. Це нам ні про що не говорить і звучить так, ніби до програмування не має жодного стосунку. Мені здається, цей опис JIT найбільше схожий на правду:
Якщо якась програма під час свого виконання створює і виконує якийсь новий код, що виконується, який не був частиною початкової програми на диску, - це JIT.
Сам же термін JIT вперше з'явився в книгах з Java Джеймса Гослінга. Айкок каже, що Гослінг перейняв цей термін в галузі промислового виробництва і почав його використовувати в ранніх 90-х. Якщо вам цікаві подробиці, прочитайте статтю Айкока. А тепер давайте подивимося, як все описане вище працює на практиці.
JIT: згенеруйте машинний код та запустіть його
Мені здається, що JIT простіше зрозуміти, якщо одразу розділити його на дві фази:
- Фаза 1: генерація машинного коду під час роботи програми
- Фаза 2: виконання машинного коду під час роботи програми
Перша фаза – це 99% усієї складності JIT. Але в той же час це найбанальніша частина процесу: це саме те, що робить звичайнийкомпілятор. Широко відомі компілятори, такі як gcc і clang/llvm, транслюють вихідники C/C++ в машинний код. Далі машинний код зазвичай зберігається у файл, але немає сенсу не залишати його в пам'яті (насправді і в gcc, і clang/llvm є готові можливості для збереження коду в пам'яті для використання його в JIT). Але в цій статті я хотів би сфокусуватись на другій фазі.
Виконання згенерованого коду
Сучасні операційні системи дуже вибіркові в тому, що програмі можна робити під час її роботи. Часи дикого заходу закінчилися з появою захищеного режиму, що дозволяє операційній системі виставляти різні права різні шматки пам'яті процесу. Тобто в "звичайному" режимі ви можете виділити пам'ять на купі, але ви не можете просто виконати код, виділений на купі, попередньо явно не попросивши про це ОС.
Я сподіваюся, всім зрозуміло, що машинний код – це дані, набір байтів. Як ось це, наприклад:
Для когось ці три байти – просто три байти, а для когось – бінарне представлення валідного x86-64 коду:
Помістити цей машинний код на згадку дуже легко. Але як зробити його виконуваним і, власне, виконати?
Подивимося на код
Далі у цій статті будуть приклади коду для POSIX-сумісної UNIX операційної системи (а саме Linux). На інших ОС (таких як Windows) код відрізнятиметься в деталях, але не підході. У всіх сучасних ОС є зручні API для того, щоб зробити те саме.
Без зайвих передмов подивимося, як динамічно створити функцію у пам'яті та виконати її. Ця функція спеціально зроблена дуже простою. У C вона виглядає так:
Ось перша спроба (повний вихідник разом з Makefile доступний у репозиторії):
Три основніетапи, які виконує цей код:
Прошу зауважити, що третій етап можливий лише тоді, коли шматок пам'яті з машинним кодом має право на виконання. Без потрібних прав виклик функції спричинив би помилку ОС (швидше за все, помилку сегментування). Це станеться, якщо, наприклад, ми виділимо m звичайним викликом malloc, який виділяє пам'ять RW, але не X.
Відвернемось на хвилинку: heap, malloc та mmap
Уважні читачі могли помітити, що я сказав про пам'ять, що виділяється mmap, як про “пам'ять з купи”. Строго кажучи, "купа" - назва для джерела пам'яті, яке використовують функції malloc, free та інші. На відміну від стека, яким управляє компілятор безпосередньо.
Але не все так просто. :-) Якщо традиційно (тобто дуже давно) malloc використовував тільки одне джерело для пам'яті, що виділяється (системний виклик sbrk ), то зараз більшість реалізацій malloc у багатьох випадках використовують mmap . Деталі відрізняються від операційної системи до операційної системи та в різних реалізаціях, але зазвичай mmap використовується для великих шматків пам'яті, а sbrk – для маленьких. Відмінність ефективності під час використання одного чи іншого способу отримання пам'яті операційної системи.
Так що називати пам'ять, отриману від mmap "пам'яттю з купи", не помилка, на мою думку, і я збираюся й надалі використовувати цю назву.
Дбаємо про безпеку
У коду вище є серйозна вразливість. Причина в блоці RWX-пам'яті, яку він виділяє – рай для експлоїтів. Давайте будемо трохи відповідальнішими. Ось трохи змінений код:
Цей приклад еквівалентний попередньому прикладу у всіх відносинах, крім одного: пам'ять спочатку виділяється з RW-правами (як і зі звичайним malloc). Це достатні права, щоб ми могли записати туди наш шматок коду. Після того,як код вже знаходиться в пам'яті, ми використовуємо mprotect , щоб змінити права з RW на RX, забороняючи запис. У результаті ефект такий самий, але ні на якому з етапів наша пам'ять не є одночасно і перезаписуваною, і виконуваною. Це добре і правильно з погляду безпеки.
Що щодо malloc?
Чи ми могли використовувати malloc замість mmap для виділення пам'яті в попередньому коді? Адже RW-пам'ять – це саме те, що нам дає malloc. Так, ми могли. Але тут більше проблем, ніж зручностей. Справа в тому, що права можна виставити лише на цілі сторінки. І, виділяючи пам'ять за допомогою malloc, нам потрібно було б вручну переконатися, що пам'ять вирівняна по межі сторінки. Mmap вирішує цю проблему таким чином, що виділяє завжди вирівняну пам'ять (бо mmap за визначенням працює тільки з цілими сторінками).
Підбиваючи підсумки
Ось чому я вважаю, що описувати JIT важливо, поділяючи дві фази. Для другої фази (яку я описав у цій статті) реалізація досить банальна та використовує стандартні API операційної системи. Для першої фази можливостей нескінченна кількість. І що саме буде в ній зрештою, залежить від конкретної програми, яку ви розробляєте.
Ви можете допомогти і перевести небагато коштів на розвиток сайту