PCF-3

Questões comentadas, artigos e notícias

Software Illustrated: O processo de boot do Kernel

Posted by papacharliefox3 em 10/03/2009

Dando continuidade a tradução (livre) dos excelentes artigos do Gustavo Duarte, seguimos para mais um post da série Sofwtare Illustrated.

O post anterior tratou do processo de boot até o ponto em que o gerenciador de boot carrega a imagem do kernel na memória. Haja vista minha inclinação empírica, as explicações aqui expostas serão diretamente ligadas ao Linux e seu código-fonte (versão 2.6). Os códigos são de fácil entendimento (sintaxe C); mesmo que você não entenda alguma parte, é possível entender o que acontece. O principal obstáculo é a falta de contextualização que ronda partes do código, cenário visto, por exemplo, quando são executados códigos da arquitetura base da máquina. Tentarei contextualizar, quando possível. A parte mais legal – interrupções e memória – será abordada de maneira breve por enquanto. O post termina com uma idéia de como funciona o boot no Windows.

At this point in the Intel x86 boot story the processor is running in real-mode, is able to address 1 MB of memory, and RAM looks like this for a modern Linux system:

Neste ponto do processo de boot em máquinas Intel x86 , o processador roda em modo real e endereça apenas 1MB de memória; esta se encontra da seguinte maneira:

Conteúdo da memória após o carregamento do bootloader

Conteúdo da memória após o carregamento do bootloader

A imagem do kernel foi carregada na memória utilizando serviços de E/S de disco (BIOS). Esta imagem é uma cópia exata do arquivo persistido no disco, a saber, /boot/vmlinuz-versão-kernel. A imagem é dividida em duas partes: uma parte menor contendo o código kernel para o modo real, o qual é carregado abaixo da barreira dos 640k; o kernel “grosso”, o qual roda em modo protegido, carregado após a barreira de 1MB de memória.

A ação começa com o kernel em modo real, como mostrado acima. Esta região da memória é utilizada para implementar o protocolo de boot do Linux, entre o gerenciador (carregador) de boot e o kernel em si. Alguns dos valores ali são lidos pelo gerenciador de boot enquanto executa seu trabalho. Esses incluem a string da versão do kernel e informações cruciais como o tamanho do kernel em modo real. O gerenciador de boot também escreve valores nessa região, como endereços de memória de parâmetros da linha de comando informada pelo usuário no menu de inicialização. Assim que o gerenciador termina, todos os parâmetros requeridos pelo cabeçalho do kernel estão inseridos e configurados. É o momento de chamar o registro de entrada do kernel. O diagrama abaixo mostra a sequência de codificação da inicialização do kernel, assim como os diretórios, arquivos e linhas envolvidos.

Inicialização do kernel na arquitetura x86

Inicialização do kernel na arquitetura x86

O primeiro arquivo lido pelo processo de inicialização do kernel é o arch/x86/boot/header.S, codificado em linguagem Assembly, linguagem rara no kernel, mas comum em códigos de boot. O início deste arquivo contém código de setor de boot, um resquício dos dias em que o Linux podia carregar sem um bootloader. Hoje em dia, este setor de boot, se executado, apenas imprime uma mensagem “bugger_off_msg” e reinicializa. Gerenciadores de boot modernos simplesmente ignoram este código legado. Após o código do setor de boot, existem os primeiros 15 bytes do cabeçalho do kernel (modo real); juntamente com o código legado, totalizam 512 bytes, tamanho típico de uma setor de disco em máquinas Intel.

Depois desses 512 bytes, no offset  0×200, encontra-se a primeira instrução do código kernel: o registro de entrada em modo real (linha 110), um jump de 2 bytes escrito diretamente em código de máquina, como 0x3aeb. É possível verificar isto através de um hexdump da imagem do kernel, onde verifica-se este valor naquele offset (apenas para deixar claro que isto não é sonho). O gerenciador de boot salta para esta localização assim que termina, o que ocasiona outro salto para a linha 229 do arquivo (cabeçalho), onde existem rotinas assembly chamadas ‘start_of_setup’. Estas pequenas rotinas configuram a stack e zeram os segmentos bss (variáveis estáticas) para o modo real e ntão saltam para o bom e velho código C, em arch/x86/boot/main.c:122.

A função main() executa algumas rotinas de manutenção como detecção do layout de memória, configuração de vídeo, etc. e depois chama a função go_to_protected_mode(). Entretanto, antes da CPU entrar no modo protegido, algumas tarefas devem ser executadas. Há dois assuntos principais: interrupções e memória. Em modo real, a tabela de vetor de interrupções está sempre no endereço de memória 0, enquanto que, em modo protegido, está em um registrador chamado IDTR. Por enquanto, a tradução de endereços lógicos de memória (manipulados por programas) para endereços lineares (valor raw, a partir do topo da memória) é diferente quando em modo real ou protegido. O modo protegido requer o registrador GDTR carregado com endereço da tabela de descrição global. Assim, go_to_protected_mode() chama setup_idt() e setup_gdt() para instalação de descritores temporários de interrupções e da tabela global de descritores.

Estamos prontos agora para mergulhar no modo protegido, feito alcançado por meio da rotina assembly protected_mode_jump. Esta é responsável por habilitar o modo em questão configurando o bit PE no registrador CR0. Até este momento, a paginação de memória está desabilitada, característica opicional do processador, mesmo em modo protegido; ademais, não há necessidade dela por enquanto. O importante é que não estamos mais confinados no mundo de 640k, mas 4GB. Aquela rotina chama o ponto de entrada do kernel de 32 bits, chamado  startup_32 em imagens compactadas. Essa rotina inicializa alguns registradores básicos e chama a função C decompress_kernel().

