Як працює помилка сегментації під кришкою?


266

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

Я припускав, що він, ймовірно, передає сигнал оболонці, і оболонка обробляє його, припиняючи процес порушення та друкуючи "Segmentation fault". Тому я перевірив це припущення, написавши надзвичайно мінімальну оболонку, яку я називаю crsh (crap shell). Ця оболонка нічого не робить, окрім як взяти введення користувача та подати його system()методу.

#include <stdio.h>
#include <stdlib.h>

int main(){
    char cmdbuf[1000];
    while (1){
        printf("Crap Shell> ");
        fgets(cmdbuf, 1000, stdin);
        system(cmdbuf);
    }
}

Тому я запустив цю оболонку в голий термінал (не bashбігаючи під ним). Тоді я перейшов до запуску програми, яка виробляє segfault. Якби мої припущення були правильними, це було б а) збоєм crsh, закриттям xterm, b) не друком "Segmentation fault", або c) обом.

braden@system ~/code/crsh/ $ xterm -e ./crsh
Crap Shell> ./segfault
Segmentation fault
Crap Shell> [still running]

Назад до квадратного, я думаю. Я щойно продемонстрував, що це не оболонка, а система внизу. Як навіть «Виник сегментації» навіть друкується? "Хто" це робить? Ядро? Щось ще? Як сигнал та всі його побічні ефекти поширюються від апаратного забезпечення до можливого припинення програми?


43
crshє чудовою ідеєю для такого роду експериментів. Дякуємо, що повідомили нам про це та ідею, що стоїть за цим.
Брюс Едігер

30
Коли я вперше побачив crsh, я думав, що це буде вимовлено "крах". Я не впевнений, чи однаково це ім'я.
jpmc26

56
Це приємний експеримент ... але ви повинні знати, що system()робиться під кришкою. Виявляється, system()породжує процес оболонки! Таким чином, ваш процес оболонки породжує інший процес оболонки, і цей процес оболонки (ймовірно, /bin/shчи щось подібне) є тим, що запускає програму. Спосіб /bin/shабо bashробота полягає у використанні fork()та exec()(або іншій функції в execve()сім'ї).
Дітріх Епп

4
@BradenBest: Саме так. Прочитайте сторінку керівництва man 2 wait, вона буде містити макроси WIFSIGNALED()та WTERMSIG().
Дітріх Епп

4
@DietrichEpp Так само, як ви сказали! Я спробував додати чек для того, (WIFSIGNALED(status) && WTERMSIG(status) == 11)щоб він надрукував щось goofy ( "YOU DUN GOOFED AND TRIGGERED A SEGFAULT"). Коли я запускав segfaultпрограму зсередини crsh, вона друкувала саме це. Тим часом команди, які виходять, зазвичай не видають повідомлення про помилку.
Бреден Кращий

Відповіді:


248

Усі сучасні процесори мають можливість перервати інструкцію, що виконується в даний час. Вони зберігають достатньо стану (зазвичай, але не завжди, у стеці), щоб дати можливість відновити виконання пізніше, як ніби нічого не сталося (перервана інструкція буде перезапущена з нуля, як правило). Потім вони починають виконувати обробник переривань , який є лише більш машинним кодом, але розміщується в спеціальному місці, щоб процесор знав, де він знаходиться заздалегідь. Обробники переривань завжди є частиною ядра операційної системи: компонент, який працює з найбільшим привілеєм і відповідає за контроль за виконанням усіх інших компонентів. 1,2

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

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

Поки кожен процес отримує доступ до пам’яті лише способами, які налаштований на процесор, захист пам’яті невидимий. Коли процес порушує правила, центральний процесор генерує синхронний перерив, просячи ядро ​​розібратися з речами. Часто трапляється, що процес насправді не порушував правила, тільки ядро ​​потрібно виконати певну роботу, перш ніж процес буде дозволено продовжувати. Наприклад, якщо сторінку пам'яті процесу потрібно "виселити" у файл swap, щоб звільнити місце в ОЗУ для чогось іншого, ядро ​​позначить цю сторінку недоступною. Наступного разу, коли процес спробує його використовувати, процесор генерує переривання захисту пам’яті; ядро витягне сторінку зі свопу, поверне її туди, де вона була, знову позначить її доступною та відновить виконання.

Але припустимо, що процес справді порушив правила. Він намагався отримати доступ до сторінки, на якій ніколи не було відображено жодної ОЗУ, або намагався виконати сторінку, яка позначена як такою, що не містить машинного коду, або іншого. Сімейство операційних систем, відомих як "Unix", всі використовують сигнали для вирішення цієї ситуації. 4 Сигнали схожі на переривання, але вони генеруються ядром і поляруються процесами, а не генеруються апаратним забезпеченням і посилаються ядром. Процеси можуть визначати обробники сигналіву власному коді та скажіть ядро, де вони є. Ці обробники сигналів будуть виконуватись, перериваючи нормальний потік управління, коли це необхідно. Усі сигнали мають число і два назви, одне з яких є криптованою абревіатурою, а інше дещо менш криптовалютною фразою. Сигнал, який генерується, коли процес порушує правила захисту пам’яті, є (за домовленістю) № 11, а його назви - SIGSEGV«Помилка сегментації». 5,6

Важливою відмінністю між сигналами та перериваннями є те, що для кожного сигналу існує поведінка за замовчуванням . Якщо операційна система не зможе визначити обробники для всіх перерв, то це помилка в ОС, і весь комп'ютер вийде з ладу, коли ЦП намагатиметься викликати відсутній обробник. Але процеси не зобов'язані визначати обробники сигналів для всіх сигналів. Якщо ядро ​​генерує сигнал для процесу, і цей сигнал був залишений за умовчанням, ядро ​​просто піде вперед і зробить все, що за замовчуванням, і не турбує процес. Більшість сигналів за замовчуванням поведінки або "нічого не роблять", або "припиняють цей процес і, можливо, також створюють основний дамп". SIGSEGVє одним із останніх.

Отже, для резюме, у нас є процес, який порушив правила захисту пам’яті. ЦП призупинив процес і створив синхронний переривання. Ядро поле, яке перериває і генерує SIGSEGVсигнал для процесу. Припустимо, що процес не встановив обробник сигналу для SIGSEGV, тому ядро ​​виконує поведінку за замовчуванням, яка полягає в припиненні процесу. Це має ті ж ефекти, що і _exitсистемний виклик: відкриті файли закриті, пам'ять розміщена тощо.

До цього моменту нічого не роздруковувало жодних повідомлень, які людина може бачити, а оболонка (або, загалом, батьківський процес процесу, який щойно закінчився) взагалі не брав участь. SIGSEGVпереходить до процесу, який порушив правила, а не його батьківського. Наступний крок в послідовності, однак, є те, щоб повідомити про це батьківському процесі , що його дитина був припинений. Це може статися кілька різних способів, найпростіші з яких є , коли батько вже чекає цього повідомлення, використовуючи один з waitсистемних викликів ( wait, waitpid, wait4і т.д.). У такому випадку ядро ​​просто спричинить повернення цього системного виклику та надасть батьківському процесу номер коду, який називається статусом виходу. 7 Статус виходу повідомляє батькові, чому дочірній процес припинено; в цьому випадку він дізнається, що дитина була припинена через поведінку SIGSEGVсигналу за замовчуванням .

Потім батьківський процес може повідомити людину про подію, надрукувавши повідомлення; програми оболонки майже завжди роблять це. Ваш crshкод не включає код для цього, але це все одно відбувається, тому що звичайна бібліотека C systemпрацює з повнофункціональною оболонкою /bin/sh"під кришкою". crshє бабуся і дідусь у цьому сценарії; поле сповіщення батьківського процесу поле /bin/sh, яке друкує його звичайне повідомлення. Потім /bin/shвін виходить, оскільки більше нічого не має, і реалізація бібліотеки C systemотримує це повідомлення про вихід. Ви можете побачити це повідомлення про вихід у своєму коді, перевіривши повернене значенняsystem; але це не скаже тобі, що процес онука загинув на сегменті, тому що його спожив проміжний процес оболонки.


Виноски

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

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

  3. Ядро - це програма , але це не процес; це більше схоже на бібліотеку. Усі процеси час від часу виконують частини коду ядра, крім власного коду. Може бути ряд "потоків ядра", які виконують лише код ядра, але вони нас тут не стосуються.

  4. Звичайно, єдина ОС, з якою ви, швидше за все, матимете справу, яка не може вважатися впровадженням Unix, - це, звичайно, Windows. Він не використовує сигналів у цій ситуації. ( На насправді, це не має сигналів, на Windows самий <signal.h>інтерфейс повністю сфальсифікована бібліотекою C) . Він використовує то , що називається « структурована обробка винятків » замість цього.

  5. Деякі порушення захисту пам’яті генерують SIGBUS(«Помилка шини») замість SIGSEGV. Лінія між ними не визначена і змінюється від системи до системи. Якщо ви написали програму, яка визначає обробник для SIGSEGV, можливо, буде хорошою ідеєю визначити той же обробник для SIGBUS.

  6. "Помилка сегментації" - це назва переривання, генерованого для порушень захисту пам'яті одним з комп'ютерів, на яких працює оригінал Unix , ймовірно, PDP-11 . " Сегментація " - це тип захисту пам'яті, але сьогодні термін " помилка сегментації " загалом відноситься до будь-якого порушення захисту пам'яті.

  7. Усі інші способи батьківського процесу можуть бути повідомлені про закінчення дитини, в кінцевому підсумку з викликом батьків waitта отримання статусу виходу. Просто щось спочатку відбувається щось інше.


@zvol: ad 2) Я не думаю, що правильно сказати, що процесор знає щось про процеси. Ви повинні сказати, що він викликає обробник переривання, який передає контроль.
користувач323094

9
@ user323094 Сучасні багатоядерні процесори насправді знають небагато про процеси; достатньо, щоб у цій ситуації вони могли призупинити лише нитку виконання, яка викликала помилку захисту пам'яті. Також я намагався не потрапляти в деталі низького рівня. З точки зору програміста користувальницького простору, найважливіше, що потрібно зрозуміти на кроці 2, - це те, що саме апаратне забезпечення виявляє порушення захисту пам'яті; менш точний розподіл праці між апаратним забезпеченням, мікропрограмним забезпеченням та операційною системою, коли мова йде про ідентифікацію "порушення процесу".
zwol

Інша тонкощі, яка може збити з пантелику наївного читача, - це «ядро надсилає порушному процесу сигнал SIGSEGV». який використовує звичайний жаргон, але насправді означає, що ядро ​​говорить про те , щоб розібратися з сигналом foo на панелі процесу (тобто код користувача не включається, якщо не встановлено обробник сигналу, питання, яке вирішує ядро). Я чомусь вважаю за краще "піднімає сигнал SIGSEGV на процесі" з цієї причини.
dmckee

2
Істотна різниця між SIGBUS (помилка шини) та SIGSEGV (помилка сегментації) полягає в наступному: SIGSEGV виникає тоді, коли процесор знає, що ви не маєте доступу до адреси (і тому він не робить жодного запиту шини пам'яті). SIGBUS виникає, коли процесор дізнається про проблему адресації лише після того, як він поставив ваш запит на його зовнішню шину адреси. Наприклад, просити фізичну адресу, на яку нічого в шині не відповідає, або просити прочитати дані про неправильно вирівняну межу (що вимагатиме отримання двох фізичних запитів замість одного)
Стюарт Кей

2
@StuartCaie Ви описуєте поведінку переривань ; Дійсно, багато процесорів роблять чітке розрізнення (хоча деякі - ні, а лінія між цими різниться). Сигнали SIGSEGV і SIGBUS, однак, НЕ достовірно відображені на цих двох умов процесора рівня. Єдиною умовою, коли POSIX вимагає SIGBUS, а не SIGSEGV, - це коли ви подаєте mmapфайл у область пам'яті, яка більша за файл, а потім отримуєте доступ до "цілих сторінок" поза кінцем файлу. (Інакше POSIX є досить невиразним, коли відбувається SIGSEGV / SIGBUS / SIGILL / тощо.)
zwol

42

Оболонка дійсно має щось спільне з цим повідомленням і crshопосередковано викликає оболонку, що, ймовірно, є bash.

Я написав невелику програму C, яка завжди простежує помилки:

#include <stdio.h>

int
main(int ac, char **av)
{
        int *i = NULL;

        *i = 12;

        return 0;
}

Коли я запускаю його зі своєї стандартної оболонки zsh, я отримую таке:

4 % ./segv
zsh: 13512 segmentation fault  ./segv

Коли я запускаю його bash, я отримую те, що ви зазначили у своєму запитанні:

bediger@flq123:csrc % ./segv
Segmentation fault

Я збирався написати обробник сигналу у своєму коді, тоді зрозумів, що system()виклик бібліотеки, що використовується crshоболонкою exec, /bin/shзгідно man 3 system. Це /bin/shмайже напевно роздруковує "Вину сегментації", оскільки, crshбезумовно, це не так.

Якщо ви перезапишете, crshщоб використовувати execve()системний виклик для запуску програми, ви не побачите рядок "Несправність сегментації". Він походить від оболонки, на яку посилається system().


5
Я щойно обговорював це з Дітріхом Еппом. Я зламав версію crsh, яка використовує execvpі знову робила тест, щоб виявити, що оболонка все ще не виходить з ладу (тобто SIGSEGV ніколи не надсилається до оболонки), вона не друкує "Segmentation Fault". Нічого не друкується взагалі. Це, мабуть, вказує на те, що оболонка виявляє, коли її дочірні процеси вбиваються, і відповідає за друк "Несправність сегментації" (або якийсь її варіант).
Бреден Кращий

2
@BradenBest - я зробив те саме, мій код стрункіший, ніж ваш код. Я взагалі не отримав жодного повідомлення, і навіть моя оболонка crappier нічого не друкує. Я використовував waitpid()кожен fork / exec, і він повертає інше значення для процесів, які мають помилку сегментації, ніж процеси, які виходять зі статусом 0.
Брюс Едігер

21

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

Це трохи підкреслений підсумок. Механізм сигналу Unix повністю відрізняється від специфічних для процесора подій, які запускають процес.

Взагалі, коли доступ до поганої адреси (або записується в область лише для читання, спроба виконати невиконаний розділ тощо), процесор генерує певну подію, пов’язану з процесором (у традиційних архітектурах, що не належать до VM, це було називається порушенням сегментації, оскільки кожен "сегмент" (традиційно виконуваний "текст", текст, який можна читати, "дані", що записується і змінної довжини, і стек, традиційно на протилежному кінці пам'яті), мав фіксований діапазон адрес - у сучасній архітектурі, швидше за все, це помилка сторінки (для незробленої пам'яті) або порушення доступу [для читання, запису та виконання дозволів], і я зосередитимусь на цьому для решти відповіді).

Тепер у цей момент ядро ​​може зробити кілька речей. Несправності сторінки також створюються для пам’яті, яка є дійсною, але не завантажується (наприклад, розміщено у вікні файлу, або у файлі mmapped тощо), і в цьому випадку ядро ​​буде відображати пам'ять, а потім перезапускати програму користувача з інструкції, яка викликала помилка. В іншому випадку він надсилає сигнал. Це не точно "спрямовує [початкову подію] на порушувальну програму", оскільки процес встановлення обробника сигналу відрізняється і, в основному, не залежить від архітектури, порівняно з тим, якби очікувалося, що програма змоделює встановлення обробника переривання.

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

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


+1, лише відповідь, що додає щось до прийнятого. Хороший опис історії "сегментації". Факт забави: x86 насправді все ще має обмежені сегменти в режимі 32-бітового захисту (із включеним підключенням або без підключення (віртуальна пам’ять)), тому вказівки щодо доступу до пам’яті можуть генерувати #PF(fault-code)(помилка сторінки) або #GP(0)(«Якщо ефективна адреса операденту пам'яті знаходиться поза CS, Обмеження сегмента DS, ES, FS або GS. "). 64-бітний режим скасовує граничну перевірку сегментів, оскільки ОС замість цього використовували пейджингові виклики та плоску модель пам'яті для простору користувача.
Пітер Кордес

Насправді, я вважаю, що більшість ОС на x86 використовують сегментовану пагінацію: купа великих сегментів всередині плоского, підказканого адресного простору. Ось так ви захищаєте та відображаєте пам’ять ядра в кожен адресний простір: кільця (рівні захисту) прив’язані до сегментів, а не до сторінок
Lorenzo Dematté

Крім того, на NT (але я хотів би знати, якщо на більшості Unixes однаково!) "Помилка сегментації" може траплятися досить часто: на початку користувальницького простору є захищений сегмент на 64 К, тому перенаправлення покажчика NULL підвищує (правильна?) помилка сегментації
Лоренцо Дематте

1
@ LorenzoDematté Так, майже або майже всі сучасні Unixes залишать шматок постійно незменених адрес на початку адресного простору, щоб отримати NULL відхилення. Це може бути досить великим - на 64-бітних системах, насправді, це може бути чотири гігабайти , так що випадкове обрізання покажчиків до 32 біт буде негайно спіймано. Однак сегментація у строгому сенсі x86 ледве не використовується; є один плоский сегмент для користувальницького простору і один для ядра, і, можливо, пара для спеціальних хитрощів, таких як отримання деякого використання з FS та GS.
zwol

1
@ LorenzoDematté NT використовує винятки, а не сигнали; у цьому випадку STATUS_ACCESS_VIOLATION.
Випадково832

18

Помилка сегментації - це доступ до адреси пам’яті, яка не дозволена (не є частиною процесу, або намагається записати дані лише для читання, або виконати невиконані дані, ...). Це потрапляє в MMU (блок управління пам’яттю, сьогодні частина процесора), викликаючи перерву. Переривання обробляється ядром, яке посилає SIGSEGFAULTсигнал (див., signal(2)Наприклад) процесу порушення. Обробник за замовчуванням для цього сигналу скидає ядро ​​(див. core(5)) І завершує процес.

Шкаралупа абсолютно не має в цьому руки.


3
Отже, ваша бібліотека C, як glibc на робочому столі, визначає рядок?
drewbenn

7
Також варто зазначити, що SIGSEGV можна обробляти / ігнорувати. Тож можна написати програму, яка не припиняється нею. Віртуальна машина Java - це один помітний приклад, який використовує внутрішньо SIGSEGV для різних цілей, про що згадується тут: stackoverflow.com/questions/3731784/…
Karol Nowak,

2
Так само в Windows, .NET не турбує додавання перевірки нульових покажчиків у більшості випадків - він просто фіксує порушення доступу (еквівалентні segfault).
іммібіс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.