Как работает ошибка сегментации под капотом?

Я не могу найти никакой информации об этом, кроме «MMU процессора посылает сигнал», и «ядро направляет его на нарушающую программу, завершая его».

Я предположил, что он, вероятно, отправляет сигнал в оболочку, и оболочка обрабатывает его, завершая процесс нарушения и печатая «Ошибка сегментации». Поэтому я протестировал это предположение, написав чрезвычайно минимальную оболочку, которую я вызываю crsh (crap shell). Эта оболочка ничего не делает, кроме ввода пользователя и подачи его в метод system ().

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

int main () {
    char cmdbuf [1000];
    тогда как (1) {
        printf («Crap Shell>»);
        fgets (cmdbuf, 1000, stdin);
        Система (cmdbuf);
    }
}

Итак, я запустил эту оболочку на одном терминале (без bash). Затем я приступил к запуску программы, которая создает segfault. Если бы мои предположения были правильными, это означало бы: a) crash crsh, закрытие xterm, b) не печатать «Ошибка сегментации», либо c) и то, и другое.

braden @ system ~ /code /crsh /$ xterm -e ./crsh
Crap Shell> ./segfault
Ошибка сегментации
Crap Shell> [Все еще работает]

Вернемся к квадрату, я думаю. Я только что продемонстрировал, что это не оболочка, которая делает это, а система внизу. Как печатается «Ошибка сегментации»? «Кто» это делает? Ядро? Что-то другое? Как сигнал и все его побочные эффекты распространяются от аппаратного обеспечения до окончательного завершения программы?

256 голосов | спросил Braden Best 25 Jpm1000000pmMon, 25 Jan 2016 22:30:02 +030016 2016, 22:30:02

4 ответа


241

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

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

Теперь большинство современных операционных систем имеют понятие процессов . В самом основном, это механизм, при котором компьютер может запускать более одной программы одновременно, но это также ключевой аспект того, как операционные системы настраивают защита памяти , что является особенностью большинства (но, тем не менее, не всех ) современных процессоров. Он сочетается с виртуальной памятью , что позволяет изменять сопоставление между адресами памяти и фактические местоположения в ОЗУ. Защита памяти позволяет операционной системе предоставлять каждому процессу свой частный кусок ОЗУ, доступ к которому возможен только для него. Он также позволяет операционной системе (действующей от имени какого-либо процесса) назначать области ОЗУ как доступные для чтения, исполняемые, разделяемые между группой взаимодействующих процессов и т. Д. Также будет фрагмент памяти, доступ к которой возможен только с помощью ядро. 3

Пока каждый процесс обращается к памяти только тем способом, которым сконфигурирован центральный процессор, защита памяти невидима. Когда процесс нарушает правила, ЦП будет генерировать синхронное прерывание, запрашивая у ядра сортировку. Часто случается, что процесс не нарушает правила действительно , только ядро ​​должно выполнить некоторую работу до того, как процесс будет разрешен для продолжения. Например, если страница памяти процесса должна быть «выселена» в файл подкачки, чтобы освободить место в ОЗУ для чего-то еще, ядро ​​отметит эту страницу недоступной. В следующий раз, когда процесс попытается использовать его, CPU будет генерировать прерывание защиты памяти; ядро будет извлекать страницу из swap, вернуть ее туда, где она есть, пометить ее снова и возобновить выполнение.

Но предположим, что этот процесс действительно нарушил правила. Он попытался получить доступ к странице, на которой никогда не было сопоставлено ОЗУ, или пыталась выполнить страницу, помеченную как не содержащую машинный код, или что-то еще. Семейство операционных систем, обычно называемых «Unix», использует сигналы для решения этой проблемы. 4 Сигналы аналогичны прерываниям, но они генерируются ядром и полем процессами, а не генерироваться аппаратными средствами и выводиться ядром. Процессы могут определять обработчики сигналов в своем собственном коде и сообщать ядру, где они есть. Затем эти обработчики сигналов будут выполняться, прерывая нормальный поток управления, когда это необходимо. У всех сигналов есть число и два имени, один из которых является загадочным аббревиатурой, а другой - чуть менее загадочной фразой. Сигнал, который генерируется, когда процесс нарушает правила защиты памяти, является (условно) номером 11, а его имена - это SIGSEGV и «Ошибка сегментации». 5,6

Важное различие между сигналами и прерываниями заключается в том, что для каждого сигнала существует поведение по умолчанию . Если операционная система не может определить обработчики для всех прерываний, это ошибка в ОС, и весь компьютер выйдет из строя, когда процессор попытается вызвать отсутствующий обработчик. Но процессы не обязаны определять обработчики сигналов для всех сигналов. Если ядро ​​генерирует сигнал для процесса, и этот сигнал остался по умолчанию, ядро ​​будет просто идти вперед и делать все, что по умолчанию, и не беспокоить процесс. Большинство поведения по умолчанию для сигналов либо «ничего не делают», либо «завершают этот процесс и, возможно, также создают основной дамп». SIGSEGV является одним из последних.

Итак, чтобы повторить, у нас есть процесс, который нарушил правила защиты памяти. Процессорприостановил процесс и создал синхронное прерывание. Ядро вывело это прерывание и сгенерировало сигнал SIGSEGV для процесса. Предположим, что процесс not установил обработчик сигнала для SIGSEGV, поэтому ядро ​​выполняет поведение по умолчанию, которое заключается в завершении процесса. Это имеет все те же последствия, что и системный вызов _exit : открытые файлы закрыты, память освобождается и т. д.

До этого момента ничто не печатало никаких сообщений, которые человек мог видеть, и оболочка (или, в более общем плане, родительский процесс только что завершившегося процесса) не была задействована вообще. SIGSEGV переходит к процессу, который нарушил правила, not его родительский. Тем не менее, шаг next в последовательности состоит в том, чтобы уведомить родительский процесс о завершении его дочернего процесса. Это может произойти несколькими различными способами, наиболее простым из которых является то, что родитель уже ожидает этого уведомления, используя один из < code> wait (wait, waitpid, wait4 и т. д.). В этом случае ядро ​​просто вызовет возврат системного вызова и предоставит родительскому процессу номер кода с именем статус выхода . 7 Статус выхода сообщает родительскому почему дочерний процесс был прерван; в этом случае он узнает, что ребенок был прерван из-за поведения по умолчанию сигнала SIGSEGV.

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


Сноски

  1. Некоторые операционные системы не реализуют драйверы устройств как часть ядра; однако все обработчики прерываний по-прежнему должны быть частью ядра, а также код, который настраивает защиту памяти, потому что аппаратное обеспечение не позволяет чему-либо , но делать это.

  2. Может быть программа под названием «гипервизор» или «диспетчер виртуальных машин», которая является еще более привилегированной, чем ядро, но для целей этого ответа ее можно считать частью аппаратного обеспечения .

  3. Ядро - это программа , но это not процесс; это больше похоже на библиотеку. Все процессы выполняют часть кода ядра, время от времени, в дополнение к их собственному коду. Может существовать ряд «потоков ядра», в которых only выполняется код ядра, но они здесь не касаются.

  4. Единственной ОС, с которой вам, скорее всего, придется иметь дело, что не может считаться реализацией Unix, конечно же, Windows. В этой ситуации он не использует сигналы. (Действительно, он не имеет имеет сигналы, а в Windows интерфейс <signal.h> полностью подделан библиотекой C.) Он использует нечто, называемое " обработка структурированных исключений ".

  5. Некоторые нарушения защиты памяти генерируют SIGBUS («Ошибка шины») вместо SIGSEGV. Линия между двумя указанными ниже и варьируется от системы к системе. Если вы написали программу, которая определяет обработчик для SIGSEGV, вероятно, неплохо определить один и тот же обработчик для SIGBUS.

  6. «Ошибка сегментации» - это имя прерывания, сгенерированного для нарушений защиты памяти, одним из компьютеров, на которых выполнялся оригинальной Unix , возможно, PDP-11 . « Сегментация » - это тип защиты памяти, но в настоящее время термин «сегментация fault "в общем случае относится к любому виду нарушений защиты памяти.

  7. Все другие могут уведомлять родительский процесс о завершении дочернего процесса, в конечном итоге с родительским вызовом wait и получением статуса выхода. Этопросто сначала происходит что-то другое.

ответил zwol 26 Jam1000000amTue, 26 Jan 2016 05:03:23 +030016 2016, 05:03:23
41

Оболочка действительно имеет какое-то отношение к этому сообщению, а crsh косвенно вызывает оболочку, которая, вероятно, bash.

Я написал небольшую программу на C, которая всегда содержит ошибки:

#include <stdio.h>

ИНТ
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
Ошибка сегментации

Я собирался написать обработчик сигнала в своем коде, тогда я понял, что вызов библиотеки system (), используемый crsh exec, является оболочкой, /bin /sh в соответствии с man 3 system. Этот код /bin /sh почти наверняка распечатывает «Ошибка сегментации», поскольку crsh, конечно, нет.

Если вы перезапишите crsh, чтобы использовать системный вызов execve () для запуска программы, вы не увидите строку «Ошибка сегментации». Он исходит из оболочки, вызванной system ().

ответил Bruce Ediger 25 Jpm1000000pmMon, 25 Jan 2016 23:59:14 +030016 2016, 23:59:14
19
  

Я не могу найти никакой информации об этом, кроме «MMU процессора посылает сигнал», и «ядро направляет его на нарушающую программу, завершая его».

Это немного искаженное резюме. Механизм сигнала Unix полностью отличается от событий, специфичных для процессора, которые запускают процесс.

В общем случае, когда доступ к плохим адресам (или записанный в область только для чтения, попытка выполнить не исполняемый раздел и т. д.), ЦП будет генерировать некоторое событие, специфичное для процессора (на традиционной не-VM архитектуры, это называлось нарушением сегментации, поскольку каждый «сегмент» (традиционно исполняемый «только текст» для чтения, записываемые и переменные длины «данные» и стек традиционно на противоположном конце памяти) имели фиксированный диапазон адресов - в современной архитектуре, скорее всего, это ошибка страницы (для немаркированной памяти) или нарушение доступа [для чтения, записи и выполнения разрешений], и я сосредоточусь на этом для остальной части ответа) .

Теперь, на этом этапе, ядро ​​может сделать несколько вещей. Ошибки страницы также генерируются для памяти, которая действительна, но не загружается (например, поменяется или в файл mmapped и т. Д.), И в этом случае ядро ​​будет отображать память и затем перезапускать пользовательскую программу из инструкции, вызвавшей ошибка. В противном случае он посылает сигнал. Это не совсем «прямое [исходное событие] для нарушающей программы», поскольку процесс установки обработчика сигнала отличается и в основном независим от архитектуры, и если программа должна была имитировать установку обработчика прерываний.

Если в пользовательской программе установлен обработчик сигнала, это означает создание кадра стека и установку позиции выполнения пользовательской программы в обработчик сигнала. То же самое делается для всех сигналов, но в случае нарушения сегментации вещи обычно устроены так, что если обработчик сигнала вернется, он перезапустит инструкцию, вызвавшую ошибку. Пользовательская программа, возможно, исправила ошибку, например. путем сопоставления памяти с оскорбительным адресом - это зависит от архитектуры, независимо от того, возможно ли это). Обработчик сигнала также может перейти в другое место в программе (обычно через longjmp или путем исключения исключения), чтобы прервать любую операцию, вызвавшую плохой доступ к памяти.

Если пользовательская программа не имеет установленного обработчика сигналов, она просто завершается. На некоторых архитектурах, если сигнал игнорируется, он может перезапускать команду снова и снова, вызывая бесконечный цикл.

ответил Random832 26 Jpm1000000pmTue, 26 Jan 2016 19:25:07 +030016 2016, 19:25:07
17

Ошибка сегментации - это доступ к адресу памяти, который не разрешен (не является частью процесса или пытается записать данные только для чтения или выполнять неисполняемые данные, ...). Это улавливается MMU (блок управления памятью, сегодня часть процессора), вызывая прерывание. Прерывание обрабатывается ядром, которое отправляет сигнал SIGSEGFAULT (см. сигнал (2)), к процессу нарушения. Обработчик по умолчанию для этого сигнала сбрасывает ядро ​​(см. core (5)) и завершает процесс.

В этой оболочке нет никакой руки.

ответил vonbrand 25 Jpm1000000pmMon, 25 Jan 2016 23:09:43 +030016 2016, 23:09:43

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132