Execução de programas

Last updated 7 months ago

Privilégios de execução

Para impedir que os programas do usuário acessem ou modifiquem dados críticos do sistema operacional, o Windows suporta dois níveis de execução de código: modo de usuário e modo de kernel, mais conhecidos por suas variante em inglês: user mode e kernel mode.

Os programas comuns rodam em user mode, enquanto os serviços internos do SO e drivers rodam em kernel mode.

Apesar de o Windows outros sistemas operacionais modernos trabalharem com somente estes dois níveis de privilégios de execução, os processadores Intel e compatíveis suportam quatro níveis, também chamado de anéis (rings), numerados de 0 a 4. Para kernel mode é utilizado o ring 0 e para user mode, o ring 3.

Dependências

Quando um programador cria um programa, em muitos casos ele utiliza funções de bibliotecas (ou libraries em inglês), também chamadas de DLL (Dynamic-Link Library). Sendo assim, analise o seguinte simples programa em C:

#include <stdio.h>
int main(void) {
printf("Olá, mundo!\n");
return 0;
}

Este programa utiliza a função printf(), que não precisou ser implementada pelo programador. Ao contrário, ele simplesmente a chamou, já que esta está definida no arquivo stdio.h.

Quando compilado, este programa terá uma dependência da biblioteca de C (arquivo msvcrt.dll no Windows) pois o código para a printf() está nela.

Tudo isso garante que vários programadores usem tal função, que tenha sempre o mesmo comportamento se usada da mesma forma. Mas já parou para pensar como a função printf() de fato escreve na tela? Como ela lidaria com as diferentes placas de vídeo, por exemplo?

O fato é que a printf() não escreve diretamente na tela. Ao contrário, a biblioteca de C pede ao kernel através de uma função de sua API que seja feito. Sendo assim, temos, neste caso um EXE que chama uma função de uma DLL que chama o kernel. Estudaremos mais a frente como isso é feito.

Loader

Quando um programa é executado, ele é copiado para a memória e um processo é criado para ele. Dizemos então que um processo está rodando. Quem faz este carregamento de um processo em memória é um componente do sistema operacional chamado de image loader, presente na biblioteca ntdll.dll.

O código do loader roda antes do código do programa a ser carregado. É um código comum a todos os processos executados no sistema operacional.

Dentre as funções do loader estão:

  • Ler os cabeçalhos do arquivo PE a ser executado e alocar a memória necessária para a imagem como um todo, suas seções, etc.

    • As seções são mapeadas para a memória, respeitando-se suas permissões.

  • Ler a tabela de importações do arquivo PE a fim de carregar as DLLs requeridas por este e que ainda não foram carregas em memória. Esse processo também é chamado de resolução de dependências.

  • Preencher a IAT com os endereços das funções importadas.

  • Carregar módulos adicionais em tempo de execução, se assim for pedido pelo executável principal (também chamado de módulo principal).

  • Manter uma lista de módulos carregados por um processo.

  • Transferir a execução para o entrypoint (EP) do programa.