A gente sempre ouve falar que o sistema operacional impede que um processo veja a memória do outro ou que o programa fale diretamente com o hardware, mas normalmente não explicam o "como".
Eu sempre achei isso meio mágico até que eu resolvi ir atrás da resposta, e é bem interessante.
Vou me basear na arquitetura x86, mas é provável que outras arquiteturas sejam parecidas.
O problema: a CPU
Pra CPU não existe processo, kernel, sistema operacional. Existe só endereços de memória de onde ela lê a próxima instrução e executa.
Se a CPU pode falar direto com a RAM, SSD, teclado, mouse, tela... O que me impede de escrever um programa pra ler suas senhas e tokens direto da RAM?
Ou de ler arquivos e alterar arquivos sensíveis direto no SSD?
Por outro lado, se o kernel fiscalizasse cada instrução que da CPU antes dela executar, isso seria extremamente lento...
Outro problema: os interrupts
Se a CPU só executasse sequencialmente, seu sistema poderia executar várias coisas e esquecer de checar se uma tecla foi apertada, se o mouse mexeu, etc...
Então certos eventos interrompem o que quer que a CPU esteja fazendo para serem tratados assim que possível.
Alguns exemplos de interrupt são:
Teclas do teclado pressionadas ou soltas
Botões e movimento do mouse
Timers
Operações de disco assíncronas
Pacotes de rede recebidos/transmitidos
Uma solução: rings
Os processadores da arquitetura x86 tem o esquema de rings. Pense em rings como grau de limitação.
Ring 0 significa limitação zero, ou seja, acesso a todas as instruções da CPU e consequentemente acesso total ao hardware e memória. O kernel roda em ring 0, ou kernel mode.
O kernel assim que é carregado configura todos os interrupts handlers da CPU para executar o handler apropriado do kernel, em kernel mode, claro.
Em ring 3 a CPU fica limitada e não pode fazer instruções consideradas privilegiadas. E obviamente em ring 3 a CPU não consegue se colocar em ring 0 sozinha, pois dessa forma qualquer programa conseguiria se pôr em ring 0.
O kernel executa os processos de usuário em ring 3, ou user mode.
Mas se os processos estão executando em ring 3, e processos são uma abstração do SO e não da CPU, como que faz pro kernel voltar a executar em ring 0?
Basicamente todo interrupt handler roda em ring 0, então sempre que o controle volta pro kernel, ele já está em kernel mode.
A ponte: system calls
Se a CPU executando código em user mode não consegue acesso a nada do hardware, surgem os problemas:
Como que faz pra escrever e ler do disco?
Como consulta a data e hora atual, se isso vem do hardware?
Como toca som e produz imagens se isso também é hardware?
Quando um programa rodando precisa ler um arquivo, ele não pode (nem consegue) ler diretamente do SSD. Ele tem que pedir ao kernel com uma instrução especial chamada syscall (system call).
Essa instrução coloca a CPU em ring 0 de novo e executa o handler previamente configurado pelo kernel. Em essência é similar a um interrupt de software, porém em x86 moderno é uma instrução própria mesmo.
O kernel avalia o processo que fez a system call, se ele tem permissão para ler aquele arquivo, e se for o caso, ele lê e copia os dados lidos para uma região da memória do processo do usuário.
Ok, isso impede que o processo acesse o hardware diretamente, mas e a memória de outros processos?
Por padrão, a CPU tem acesso total a toda memória RAM, memória de vídeo, dispositivos, etc.
Se o kernel verificasse cada acesso de memória do processo pra garantir que ele se comportasse, também seria extremamente lento e vulnerável.
Pra resolver o problema o kernel usa outra feature dos processadores modernos: memória virtual.
Outra solução: memória virtual
A memória virtual permite que o processador execute instruções usando endereços "fake" e por baixo dos panos ele traduza esses endereços para endereços reais.
Por exemplo, o processador pode executar uma instrução de ler o endereço 0x0050, mas por baixo dos panos ele está lendo o enderço real 0x1234.
Obs.: geralmente os sistemas não mapeiam o endereço 0x0000, então ele é o famoso null pointer
Obs.: da mesma forma que em ring 3 você não consegue sair de ring 3, você também não consegue alterar a memória virtual, porque isso também furaria a proposta de segurança
O kernel configura cada processo pra enxergar só as partes da memória que interessam àquele processo.
Quando um processo tenta ler um endereço não mapeado pela memória virtual, a CPU estoura um page fault, o kernel processa e dependendo do caso encerra o processo com o aterrorizante segmentation fault ou equivalente.
Há casos também onde um page fault não necessariamente é um problema, e sim o kernel sendo "lazy".
Por exemplo você pode alocar memória, mas enquanto você não tentar ler nem escrever, o kernel pode nem ter alocado a memória física ainda.
Outro caso é a stack do programa, que começa pequena, e conforme ela cresce pode causar um page fault e o kernel entender que é só questão de mapear mais memória pra stack...
E os loops infinitos?
Se a CPU só executa instrução a instrução, como que o sistema operacional impede que um loop infinito seja... infinito?
Um programa que fizesse um loop infinito em cada core da CPU bloquearia todos os cores e o sistema operacional nem conseguiria interrompê-lo, exceto se um interrupt de hardware acontecesse.
E na prática o que salva o kernel mesmo é o um interrupt de hardware!
O kernel configura um timer (hardware) para causar interrupts na CPU de tempos em tempos.
Esse interrupt, assim como os outros, interrompe o código do usuário e passa o controle pro kernel.
Se o kernel vir que um processo está executando a muito tempo sem interrupções, ele vai pausar aquele processo e dar oportunidade para outros processos rodarem.
Conclusão
Bom a princípio foi isso que entendi sobre os dois mecanismos fundamentais de segurança dos sistemas operacionais.
Mais pra frente vou compartilhar novas coisas que eu for brincando com, como por exemplo meu projeto de interceptação de system calls no Linux.
Top comments (0)