Кільця процесора - це найяскравіша відмінність
У захищеному режимі x86 процесор завжди знаходиться в одному з 4-х кілець. Ядро Linux використовує лише 0 і 3:
- 0 для ядра
- 3 для користувачів
Це найскладніше та швидке визначення ядра проти користувача.
Чому Linux не використовує кільця 1 і 2: https://stackoverflow.com/questions/6710040/cpu-privilege-rings-why-rings-1-and-2-arent-used
Як визначається поточне кільце?
Поточне кільце вибирається комбінацією:
глобальна таблиця дескрипторів: таблиця пам'яті записів GDT, і кожен запис має поле, Privl
яке кодує кільце.
Інструкція LGDT встановлює адресу в поточній таблиці дескрипторів.
Дивіться також: http://wiki.osdev.org/Global_Descriptor_Table
сегмент реєструє CS, DS тощо, які вказують на індекс запису в GDT.
Наприклад, CS = 0
означає, що перший запис GDT в даний час активний для виконуючого коду.
Що може зробити кожне кільце?
Фізичний процесор фізично побудований так:
кільце 0 може зробити що завгодно
кільце 3 не може виконати кілька інструкцій і записати в декілька регістрів, особливо:
не може змінити власне кільце! В іншому випадку він може встановити себе на 0 і дзвінки будуть марні.
Іншими словами, не може змінювати дескриптор поточного сегмента , який визначає поточне кільце.
не вдається змінити таблиці сторінок: https://stackoverflow.com/questions/18431261/how-does-x86-paging-work
Іншими словами, не вдається змінити регістр CR3, а сама сторінка перешкоджає зміні таблиць сторінок.
Це заважає одному процесу бачити пам'ять інших процесів з безпеки / простоти програмування.
не може зареєструвати обробників переривань. Вони налаштовані записом у місця пам'яті, що також запобігається підключенням сторінки.
Обробники працюють у кільці 0 і порушують модель безпеки.
Іншими словами, не можна використовувати інструкції LGDT та LIDT.
не може зробити інструкції введення - виведення , як in
і out
, і , таким чином , мають довільний доступ до залозу.
В іншому випадку, наприклад, дозволи на файли були б марними, якби будь-яка програма могла безпосередньо читати з диска.
Точніше завдяки Майклу Петчу : насправді ОС може дозволити вказівки вводу-виводу на кільце 3, це фактично контролюється сегментом стану завдань .
Що неможливо - це, щоб кільце 3 дало собі дозвіл на це, якщо б його не було в першу чергу.
Linux завжди забороняє це. Дивіться також: https://stackoverflow.com/questions/2711044/why-doesnt-linux-use-the-hardware-context-switch-via-the-tss
Як програми та операційні системи переходять між кільцями?
коли ЦП увімкнено, він починає виконувати початкову програму в кільці 0 (добре вид, але це гарне наближення). Ви можете вважати, що ця початкова програма є ядром (але зазвичай це завантажувач, який потім викликає ядро все ще в кільці 0).
коли процес користування користувачем хоче, щоб ядро щось зробило для нього, наприклад, записати у файл, воно використовує інструкцію, яка генерує переривання, наприклад, int 0x80
абоsyscall
для сигналізації ядра. Приклад світового приводу системного виклику x86-64:
.data
hello_world:
.ascii "hello world\n"
hello_world_len = . - hello_world
.text
.global _start
_start:
/* write */
mov $1, %rax
mov $1, %rdi
mov $hello_world, %rsi
mov $hello_world_len, %rdx
syscall
/* exit */
mov $60, %rax
mov $0, %rdi
syscall
компілювати та запускати:
as -o hello_world.o hello_world.S
ld -o hello_world.out hello_world.o
./hello_world.out
GitHub вище за течією .
Коли це відбувається, процесор викликає обробник зворотного виклику переривання, який ядро зареєструвало під час завантаження. Ось конкретний бареметальний приклад, який реєструє обробник і використовує його .
Цей обробник працює в кільці 0, яке вирішує, чи дозволить ядро дозволити цю дію, зробіть дію та перезапустіть програму "userland" в кільці 3. x86_64
коли використовується exec
системний виклик (або коли ядро почнеться/init
), ядро готує регістри та пам'ять нового процесу користувальницької програми, після чого переходить до точки входу і перемикає процесор на 3
Якщо програма намагається зробити щось неслухняне, наприклад записувати в заборонений регістр або адресу пам'яті (через підкачку), процесор також викликає деякий обробник зворотних викликів ядра в кільці 0.
Але оскільки користувальницька область була неслухняною, ядро може цього разу вбити процес або надіслати його попередженням із сигналом.
Коли ядро завантажується, воно встановлює апаратний годинник з певною фіксованою частотою, яка генерує періодично переривання.
Цей апаратний годинник генерує переривання, які виконують кільце 0, і дозволяють йому планувати, які процеси користувача пробуджуються.
Таким чином, планування може відбутися, навіть якщо процеси не здійснюють жодних системних викликів.
Який сенс мати кілька кілець?
Є дві основні переваги розділення ядра та користувальницької області:
- простіше робити програми, оскільки ви впевненіші, що одна не буде заважати іншим. Наприклад, один процес користувача не повинен турбуватися про перезавантаження пам'яті іншої програми через пейджингові виклики, а також про те, щоб встановити апаратне забезпечення в недійсний стан для іншого процесу.
- це більш безпечно. Наприклад, дозволи на файли та розділення пам’яті можуть запобігти злому програми зчитування ваших банківських даних. Це, звичайно, передбачає, що ви довіряєте ядру.
Як з цим пограти?
Я створив голий металевий інструмент, який повинен бути хорошим способом безпосередньо керувати кільцями: https://github.com/cirosantilli/x86-bare-metal-examples
На жаль, я не мав терпіння зробити приклад userland, на жаль, але я все-таки зайшов у налаштування підкачки, тому поле користування повинно бути здійсненим. Я хотів би побачити прохання про тягнення.
Крім того, модулі ядра Linux працюють у кільці 0, так що ви можете використовувати їх для випробування привілейованих операцій, наприклад, читати регістри управління: https://stackoverflow.com/questions/7415515/how-to-access-the-control-registers -cr0-cr2-cr3-from-a-program-getting-segmenta / 7419306 # 7419306
Ось зручна настройка QEMU + Buildroot, щоб спробувати це, не вбиваючи хоста.
Мінусом модулів ядра є те, що інші kthreads працюють і можуть перешкоджати вашим експериментам. Але теоретично ви можете взяти на себе всі обробники переривань за допомогою модуля ядра та володіти системою, що насправді був би цікавим проектом.
Негативні кільця
Хоча на негативні дзвінки фактично не посилаються в посібнику Intel, фактично існують режими процесора, які мають додаткові можливості, ніж саме кільце 0, і тому добре підходять для імені "негативне кільце".
Одним із прикладів є режим гіпервізора, який використовується у віртуалізації.
Детальнішу інформацію див: https://security.stackexchange.com/questions/129098/what-is-protection-ring-1
ARM
В ARM кільця називаються рівнями виключення, але основні ідеї залишаються тими ж.
У ARMv8 існує 4 рівні винятків, які зазвичай використовуються як:
EL0: поле користування
EL1: ядро ("супервізор" в термінології ARM).
Входить до svc
інструкції (SuperVisor Call), раніше відомої як swi
до уніфікованої збірки , що є інструкцією, що використовується для здійснення системних викликів Linux. Привіт світу ARMv8 приклад:
.text
.global _start
_start:
/* write */
mov x0, 1
ldr x1, =msg
ldr x2, =len
mov x8, 64
svc 0
/* exit */
mov x0, 0
mov x8, 93
svc 0
msg:
.ascii "hello syscall v8\n"
len = . - msg
GitHub вище за течією .
Перевірте це за допомогою QEMU на Ubuntu 16.04:
sudo apt-get install qemu-user gcc-arm-linux-gnueabihf
arm-linux-gnueabihf-as -o hello.o hello.S
arm-linux-gnueabihf-ld -o hello hello.o
qemu-arm hello
Ось конкретний бареметальний приклад, який реєструє обробник SVC та виконує виклик SVC .
EL2: гіпервізори , наприклад Xen .
Введено в hvc
інструкцію (HyperVisor Call).
Гіпервізор - це ОС, що ОС - для користувачів.
Наприклад, Xen дозволяє одночасно запускати кілька операційних систем, таких як Linux або Windows, в одній системі, і він ізолює ОС один від одного для безпеки та зручності налагодження, як і Linux для програм користувача.
Гіпервізори є ключовою частиною сьогоднішньої хмарної інфраструктури: вони дозволяють безліччю серверів працювати на одному апаратному забезпеченні, зберігаючи використання обладнання завжди близько 100% та економлячи багато грошей.
Наприклад, AWS використовував Xen до 2017 року, коли його перехід на KVM повідомив новину .
EL3: ще один рівень. Приклад TODO
Введено smc
інструкцією (захищений режим виклику)
ARMv8 Architecture Reference Model DDI 0487C.a - Глава D1 - Модель The AArch64 System Level Програміста - Малюнок D1-1 ілюструє це красиво:
Зверніть увагу, як ARM, можливо, завдяки перевазі заднього огляду, має кращу конвенцію іменування рівнів привілеїв, ніж x86, без необхідності негативних рівнів: 0 - нижчий та 3 найвищий. Вищі рівні, як правило, створюються частіше, ніж нижчі.
Поточний EL можна запитати за допомогою MRS
інструкції: https://stackoverflow.com/questions/31787617/what-is-the-current-execution-mode-exception-level-etc
ARM не вимагає наявності всіх рівнів винятків, щоб дозволити реалізації, які не потребують функції для збереження області чіпів. ARMv8 "Рівень винятку" говорить:
Впровадження може не включати всі рівні виключень. Усі реалізації повинні включати EL0 та EL1. EL2 та EL3 необов'язкові.
Наприклад, QEMU за замовчуванням для EL1, але EL2 та EL3 можна включити за допомогою параметрів командного рядка: https://stackoverflow.com/questions/42824706/qemu-system-aarch64-entering-el1-when-emulating-a53-power-up
Фрагменти коду, протестовані на Ubuntu 18.10.