Похоже, я не могу найти какую-либо информацию по этому поводу, кроме того, что «MMU ЦП посылает сигнал» и «ядро направляет его в вызывающую программу, завершая ее».
Я предположил, что он, вероятно, отправляет сигнал в оболочку, а оболочка обрабатывает его, завершая ошибочный процесс и печать "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. Если бы мои предположения были верны, это было бы либо a) сбой crsh
, закрытие xterm, b) не печать "Segmentation fault"
, либо c) оба.
braden@system ~/code/crsh/ $ xterm -e ./crsh
Crap Shell> ./segfault
Segmentation fault
Crap Shell> [still running]
Думаю, вернемся к исходной точке. Я только что продемонстрировал, что это делает не оболочка, а система под ней. Как «Ошибка сегментации» вообще печатается? «Кто» это делает? Ядро? Что-то другое? Как сигнал и все его побочные эффекты распространяются от аппаратного обеспечения до возможного завершения программы?
источник
crsh
отличная идея для такого рода экспериментов. Спасибо, что сообщили нам всем об этом и идее.crsh
, я подумал, что это будет произносится как «крах». Я не уверен, что это одинаково подходящее имя.system()
делает под капотом. Оказывается,system()
будет порожден процесс оболочки! Таким образом, ваш процесс оболочки порождает другой процесс оболочки, и этот процесс оболочки (возможно,/bin/sh
или что-то в этом роде) является тем, который запускает программу. Способ/bin/sh
илиbash
работа заключается в использованииfork()
иexec()
(или другой функции вexecve()
семье).man 2 wait
, она будет включать макросыWIFSIGNALED()
иWTERMSIG()
.(WIFSIGNALED(status) && WTERMSIG(status) == 11)
чтобы он напечатал что-то глупое ("YOU DUN GOOFED AND TRIGGERED A SEGFAULT"
). Когда я запустилsegfault
программу изнутриcrsh
, она напечатала именно это. Между тем команды, которые обычно выходят, не выдают сообщение об ошибке.Ответы:
Все современные процессоры способны прерывать выполняющуюся в данный момент машинную инструкцию. Они сохраняют достаточно состояния (обычно, но не всегда, в стеке), чтобы впоследствии можно было возобновить выполнение, как будто ничего не произошло (прерванная инструкция будет перезапущена с нуля, как правило). Затем они начинают выполнять обработчик прерываний , который представляет собой просто машинный код, но размещается в специальном месте, чтобы процессор знал заранее, где он находится. Обработчики прерываний всегда являются частью ядра операционной системы: компонент, который работает с наибольшей привилегией и отвечает за контроль выполнения всех других компонентов. 1,2
Прерывания могут быть синхронными , что означает, что они запускаются самим ЦП как прямой ответ на то, что выполняла текущая инструкция, или асинхронными , что означает, что они происходят в непредсказуемое время из-за внешнего события, такого как данные, поступающие в сеть порт. Некоторые люди резервируют термин «прерывание» для асинхронных прерываний и вместо этого называют синхронные прерывания «ловушками», «ошибками» или «исключениями», но все эти слова имеют другие значения, поэтому я буду придерживаться «синхронного прерывания».
В настоящее время большинство современных операционных систем имеют представление о процессах . По своей сути, это механизм, с помощью которого компьютер может запускать более одной программы одновременно, но это также является ключевым аспектом того, как операционные системы конфигурируют защиту памяти , что является особенностью большинства (но, увы, еще не все ) современные процессоры. Это идет вместе с виртуальной памятью, которая является способностью изменять отображение между адресами памяти и фактическими местоположениями в RAM. Защита памяти позволяет операционной системе предоставлять каждому процессу свой частный фрагмент оперативной памяти, к которому имеет доступ только он. Это также позволяет операционной системе (действующей от имени какого-либо процесса) определять области ОЗУ как доступные только для чтения, исполняемые, совместно используемые группой взаимодействующих процессов и т. Д. Также будет фрагмент памяти, доступный только для ядро. 3
Пока каждый процесс обращается к памяти только так, как это разрешено центральным процессором, защита памяти невидима. Когда процесс нарушает правила, процессор сгенерирует синхронное прерывание, попросив ядро разобраться. Регулярно случается, что процесс на самом деле не нарушает правила, только ядру нужно проделать некоторую работу, прежде чем процесс можно будет продолжить. Например, если страницу памяти процесса необходимо «выселить» в файл подкачки, чтобы освободить место в ОЗУ для чего-то другого, ядро пометит эту страницу как недоступную. В следующий раз, когда процесс попытается его использовать, процессор сгенерирует прерывание защиты памяти; ядро извлечет страницу из раздела подкачки, вернет ее на прежнее место, снова отметит ее как доступную и возобновит выполнение.
Но предположим, что процесс действительно нарушил правила. Он пытался получить доступ к странице, на которой никогда не было отображено ОЗУ, или пытался выполнить страницу, которая помечена как не содержащая машинный код, или что-то еще. Семейство операционных систем, обычно известных как «Unix», использует сигналы для решения этой ситуации. 4 Сигналы похожи на прерывания, но они генерируются ядром и отправляются процессами, а не генерируются оборудованием и отправляются ядром. Процессы могут определять обработчики сигналовв своем собственном коде и сообщить ядру, где они находятся. Затем эти обработчики сигналов будут выполняться, прерывая нормальный поток управления, когда это необходимо. Все сигналы имеют номер и два имени, одно из которых является загадочным сокращением, а другое - несколько менее загадочной фразой. Сигнал, который генерируется, когда процесс нарушает правила защиты памяти, - это (по соглашению) номер 11, а его имена
SIGSEGV
и «Ошибка сегментации». 5,6Важное различие между сигналами и прерываниями заключается в том, что для каждого сигнала существует поведение по умолчанию . Если операционная система не может определить обработчики для всех прерываний, это ошибка в ОС, и весь компьютер рухнет, когда процессор попытается вызвать отсутствующий обработчик. Но процессы не обязаны определять обработчики сигналов для всех сигналов. Если ядро генерирует сигнал для процесса, и этот сигнал был оставлен в своем поведении по умолчанию, ядро просто пойдет дальше и сделает то, что по умолчанию, и не будет беспокоить процесс. Поведение большинства сигналов по умолчанию - «ничего не делать» или «завершить этот процесс и, возможно, также создать дамп ядра».
SIGSEGV
является одним из последних.Итак, подведем итог: у нас есть процесс, который нарушает правила защиты памяти. Процессор приостановил процесс и сгенерировал синхронное прерывание. Ядро выставило это прерывание и сгенерировало
SIGSEGV
сигнал для процесса. Давайте предположим, что процесс не настроил обработчик сигналаSIGSEGV
, поэтому ядро выполняет поведение по умолчанию, которое заключается в прекращении процесса. Это имеет тот же эффект, что и_exit
системный вызов: открытые файлы закрываются, память освобождается и т. Д.До этого момента ничто не распечатывало никаких сообщений, которые может видеть человек, и оболочка (или, в более общем случае, родительский процесс только что завершившегося процесса) вообще не была задействована.
SIGSEGV
идет к процессу, который нарушил правила, а не к его родителю. Следующий шаг в последовательности, однако, является то, чтобы уведомить об этом родительском процессе , что его ребенок был прекращен. Это может произойти несколько различных способов, простейшие из которых являются , когда родитель уже ждет этого уведомления, используя один изwait
системных вызовов (wait
,waitpid
,wait4
и т.д.). В этом случае ядро просто вызовет возврат этого системного вызова и предоставит родительскому процессу кодовый номер, называемый состоянием выхода., 7 Статус выхода информирует родителя, почему дочерний процесс был прерван; в этом случае он узнает, что дочерний процесс был прекращен из-за поведенияSIGSEGV
сигнала по умолчанию .Затем родительский процесс может сообщить о событии человеку, напечатав сообщение; программы оболочки почти всегда делают это. Ваш
crsh
код для этого не включен, но в любом случае это происходит, потому что подпрограмма библиотеки Csystem
запускает полнофункциональную оболочку/bin/sh
«под капотом».crsh
является прародителем в этом сценарии; отправляется уведомление родительского процесса/bin/sh
, которое печатает свое обычное сообщение. Затем/bin/sh
сам завершает работу, так как ему больше нечего делать, и реализация библиотеки Csystem
получает это уведомление о выходе. Вы можете увидеть это уведомление о выходе в своем коде, проверив возвращаемое значениеsystem
; но это не скажет вам, что процесс внука умер в результате segfault, потому что он был поглощен промежуточным процессом оболочки.Сноски
Некоторые операционные системы не реализуют драйверы устройств как часть ядра; однако все обработчики прерываний по-прежнему должны быть частью ядра, как и код, который настраивает защиту памяти, потому что аппаратное обеспечение не позволяет ничего, кроме ядра, делать эти вещи.
Может существовать программа под названием «гипервизор» или «менеджер виртуальной машины», которая даже более привилегирована, чем ядро, но для целей этого ответа ее можно считать частью аппаратного обеспечения .
Ядро - это программа , но это не процесс; это больше похоже на библиотеку. Все процессы время от времени выполняют части кода ядра в дополнение к своему собственному коду. Может быть несколько «потоков ядра», которые только исполняют код ядра, но они нас здесь не касаются.
Единственная ОС, с которой вам, скорее всего, придется иметь дело, которая не может считаться реализацией Unix, - это, конечно, Windows. Он не использует сигналы в этой ситуации. (Действительно, он не имеет сигналов; в Windows
<signal.h>
интерфейс полностью подделан библиотекой C.) Вместо этого он использует то, что называется « структурной обработкой исключений ».Некоторые нарушения защиты памяти генерируют
SIGBUS
(«Ошибка шины») вместоSIGSEGV
. Граница между ними не указана и варьируется от системы к системе. Если вы написали программу, для которой определен обработчикSIGSEGV
, вероятно, будет хорошей идеей определить тот же обработчикSIGBUS
.«Ошибка сегментации» - это имя прерывания, сгенерированного для нарушений защиты памяти одним из компьютеров, на которых работал исходный Unix , возможно, PDP-11 . « Сегментация » - это тип защиты памяти, но в настоящее время термин « ошибка сегментации » в общем относится к любому виду нарушения защиты памяти.
Все остальные способы, которыми родительский процесс может быть уведомлен о завершении дочернего процесса, заканчиваются вызовом родительского процесса
wait
и получением статуса выхода. Просто сначала что-то происходит.источник
mmap
файл в область памяти, которая больше, чем файл, и затем получаете доступ к «целым страницам» за концом файла. (POSIX в остальном довольно расплывчато, когда случается SIGSEGV / SIGBUS / SIGILL / и т. Д.)Оболочка действительно имеет отношение к этому сообщению и
crsh
косвенно вызывает оболочку, что, вероятно, и естьbash
.Я написал небольшую программу на C, которая всегда вызывает ошибки:
Когда я запускаю его из моей оболочки по умолчанию
zsh
, я получаю это:Когда я запускаю его
bash
, я получаю то, что вы отметили в своем вопросе:Я собирался написать обработчик сигнала в моем коде, я понял , что
system()
вызов библиотеки используетсяcrsh
Exec это оболочка, в/bin/sh
соответствии сman 3 system
. Это/bin/sh
почти наверняка распечатка "Ошибка сегментации", поскольку,crsh
конечно, нет.Если вы переписываете
crsh
использоватьexecve()
системный вызов для запуска программы, вы не увидите «ошибка» Сегментации строки. Это происходит из оболочки, вызваннойsystem()
.источник
execvp
и провел тест снова, чтобы обнаружить, что, хотя оболочка все еще не падает (то есть SIGSEGV никогда не отправляется в оболочку), она не печатает «Ошибка сегментации». Ничего не печатается вообще. Кажется, это указывает на то, что оболочка обнаруживает, когда ее дочерние процессы уничтожаются, и отвечает за печать «Ошибка сегментации» (или некоторый ее вариант).waitpid()
на каждом форке / exec, и он возвращает другое значение для процессов, которые имеют ошибку сегментации, чем процессы, которые выходят с 0 статусом.Это немного искаженное резюме. Сигнальный механизм Unix полностью отличается от специфичных для процессора событий, запускающих процесс.
Как правило, при обращении к неверному адресу (или при записи в область только для чтения, попытке выполнить неисполняемый раздел и т. Д.) ЦП будет генерировать какое-то специфичное для ЦП событие (в традиционных архитектурах, отличных от ВМ, это было называется нарушением сегментации, поскольку каждый «сегмент» (традиционно исполняемый «текст» только для чтения, «данные» с возможностью записи и переменной длины и стек традиционно на противоположном конце памяти) имеет фиксированный диапазон адресов - в современной архитектуре это скорее будет ошибка страницы [для не отображенной памяти] или нарушение прав доступа [для проблем с правами чтения, записи и выполнения], и я сосредоточусь на этом до конца ответа).
Теперь на этом этапе ядро может делать несколько вещей. Ошибки страницы также генерируются для памяти, которая является действительной, но не загружена (например, выгружена, или в файле mmapped, и т. Д.), И в этом случае ядро отобразит память и затем перезапустит пользовательскую программу из инструкции, которая вызвала ошибка. В противном случае он отправляет сигнал. Это не совсем «направляет [исходное событие] к программе-нарушителю», поскольку процесс установки обработчика сигналов отличается и в основном не зависит от архитектуры, в отличие от того, если программа должна была имитировать установку обработчика прерываний.
Если в пользовательской программе установлен обработчик сигналов, это означает создание стекового фрейма и установку позиции выполнения пользовательской программы в обработчике сигналов. То же самое делается для всех сигналов, но в случае нарушения сегментации, как правило, все устроено так, что, если обработчик сигнала возвращается, он перезапустит инструкцию, вызвавшую ошибку. Пользовательская программа могла исправить ошибку, например, путем сопоставления памяти с адресом-нарушителем - это зависит от архитектуры, возможно ли это). Обработчик сигнала также может перейти в другое место в программе (обычно через longjmp или с помощью исключения), чтобы прервать любую операцию, вызвавшую плохой доступ к памяти.
Если в пользовательской программе не установлен обработчик сигнала, он просто завершается. На некоторых архитектурах, если сигнал игнорируется, он может перезапускать инструкцию снова и снова, вызывая бесконечный цикл.
источник
#PF(fault-code)
(сбой страницы) или#GP(0)
(«Если эффективный адрес операнда памяти находится за пределами CS, Предел сегмента DS, ES, FS или GS. "). В 64-битном режиме пропускаются проверки предельных значений сегментов, поскольку операционные системы просто используют подкачку, а модель плоской памяти для пользовательского пространства.Ошибка сегментации - это доступ к адресу памяти, который не разрешен (не является частью процесса, или пытается записать данные только для чтения, или выполнить неисполняемые данные, ...). Это перехватывается MMU (блок управления памятью, сегодня часть ЦП), вызывая прерывание. Прерывание обрабатывается ядром, которое отправляет
SIGSEGFAULT
сигнал (см.,signal(2)
Например) процессу-нарушителю. Обработчик по умолчанию для этого сигнала сбрасывает ядро (см.core(5)
) И завершает процесс.Оболочка не имеет к этому никакого отношения.
источник