A função de descompressão imprime a famosa mensagem “Decompressing Linux…”. A descompressão ou descompactação sobrescreve a imagem inicialmente armazenada (primeiro diagrama), assim, a versão descompactada também inicia-se em 1MB; ao final, é imprimida a mesagem “done.” e a confortante mensagem “Booting the kernel” finalmente é mostrada. Perceba que “Booting” significa um salto para o último ponto de entrada (entry ponit), isso é outra longa estória. Segundo o próprio Linus, isso foi lhe concebido por Deus no topo do Monte Halti (comentário nerd do ‘tradutor’: que comédia! hehe), esse final entry point é o ponto de entrada do kernel em modo protegido, iniciado no segundo megabyte de memória (0×100000). Esse local sagrado contém a rotina  chamada, lógico, startup_32. Mas este está em um diretório diferente, como veremos.

A segunda encarnação desta rotina também é escrita em assembly, mas contém inicializações em 32 bits. Ela zera o segmento (bss) para o kernel em modo protegido (este rodará até a máquina ser desligada ou reinicializada), configura a tabela final de descritores globais na memória, constrói tabelas de memória para habilitar a paginação, inicializa a stack, cria a tabela final de descritores de interrupções e, finalmente, salta para o kernel independente de arquitetura, start_kernel().O diagrama abaixo mostra o fluxo de código para a última rodada do processo de boot:

Inicialização do kernel do Linux, independente de arquitetura

Inicialização do kernel do Linux, independente de arquitetura

A rotina start_kernel() é tipicamente código de kernel, cujo conteúdo é puro C, sem dependência de arquitetura. A função roda uma imensa lista de códigos de inicialização dos vários subsistemas e estruturas de dados do kernel. Estas incluem o escalonador, zonas de memória, etc. A rotina chama rest_init(), ponto em que as coisas estão quase prontas. rest_init() cria uma thread passando como parâmetro outra função, kernel_init(). A rotina schedule() é chamada para iniciar o agendamento de tarefas e, logo em seguida, torna-se inativa (idle) após a execução de cpu_idle(), a idle thread do Linux. Esta rotina roda para sempre assim como o processo zero, que a armazena. Toda vez que há algo para ser feito – um processo executável -, o processo zero some da CPU, retornando apenas quando não houver mais processos para execução.

Eis aqui a grande questão. O loop idle é o final da longa thread seguida desde o processo de boot, é o descendente único do primeiríssimo salto (jump) executado pelo processador depois de energizado. Toda essa estória, desde a inicialização do vetor, BIOS, MBR, gerenciador de boot, mudança de modos; tudo isso, salto a salto acaba no loop idle, cpu_idle(). O que pode ser legal, entretanto, isso não é tudo, se fosse, o computador não funcionaria.

Neste momento, a thread do kernel iniciada previamente está preparada pra mandar ver, desalocando o processo 0 e a thread idle. E assim é feito. A partir deste momento kernel_init() começa a rodar. Esta rotina é responsável por inicializar o restante dos processadores presentes no sistema, os quais estavam inativos desde o boot. Todo o código que vimos até agora é executado em um único processador, chamado de processador de boot. Assim que os outros processadores são ativados, eles assim o fazem em modo real, tendo que passar por vários processos de inicialização, inclusive. Vários caminhos são comuns, entretanto há alguns forks seguidos por alguns processadores. Finalmente, kernel_init() chama init_post(), que tenta entrar no modo usuário seguindo a ordem, a saber: /sbin/init, /etc/init, /bin/init, and /bin/sh. Se todos falharem, o kernel entra em pânico (kernel panic). Por sorte, o init está normalmente presente, esse utiliza o PID de número um. O arquivo de configuração é checado para saber quais processos deverão ser iniciados, dentre estes: X Windows, programas de logging, serviços de rede, etc. Assim, chega-se ao final do processo de boot e mais um Linux está pronto para uso em algum lugar do planeta. Como está o uptime do seu Linux?

O processo para o Windows é similar haja vista a arquitetura comum. A maioria dos problemas também são encontrados assim como a mesma rotina de inicialização deve ser feita. Durante o boot, uma das grandes diferenças está no fato de que o Windows empacota todo o código kernel para modo real e algumas partes do código protegido no bootloader (NTLDR). Assim, em vez de se ter duas regiões na mesma imagem do kernel, tem-se imagens diferentes. Além disso, o Linux separa completamente o bootloader e o kernel, de forma que isso espalha-se em processos open source. O diagrama abaixo mostra os principais passos do processo, no Windows:

Inicialização do kernel no Windows

Inicialização do kernel no Windows

O modo usuário do Windows inicia naturalmente bem diferente. Não há init, mas o Csrss.exe e o Winlogon.exe. Este dispara o Services.exe, o qual inicia os serviços e o Lsass.exe, o subsistema de autenticação segura. O clássico logon do Windows roda no contexto do Winlogon.

2 Respostas to “Software Illustrated: O processo de boot do Kernel”

  1. Gustavo Duarte, nao Gabriel :)

    Obrigado por traduzir o texto!

  2. papacharliefox3 said

    Putz Gustavo! Desculpe a falha, estava com o nome do meu filho na cabeça! rsrsrs

    Fico muito feliz em saber que gostou! Não avisei-o com receio de não gostar. Aproveito aqui para deixar meu agradecimento aos seus textos, são bastante didáticos!

    Trabalho com Linux há uns 10 anos, este último post é cheio de fatos históricos pra quem curte o SO! Parabéns!

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s

 
Seguir

Obtenha todo post novo entregue na sua caixa de entrada.

%d blogueiros gostam disto: