Skip to main content
LibreTexts - Ukrayinska

4.4: Привілеї

  • Page ID
    34548
    \( \newcommand{\vecs}[1]{\overset { \scriptstyle \rightharpoonup} {\mathbf{#1}} } \) \( \newcommand{\vecd}[1]{\overset{-\!-\!\rightharpoonup}{\vphantom{a}\smash {#1}}} \)\(\newcommand{\id}{\mathrm{id}}\) \( \newcommand{\Span}{\mathrm{span}}\) \( \newcommand{\kernel}{\mathrm{null}\,}\) \( \newcommand{\range}{\mathrm{range}\,}\) \( \newcommand{\RealPart}{\mathrm{Re}}\) \( \newcommand{\ImaginaryPart}{\mathrm{Im}}\) \( \newcommand{\Argument}{\mathrm{Arg}}\) \( \newcommand{\norm}[1]{\| #1 \|}\) \( \newcommand{\inner}[2]{\langle #1, #2 \rangle}\) \( \newcommand{\Span}{\mathrm{span}}\) \(\newcommand{\id}{\mathrm{id}}\) \( \newcommand{\Span}{\mathrm{span}}\) \( \newcommand{\kernel}{\mathrm{null}\,}\) \( \newcommand{\range}{\mathrm{range}\,}\) \( \newcommand{\RealPart}{\mathrm{Re}}\) \( \newcommand{\ImaginaryPart}{\mathrm{Im}}\) \( \newcommand{\Argument}{\mathrm{Arg}}\) \( \newcommand{\norm}[1]{\| #1 \|}\) \( \newcommand{\inner}[2]{\langle #1, #2 \rangle}\) \( \newcommand{\Span}{\mathrm{span}}\)

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

    Однак, коли програма працює, вона має ексклюзивне використання процесора. Ми бачимо, як це працює, коли ми вивчаємо процеси в наступному розділі. Забезпечення доступу програми лише до пам'яті, якою вона володіє, реалізується системою віртуальної пам'яті, яку ми розглядаємо в розділі після наступного. Суттєвим моментом є те, що апаратне забезпечення відповідає за дотримання цих правил.

    Інтерфейс системного виклику, який ми розглянули, є шлюзом до програми, що потрапляє до системних ресурсів. Змушуючи додаток запитувати ресурси через системний виклик в ядро, ядро може застосовувати правила щодо того, який тип доступу може бути надано. Наприклад, коли програма виконує системний виклик open (), щоб відкрити файл на диску, вона перевірятиме права доступу користувача щодо прав доступу та дозволити або заборонити доступ.

    Апаратний захист зазвичай можна розглядати як набір концентричних кілець навколо основного набору операцій.

    Малюнок 4.3. Кільця. Рівні привілеїв на x86

    У внутрішньому самому кільці знаходяться найбільш захищені інструкції; ті, які має бути дозволено викликати тільки ядро. Наприклад, інструкція HLT для зупинки процесора не повинна бути дозволена для запуску користувальницьким додатком, оскільки це зупинить роботу всього комп'ютера. Однак ядро повинно мати можливість викликати цю інструкцію, коли комп'ютер законно вимкнений. [11]

    Кожне внутрішнє кільце може отримати доступ до будь-яких інструкцій, захищених подальшим зовнішнім кільцем, але не захищеним додатковим кільцем. Не всі архітектури мають кілька рівнів кілець, як зазначено вище, але більшість з них передбачають принаймні рівень «ядра» та «користувача».

    Модель захисту 386 має чотири кільця, хоча більшість операційних систем (таких як Linux та Windows) використовують лише два кільця для підтримки сумісності з іншими архітектурами, які тепер дозволяють стільки дискретних рівнів захисту.

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

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

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

    Щоб зрозуміти описаний вище механізм call-gate, потрібно вивчити геніальну, але складну схему сегментації, яка використовується процесором. Первісна причина сегментації полягала в тому, щоб мати можливість використовувати більше 16 бітів, доступних в реєстрі для адреси, як показано на малюнку 4.4, «Адресація сегментації x86».

    Малюнок 4.4. Адресація сегментації x86. Сегментація розширює адресний простір процесора шляхом поділу його на шматки. Процесор зберігає спеціальні сегментні регістри, а адреси задаються сегментним регістром і зміщенням комбінації. Значення регістра сегментів додається до частини зсуву, щоб знайти кінцеву адресу.

    Коли x86 перемістився на 32-бітові регістри, схема сегментації залишилася, але в іншому форматі. Замість фіксованих розмірів сегментів, сегменти можуть бути будь-якого розміру. Це означає, що процесор повинен відстежувати всі ці різні сегменти та їх розміри, що він робить за допомогою дескрипторів. Доступні всім дескриптори сегментів зберігаються в глобальній таблиці дескрипторів або GDT для стислості. Кожен процес має ряд регістрів, які вказують на записи в GDT; це сегменти, до яких може отримати доступ процес (є також локальні таблиці дескрипторів, і всі вони взаємодіють з сегментами стану завдання, але це не важливо зараз). Загальна ситуація проілюстрована на малюнку 4.5, «х86 сегменти».

    Малюнок 4.5. Відрізки х86. Відрізки х86 в дії. Зверніть увагу, як «далекий виклик» проходить через ворота виклику, який перенаправляє на сегмент коду, що працює на нижчому рівні кільця. Єдиний спосіб змінити селектор код-сегмент, який неявно використовується для всіх кодових адрес, - це механізм виклику. Таким чином механізм call-gate гарантує, що для вибору нового дескриптора сегмента і, отже, можливо, зміни рівнів захисту, ви повинні перейти через відому точку входу.

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

    Картинка стає більш цікавою, коли при запуску коду потрібно здійснювати виклики в код, який знаходиться в іншому сегменті. Як обговорювалося в розділі під назвою «386 модель захисту», x86 робить це з кільцями, де кільце 0 є найвищим дозволом, кільце 3 є найнижчим, а внутрішні кільця можуть отримати доступ до зовнішніх кілець, але не навпаки.

    Як обговорювалося в розділі під назвою «Підвищення привілеїв», коли код ring 3 хоче перейти в код кільця 0, він, по суті, змінює свій селектор сегмента коду, щоб вказати на інший сегмент. Для цього він повинен використовувати спеціальну інструкцію далекого виклику, яка апаратне забезпечення забезпечує проходження через затвор виклику. Немає іншого способу для запущеного процесу вибрати новий дескриптор сегмента коду, і, отже, процесор почне виконувати код з відомим зміщенням в межах сегмента ring 0, який відповідає за збереження цілісності (наприклад, не читання довільного і, можливо, шкідливого коду і його виконання. Звичайно, гнусні зловмисники завжди будуть шукати способи змусити ваш код робити те, що ви цього не збиралися!).

    Це дозволяє цілу ієрархію сегментів і дозволів між ними. Можливо, ви помітили, що виклик перехресного сегмента звучить точно так само, як системний виклик. Якщо ви коли-небудь дивилися на збірку Linux x86, стандартний спосіб зробити системний виклик - int 0x80, який піднімає переривання 0x80. Переривання зупиняє процесор і переходить до елемента переривання, який потім працює так само, як затвор виклику - він змінює рівень привілеїв і відштовхує вас від іншої області коду.

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

    На сучасній системі x86 сегментація і чотирирівнева кільцева система не використовується завдяки віртуальній пам'яті, розглянутої повністю в Главі 6, Virtual Memory. Єдине, що дійсно відбувається при перемиканні сегментації - це системні виклики, які по суті переходять з режиму 3 (userspace) в режим 0 і переходять до коду обробника системних викликів всередині ядра. Таким чином, процесор забезпечує додаткові швидкі інструкції системного виклику під назвою sysenter (і sysexit, щоб отримати назад), які прискорюють весь процес над викликом int 0x80 шляхом видалення загального характеру далекого виклику - тобто можливість переходу в будь-який сегмент на будь-якому рівні кільця — і обмежує виклик лише переходом до кільцевого коду 0 на певному сегменті та зміщенні, як зберігається в регістрах.

    Оскільки загальний характер був замінений такою кількістю відомої раніше інформації, весь процес можна прискорити, і, отже, у нас є вищезгаданий швидкий системний виклик. Інша річ, яку слід зазначити, це те, що стан не зберігається, коли ядро отримує контроль. Ядро має бути обережним, щоб не знищити стан, але це також означає, що воно може зберігати лише стільки стану, скільки потрібно для виконання роботи, тому може бути набагато ефективнішим щодо цього. Це дуже філософія RISC, і ілюструє, як лінія розмивається між процесорами RISC і CISC.

    Докладніше про те, як це реалізовано в ядрі Linux, див. розділ «Бібліотека ядра».

    про іокти

    про proc, sysfs, налагодження тощо


    [11] Що відбувається, коли «неслухняний» додаток все одно викликає цю інструкцію? Апаратне забезпечення зазвичай викликає виняток, який буде включати перехід до вказаного обробника в операційній системі аналогічно обробнику системних викликів. Операційна система, ймовірно, завершить роботу програми, зазвичай видаючи користувачеві деяку помилку про те, як програма розбилася.