Exemplos executáveis
Tecnicamente, um programa que é executado sem um sistema operacional é um sistema operacional. Então, vamos ver como criar e executar alguns SOs minúsculos do Hello World.
O código de todos os exemplos abaixo está presente neste repositório do GitHub .
Setor de inicialização
No x86, a coisa mais simples e de nível mais baixo que você pode fazer é criar um MBR (setor de inicialização mestre) , que é um tipo de setor de inicialização , e depois instalá-lo em um disco.
Aqui, criamos um com uma única printf
chamada:
printf '\364%509s\125\252' > main.img
sudo apt-get install qemu-system-x86
qemu-system-x86_64 -hda main.img
Resultado:
Testado no Ubuntu 18.04, QEMU 2.11.1.
main.img
contém o seguinte:
\364
in octal == 0xf4
in hex: a codificação de uma hlt
instrução, que diz à CPU para parar de funcionar.
Portanto, nosso programa não fará nada: apenas inicie e pare.
Usamos octal porque os \x
números hexadecimais não são especificados pelo POSIX.
Poderíamos obter essa codificação facilmente com:
echo hlt > a.asm
nasm -f bin a.asm
hd a
mas a 0xf4
codificação também está documentada no manual da Intel, é claro.
%509s
produzir 509 espaços. Necessário preencher o arquivo até o byte 510.
\125\252
em octal == 0x55
seguido por 0xaa
: bytes mágicos requeridos pelo hardware. Eles devem ser bytes 511 e 512.
Se não estiver presente, o hardware não tratará isso como um disco inicializável.
Observe que, mesmo sem fazer nada, alguns caracteres já estão impressos na tela. Esses são impressos pelo firmware e servem para identificar o sistema.
Executar em hardware real
Os emuladores são divertidos, mas o hardware é o verdadeiro negócio.
Observe, no entanto, que isso é perigoso e você pode limpar seu disco por engano: faça isso apenas em máquinas antigas que não contêm dados críticos! Ou melhor ainda, devboards como o Raspberry Pi, veja o exemplo do ARM abaixo.
Para um laptop típico, você precisa fazer algo como:
Grave a imagem em um dispositivo USB (destruirá seus dados!):
sudo dd if=main.img of=/dev/sdX
conecte o USB ao computador
ligue
diga para inicializar a partir do USB.
Isso significa fazer com que o firmware escolha USB antes do disco rígido.
Se esse não é o comportamento padrão da sua máquina, continue pressionando Enter, F12, ESC ou outras teclas estranhas após a inicialização, até que você obtenha um menu de inicialização onde você pode selecionar para inicializar a partir do USB.
Geralmente, é possível configurar a ordem de pesquisa nesses menus.
Por exemplo, no meu antigo Lenovo Thinkpad T430, UEFI BIOS 1.16, posso ver:
Olá Mundo
Agora que criamos um programa mínimo, vamos para o mundo olá.
A pergunta óbvia é: como fazer IO? Algumas opções:
- peça ao firmware, por exemplo, BIOS ou UEFI, que faça por nós
- VGA: região de memória especial que é impressa na tela se gravada. Pode ser usado no modo protegido.
- escreva um driver e fale diretamente com o hardware da tela. Esta é a maneira "correta" de fazê-lo: mais poderoso, mas mais complexo.
porta serial . Este é um protocolo padronizado muito simples que envia e recupera caracteres de um terminal host.
Fonte .
Infelizmente, ele não está exposto na maioria dos laptops modernos, mas é o caminho mais comum para as placas de desenvolvimento, veja os exemplos de ARM abaixo.
Isso é realmente uma pena, pois essas interfaces são realmente úteis para depurar o kernel do Linux, por exemplo .
use recursos de depuração de chips. O ARM chama de semi - hospedagem deles, por exemplo. Em hardware real, requer algum suporte extra de hardware e software, mas em emuladores pode ser uma opção conveniente e gratuita. Exemplo .
Aqui vamos fazer um exemplo de BIOS, pois é mais simples no x86. Mas observe que este não é o método mais robusto.
main.S
.code16
mov $msg, %si
mov $0x0e, %ah
loop:
lodsb
or %al, %al
jz halt
int $0x10
jmp loop
halt:
hlt
msg:
.asciz "hello world"
link.ld
SECTIONS
{
. = 0x7c00;
.text :
{
__start = .;
*(.text)
. = 0x1FE;
SHORT(0xAA55)
}
}
Monte e vincule com:
gcc -c -g -o main.o main.S
ld --oformat binary -o main.img -T linker.ld main.o
Resultado:
Testado em: Lenovo Thinkpad T430, UEFI BIOS 1.16. Disco gerado em um host Ubuntu 18.04.
Além das instruções de montagem padrão da userland, temos:
.code16
: diz ao GAS para gerar código de 16 bits
cli
: desativar interrupções de software. Isso pode fazer com que o processador volte a funcionar após ohlt
int $0x10
: faz uma chamada do BIOS. É isso que imprime os caracteres um por um.
Os sinalizadores de link importantes são:
--oformat binary
: produz código de montagem binário bruto, não o distorce dentro de um arquivo ELF, como é o caso de executáveis regulares da terra do usuário.
Use C em vez de montagem
Como o C é compilado no assembly, o uso do C sem a biblioteca padrão é bastante simples, você basicamente precisa:
- um script vinculador para colocar as coisas na memória no lugar certo
- sinalizadores que informam ao GCC para não usar a biblioteca padrão
- um pequeno ponto de entrada de montagem que define o estado C necessário para
main
, principalmente:
TODO: link para algum exemplo x86 no GitHub. Aqui está um ARM que eu criei .
No entanto, as coisas ficam mais divertidas se você quiser usar a biblioteca padrão, já que não temos o kernel Linux, o que implementa grande parte da funcionalidade da biblioteca padrão C através do POSIX .
Algumas possibilidades, sem acessar um sistema operacional completo como o Linux, incluem:
BRAÇO
No ARM, as idéias gerais são as mesmas. Eu enviei:
Para o Raspberry Pi, https://github.com/dwelch67/raspberrypi parece o tutorial mais popular disponível hoje.
Algumas diferenças do x86 incluem:
IO é feito escrevendo diretamente para endereços de mágica, não há in
e out
instruções.
Isso é chamado de E / S mapeada na memória .
para hardware real, como o Raspberry Pi, você pode adicionar o firmware (BIOS) à imagem do disco.
Isso é bom, pois torna a atualização desse firmware mais transparente.
Firmware
Na verdade, seu setor de inicialização não é o primeiro software executado na CPU do sistema.
O que realmente roda primeiro é o chamado firmware , que é um software:
- feita pelos fabricantes de hardware
- fonte tipicamente fechada, mas provavelmente baseada em C
- armazenado na memória somente leitura e, portanto, mais difícil / impossível de modificar sem o consentimento do fornecedor.
Firmwares conhecidos incluem:
- BIOS : firmware x86 antigo completo. SeaBIOS é a implementação de código aberto padrão usada pelo QEMU.
- UEFI : sucessor do BIOS, melhor padronizado, mas mais capaz e incrivelmente inchado.
- Coreboot : a nobre tentativa de código aberto de arco cruzado
O firmware faz coisas como:
faça um loop sobre cada disco rígido, USB, rede etc. até encontrar algo inicializável.
Quando executamos o QEMU, -hda
diz que main.img
é um disco rígido conectado ao hardware e
hda
é o primeiro a ser experimentado e é usado.
carregue os primeiros 512 bytes no endereço de memória RAM 0x7c00
, coloque o RIP da CPU lá e deixe executar
mostrar coisas como o menu de inicialização ou as chamadas de impressão do BIOS no visor
O firmware oferece funcionalidade semelhante ao sistema operacional da qual a maioria dos sistemas operacionais depende. Por exemplo, um subconjunto Python foi portado para execução no BIOS / UEFI: https://www.youtube.com/watch?v=bYQ_lq5dcvM
Pode-se argumentar que os firmwares são indistinguíveis dos sistemas operacionais, e que o firmware é a única programação bare metal "verdadeira" que se pode fazer.
Como este desenvolvedor do CoreOS coloca :
A parte difícil
Quando você liga um PC, os chips que compõem o chipset (northbridge, southbridge e SuperIO) ainda não foram inicializados corretamente. Mesmo que a ROM do BIOS esteja o mais longe possível da CPU, ela pode ser acessada pela CPU, porque precisa ser, caso contrário, a CPU não teria instruções para executar. Isso não significa que a ROM do BIOS esteja completamente mapeada, geralmente não. Mas apenas o suficiente é mapeado para iniciar o processo de inicialização. Quaisquer outros dispositivos, apenas esqueça.
Ao executar o Coreboot no QEMU, você pode experimentar as camadas superiores do Coreboot e as cargas úteis, mas o QEMU oferece poucas oportunidades para experimentar o código de inicialização de baixo nível. Por um lado, a RAM funciona desde o início.
Estado inicial do BIOS pós
Como muitas coisas no hardware, a padronização é fraca, e uma das coisas em que você não deve confiar é o estado inicial dos registros quando o código começa a ser executado após o BIOS.
Então faça um favor a si mesmo e use algum código de inicialização como o seguinte: https://stackoverflow.com/a/32509555/895245
Os registros gostam %ds
e %es
têm efeitos colaterais importantes, portanto, você deve zerá-los, mesmo que não os esteja usando explicitamente.
Observe que alguns emuladores são mais agradáveis que o hardware real e fornecem um bom estado inicial. Então, quando você roda em hardware real, tudo quebra.
Inicialização múltipla GNU GRUB
Os setores de inicialização são simples, mas não são muito convenientes:
- você só pode ter um sistema operacional por disco
- o código de carregamento deve ser muito pequeno e caber em 512 bytes. Isso pode ser resolvido com a chamada int 0x13 do BIOS .
- você tem que fazer muita inicialização, como mudar para o modo protegido
É por esses motivos que o GNU GRUB criou um formato de arquivo mais conveniente chamado de inicialização múltipla.
Exemplo de trabalho mínimo: https://github.com/cirosantilli/x86-bare-metal-examples/tree/d217b180be4220a0b4a453f31275d38e697a99e0/multiboot/hello-world
Também o uso no repositório de exemplos do GitHub para poder executar facilmente todos os exemplos em hardware real sem queimar o USB um milhão de vezes. No QEMU, fica assim:
Se você preparar seu sistema operacional como um arquivo de inicialização múltipla, o GRUB poderá encontrá-lo dentro de um sistema de arquivos comum.
É isso que a maioria das distros faz, colocando as imagens do SO em baixo /boot
.
Os arquivos de inicialização múltipla são basicamente um arquivo ELF com um cabeçalho especial. Eles são especificados pelo GRUB em: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html
Você pode transformar um arquivo de inicialização múltipla em um disco inicializável com grub-mkrescue
.
El Torito
Formato que pode ser gravado em CDs: https://en.wikipedia.org/wiki/El_Torito_%28CD-ROM_standard%29
Também é possível produzir uma imagem híbrida que funcione em ISO ou USB. Isso pode ser feito com grub-mkrescue
( exemplo ) e também é feito pelo kernel do Linux no make isoimage
uso isohybrid
.
Recursos