Não consigo encontrar nenhuma informação sobre isso além de "o MMU da CPU envia um sinal" e "o kernel o direciona para o programa incorreto, encerrando-o".
Eu assumi que provavelmente ele envia o sinal para o shell, e o shell lida com ele encerrando o processo e a impressão incorretos "Segmentation fault"
. Então, testei essa suposição escrevendo um shell extremamente mínimo que chamo de crsh (casca de merda). Esse shell não faz nada, exceto pegar a entrada do usuário e alimentá-la com o system()
método
#include <stdio.h>
#include <stdlib.h>
int main(){
char cmdbuf[1000];
while (1){
printf("Crap Shell> ");
fgets(cmdbuf, 1000, stdin);
system(cmdbuf);
}
}
Então, eu executei esse shell em um terminal vazio (sem bash
correr por baixo). Então eu comecei a executar um programa que produz um segfault. Se minhas suposições estivessem corretas, isso poderia a) travar crsh
, fechar o xterm, b) não imprimir "Segmentation fault"
, ou c) ambos.
braden@system ~/code/crsh/ $ xterm -e ./crsh
Crap Shell> ./segfault
Segmentation fault
Crap Shell> [still running]
De volta à estaca zero, eu acho. Acabei de demonstrar que não é o shell que faz isso, mas o sistema abaixo. Como é impressa a "falha de segmentação"? "Quem" está fazendo isso? O kernel? Algo mais? Como o sinal e todos os seus efeitos colaterais se propagam do hardware para a finalização do programa?
fonte
crsh
é uma ótima idéia para esse tipo de experimentação. Obrigado por nos informar sobre a idéia e por trás dela.crsh
, pensei que seria pronunciado "acidente". Não tenho certeza se esse é um nome igualmente adequado.system()
faz sob o capô. Acontece que issosystem()
gerará um processo de shell! Portanto, seu processo de shell gera outro processo de shell e esse processo de shell (provavelmente/bin/sh
ou algo parecido) é o que executa o programa. A maneira/bin/sh
oubash
funciona é usandofork()
eexec()
(ou outra função naexecve()
família).man 2 wait
, ela incluirá as macrosWIFSIGNALED()
eWTERMSIG()
.(WIFSIGNALED(status) && WTERMSIG(status) == 11)
que ele imprima algo pateta ("YOU DUN GOOFED AND TRIGGERED A SEGFAULT"
). Quando executei osegfault
programa de dentrocrsh
, ele imprimiu exatamente isso. Enquanto isso, os comandos que saem normalmente não produzem a mensagem de erro.Respostas:
Todas as CPUs modernas têm capacidade para interromper as instruções da máquina em execução no momento. Eles economizam estado suficiente (geralmente, mas nem sempre, na pilha) para tornar possível retomar a execução mais tarde, como se nada tivesse acontecido (a instrução interrompida será reiniciada do zero, geralmente). Então eles começam a executar um manipulador de interrupção , que é apenas mais código de máquina, mas colocado em um local especial para que a CPU saiba onde está com antecedência. Manipuladores de interrupção sempre fazem parte do kernel do sistema operacional: o componente que é executado com o maior privilégio e é responsável por supervisionar a execução de todos os outros componentes. 1,2
As interrupções podem ser síncronas , o que significa que elas são acionadas pela própria CPU como uma resposta direta a algo que a instrução atualmente em execução fez, ou assíncronas , o que significa que elas ocorrem em um momento imprevisível por causa de um evento externo, como dados chegando na rede porta. Algumas pessoas reservam o termo "interrupção" para interrupções assíncronas e chamam interrupções síncronas de "traps", "falhas" ou "exceções", mas todas essas palavras têm outros significados, por isso vou usar a "interrupção síncrona".
Agora, os sistemas operacionais mais modernos têm uma noção de processos . No mais básico, esse é um mecanismo pelo qual o computador pode executar mais de um programa ao mesmo tempo, mas também é um aspecto essencial de como os sistemas operacionais configuram a proteção de memória , que é um recurso da maioria (mas, infelizmente, ainda não todos ) CPUs modernas. Combina com a memória virtual, que é a capacidade de alterar o mapeamento entre endereços de memória e locais reais na RAM. A proteção de memória permite que o sistema operacional forneça a cada processo seu próprio pedaço privado de RAM, que somente ele pode acessar. Ele também permite que o sistema operacional (agindo em nome de algum processo) designe regiões da RAM como somente leitura, executável, compartilhada entre um grupo de processos cooperativos etc. Também haverá um pedaço de memória acessível apenas pelo núcleo. 3
Enquanto cada processo acessa a memória apenas da maneira que a CPU está configurada para permitir, a proteção da memória é invisível. Quando um processo quebra as regras, a CPU gera uma interrupção síncrona, pedindo ao kernel para resolver as coisas. Ocorre regularmente que o processo realmente não violou as regras, apenas o kernel precisa fazer algum trabalho antes que o processo possa continuar. Por exemplo, se uma página da memória de um processo precisar ser "despejada" no arquivo de permuta para liberar espaço na RAM para outra coisa, o kernel marcará essa página como inacessível. Na próxima vez que o processo tentar usá-lo, a CPU gerará uma interrupção na proteção de memória; o kernel recuperará a página do swap, colocará de volta onde estava, marcará como acessível novamente e retomará a execução.
Mas suponha que o processo realmente tenha infringido as regras. Ele tentou acessar uma página que nunca teve nenhuma RAM mapeada para ele ou tentou executar uma página marcada como não contendo código de máquina ou qualquer outra coisa. A família de sistemas operacionais geralmente conhecida como "Unix" usa sinais para lidar com essa situação. 4 Os sinais são semelhantes às interrupções, mas são gerados pelo kernel e colocados em campo por processos, em vez de serem gerados pelo hardware e em campo pelo kernel. Processos podem definir manipuladores de sinalem seu próprio código e diga ao kernel onde eles estão. Esses manipuladores de sinal serão executados, interrompendo o fluxo normal de controle, quando necessário. Todos os sinais têm um número e dois nomes, um dos quais é um acrônimo enigmático e o outro uma frase um pouco menos enigmática. O sinal gerado quando um processo quebra as regras de proteção de memória é (por convenção) o número 11 e seus nomes são
SIGSEGV
"Falha na segmentação". 5,6Uma diferença importante entre sinais e interrupções é que existe um comportamento padrão para cada sinal. Se o sistema operacional falhar na definição de manipuladores para todas as interrupções, isso é um bug no sistema operacional e o computador inteiro falhará quando a CPU tentar invocar um manipulador ausente. Mas os processos não têm obrigação de definir manipuladores de sinais para todos os sinais. Se o kernel gerar um sinal para um processo, e esse sinal tiver sido deixado em seu comportamento padrão, o kernel apenas seguirá em frente e fará o que for o padrão e não incomodará o processo. O comportamento padrão da maioria dos sinais é "não fazer nada" ou "encerrar esse processo e talvez também produzir um dump principal".
SIGSEGV
é um dos últimos.Então, para recapitular, temos um processo que violou as regras de proteção de memória. A CPU suspendeu o processo e gerou uma interrupção síncrona. O kernel colocou em campo essa interrupção e gerou um
SIGSEGV
sinal para o processo. Vamos supor que o processo não configurou um manipulador de sinal paraSIGSEGV
, portanto, o kernel executa o comportamento padrão, que é finalizar o processo. Isso tem os mesmos efeitos que a_exit
chamada do sistema: os arquivos abertos são fechados, a memória é desalocada, etc.Até esse momento, nada imprimia nenhuma mensagem que um humano pudesse ver, e o shell (ou, de maneira mais geral, o processo pai do processo que acabou de terminar) não estava envolvido.
SIGSEGV
vai para o processo que violou as regras, não seu pai. A próxima etapa da sequência, no entanto, é notificar o processo pai de que seu filho foi encerrado. Isso pode acontecer de várias maneiras diferentes, das quais a mais simples é quando o pai já está esperando por esta notificação, usando uma daswait
chamadas de sistema (wait
,waitpid
,wait4
, etc). Nesse caso, o kernel fará com que a chamada do sistema retorne e forneça ao processo pai um número de código chamado status de saída. 7 O status de saída informa ao pai por que o processo filho foi encerrado; nesse caso, aprenderá que a criança foi encerrada devido ao comportamento padrão de umSIGSEGV
sinal.O processo pai pode então relatar o evento a um humano imprimindo uma mensagem; programas shell quase sempre fazem isso. Você
crsh
não inclui código para fazer isso, mas acontece assim mesmo, porque a rotina da biblioteca Csystem
executa um shell com todos os recursos/bin/sh
, "sob o capô".crsh
é o avô nesse cenário; a notificação do processo pai é preenchida por/bin/sh
, que imprime sua mensagem usual. Em seguida,/bin/sh
ele sai, pois não tem mais nada a fazer, e a implementação da biblioteca Csystem
recebe essa notificação de saída. Você pode ver essa notificação de saída em seu código, inspecionando o valor de retorno desystem
; mas não lhe dirá que o processo do neto morreu em um segfault, porque foi consumido pelo processo intermediário do shell.Notas de rodapé
Alguns sistemas operacionais não implementam drivers de dispositivo como parte do kernel; no entanto, todos os manipuladores de interrupção ainda precisam fazer parte do kernel, assim como o código que configura a proteção de memória, porque o hardware não permite nada além do kernel fazer essas coisas.
Pode haver um programa chamado "hypervisor" ou "gerenciador de máquina virtual" que seja ainda mais privilegiado que o kernel, mas, para fins desta resposta, pode ser considerado parte do hardware .
O kernel é um programa , mas é não um processo; é mais como uma biblioteca. Todos os processos executam partes do código do kernel, de tempos em tempos, além de seu próprio código. Pode haver vários "threads do kernel" que executam apenas o código do kernel, mas eles não nos interessam aqui.
O único sistema operacional com o qual você provavelmente precisará lidar mais que não pode ser considerado uma implementação do Unix é, obviamente, o Windows. Não usa sinais nesta situação. (Na verdade, ele não possui sinais; no Windows, a
<signal.h>
interface é completamente falsificada pela biblioteca C.) Ela usa algo chamado " manipulação de exceção estruturada ".Algumas violações de proteção de memória são geradas
SIGBUS
("Erro de barramento") em vez deSIGSEGV
. A linha entre os dois é subespecificada e varia de sistema para sistema. Se você escreveu um programa que define um manipuladorSIGSEGV
, provavelmente é uma boa ideia definir o mesmo manipulador paraSIGBUS
."Falha de segmentação" foi o nome da interrupção gerada por violações da proteção de memória por um dos computadores que executaram o Unix original , provavelmente o PDP-11 . " Segmentação " é um tipo de proteção de memória, mas atualmente o termo " falha de segmentação " refere-se genericamente a qualquer tipo de violação de proteção de memória.
Todas as outras maneiras pelas quais o processo pai pode ser notificado sobre a conclusão de um filho terminam com o pai chamando
wait
e recebendo um status de saída. É que algo mais acontece primeiro.fonte
mmap
um arquivo em uma região de memória maior que o arquivo e acessa "páginas inteiras" além do final do arquivo. (POSIX é de outro modo muito vago sobre quando SIGSEGV / SIGBUS / SIGILL / etc acontecer.)O shell realmente tem algo a ver com essa mensagem e
crsh
indiretamente chama um shell, o que provavelmente ébash
.Eu escrevi um pequeno programa C que sempre seg falhas:
Quando o executo do meu shell padrão
zsh
, recebo o seguinte:Quando o executo
bash
, recebo o que você anotou na sua pergunta:Eu ia escrever um manipulador de sinal no meu código, então percebi que a
system()
chamada de biblioteca usada pelocrsh
exec é um shell, de/bin/sh
acordo comman 3 system
. Isso/bin/sh
quase certamente está imprimindo "Falha na segmentação", poiscrsh
certamente não está.Se você reescrever
crsh
para usar aexecve()
chamada do sistema para executar o programa, não verá a sequência "Falha na segmentação". Vem do shell invocado porsystem()
.fonte
execvp
e fez o teste novamente para descobrir que enquanto o shell ainda não falhar (o que significa SIGSEGV nunca é enviado para o shell), ele não imprimir "falha de segmentação". Nada é impresso. Isso parece indicar que o shell detecta quando seus processos filhos são mortos e é responsável pela impressão de "Falha de segmentação" (ou alguma variante do mesmo).waitpid()
em cada fork / exec e ele retorna um valor diferente para processos com falha de segmentação do que para processos que saem com o status 0.Este é um resumo um pouco distorcido. O mecanismo de sinal Unix é totalmente diferente dos eventos específicos da CPU que iniciam o processo.
Em geral, quando um endereço incorreto é acessado (ou gravado em uma área somente leitura, tenta executar uma seção não executável, etc.), a CPU gera um evento específico da CPU (nas arquiteturas tradicionais não VM, isso foi chamado de violação de segmentação, já que cada "segmento" (tradicionalmente, o "texto executável somente leitura", os "dados" graváveis e de comprimento variável e a pilha tradicionalmente na extremidade oposta da memória) tinha um intervalo fixo de endereços - em uma arquitetura moderna, é mais provável que seja uma falha de página [para memória não mapeada] ou uma violação de acesso [para problemas de permissão de leitura, gravação e execução], e vou me concentrar nisso no restante da resposta).
Agora, neste ponto, o kernel pode fazer várias coisas. As falhas de página também são geradas para a memória que é válida, mas não carregada (por exemplo, trocada ou em um arquivo mmapped etc.), e nesse caso o kernel mapeará a memória e reiniciará o programa do usuário a partir da instrução que causou o erro. erro. Caso contrário, ele envia um sinal. Isso não exatamente "direciona [o evento original] para o programa incorreto", pois o processo de instalação de um manipulador de sinal é diferente e, principalmente, independente da arquitetura, em comparação com o programa que simula a instalação de um manipulador de interrupção.
Se o programa do usuário tiver um manipulador de sinais instalado, isso significa criar um quadro de pilha e definir a posição de execução do programa do usuário para o manipulador de sinais. O mesmo é feito para todos os sinais, mas, no caso de uma violação de segmentação, as coisas geralmente são organizadas para que, se o manipulador de sinais retornar, reinicie a instrução que causou o erro. O programa do usuário pode ter corrigido o erro, por exemplo, mapeando a memória para o endereço incorreto - depende da arquitetura se isso é possível). O manipulador de sinal também pode pular para um local diferente no programa (normalmente via longjmp ou lançando uma exceção), para abortar qualquer operação que tenha causado o acesso ruim à memória.
Se o programa do usuário não tiver um manipulador de sinal instalado, ele simplesmente será encerrado. Em algumas arquiteturas, se o sinal for ignorado, ele poderá reiniciar a instrução repetidamente, causando um loop infinito.
fonte
#PF(fault-code)
(falha de página) ou#GP(0)
("Se um endereço efetivo de operando de memória estiver fora do CS, Limite de segmento DS, ES, FS ou GS. "). O modo de 64 bits descarta as verificações de limite de segmento, já que os sistemas operacionais apenas usavam paginação e um modelo de memória plana para o espaço do usuário.Uma falha de segmentação é um acesso a um endereço de memória que não é permitido (não faz parte do processo, ou tenta gravar dados somente leitura ou executar dados não executáveis, ...). Isso é detectado pela MMU (Unidade de Gerenciamento de Memória, hoje parte da CPU), causando uma interrupção. A interrupção é tratada pelo kernel, que envia um
SIGSEGFAULT
sinal (veja,signal(2)
por exemplo) para o processo incorreto. O manipulador padrão para esse sinal despeja o núcleo (consultecore(5)
) e finaliza o processo.A concha não tem absolutamente nenhuma mão nisso.
fonte