Como você executa um programa sozinho sem um sistema operacional em execução? Você pode criar programas de montagem que o computador possa carregar e executar na inicialização, por exemplo, inicializar o computador a partir de uma unidade flash e executar o programa que está na CPU?
assembly
x86
operating-system
bootloader
osdev
user2320609
fonte
fonte
Respostas:
Você coloca seu código binário em um local onde o processador procura após a reinicialização (por exemplo, endereço 0 no ARM).
Resposta geral à pergunta: isso pode ser feito. É freqüentemente chamado de "programação bare metal". Para ler a partir da unidade flash, você quer saber o que é USB e deseja ter algum driver para trabalhar com este USB. O programa nesta unidade também teria que estar em algum formato específico, em algum sistema de arquivos específico ... Isso é algo que os carregadores de inicialização geralmente fazem, mas seu programa pode incluir seu próprio carregador de inicialização, para que seja independente, se o firmware for apenas carregue um pequeno bloco de código.
Muitas placas ARM permitem fazer algumas dessas coisas. Alguns possuem gerenciadores de inicialização para ajudá-lo com a configuração básica.
Aqui você pode encontrar um ótimo tutorial sobre como fazer um sistema operacional básico em um Raspberry Pi.
Edit: Este artigo, e todo o wiki.osdev.org responderão à maioria das suas perguntas http://wiki.osdev.org/Introduction
Além disso, se você não quiser experimentar diretamente no hardware, poderá executá-lo como uma máquina virtual usando hipervisores como o qemu. Veja como executar o "hello world" diretamente no hardware ARM virtualizado aqui .
fonte
Exemplos executáveis
Vamos criar e executar alguns minúsculos programas hello world do bare metal que são executados sem um sistema operacional:
Também os testaremos no emulador QEMU, tanto quanto possível, pois é mais seguro e conveniente para o desenvolvimento. Os testes do QEMU foram realizados em um host Ubuntu 18.04 com o QEMU 2.11.1 pré-empacotado.
O código de todos os exemplos x86 abaixo e mais está presente neste repositório do GitHub .
Como executar os exemplos no hardware real x86
Lembre-se de que exemplos em execução em hardware real podem ser perigosos, por exemplo, você pode limpar seu disco ou bloquear o hardware por engano: faça isso apenas em máquinas antigas que não contêm dados críticos! Ou melhor ainda, use devboards semi-descartáveis baratos, como o Raspberry Pi, veja o exemplo do ARM abaixo.
Para um laptop x86 típico, é necessário fazer algo como:
Grave a imagem em um pen drive (destruirá seus dados!):
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 T430, vejo o seguinte.
Depois de ligar, é quando eu tenho que pressionar Enter para entrar no menu de inicialização:
Então, aqui eu tenho que pressionar F12 para selecionar o USB como o dispositivo de inicialização:
A partir daí, posso selecionar o USB como o dispositivo de inicialização assim:
Como alternativa, para alterar a ordem de inicialização e escolher o USB com maior precedência, para que eu não precise selecioná-lo manualmente todas as vezes, pressione F1 na tela "Menu de interrupção de inicialização" e navegue até:
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:Resultado:
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.
E no T430, obtemos uma tela em branco com um cursor piscando:
main.img
contém o seguinte:\364
in octal ==0xf4
in hex: a codificação de umahlt
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:
quais saídas:
mas também está documentado 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 por0xaa
.Estes são 2 bytes mágicos necessários, que devem ser os bytes 511 e 512.
O BIOS passa por todos os nossos discos procurando por inicializáveis, e considera apenas inicializáveis aqueles que possuem esses dois bytes mágicos.
Se não estiver presente, o hardware não tratará isso como um disco inicializável.
Se você não é um
printf
mestre, pode confirmar o conteúdomain.img
com:que mostra o esperado:
onde
20
é um espaço em ASCII.O firmware do BIOS lê esses 512 bytes do disco, coloca-os na memória e define o PC como o primeiro byte para começar a executá-los.
Olá, setor de inicialização mundial
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 recebe caracteres de um terminal host.
Nos desktops, fica assim:
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.
É 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
GitHub upstream .
link.ld
Monte e vincule com:
Resultado:
E no T430:
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 bitscli
: 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
: imprima o código de montagem binário bruto, não o envolva em um arquivo ELF, como é o caso dos executáveis regulares da terra do usuário.Para entender melhor a parte do script do vinculador, familiarize-se com a etapa de realocação do link: O que os vinculadores fazem?
Programas cooler x86 bare metal
Aqui estão algumas configurações bare metal mais complexas que eu já consegui:
Use C em vez de montagem
Resumo: use a inicialização múltipla do GRUB, que resolverá muitos problemas irritantes em que você nunca pensou. Veja a seção abaixo.
A principal dificuldade do x86 é que o BIOS carrega apenas 512 bytes do disco na memória e é provável que você exploda esses 512 bytes ao usar o C!
Para resolver isso, podemos usar um carregador de inicialização de dois estágios . Isso faz outras chamadas do BIOS, que carregam mais bytes do disco na memória. Aqui está um exemplo mínimo de montagem do estágio 2 do zero usando as chamadas int 0x13 do BIOS :
Alternativamente:
-kernel
opção que carrega um arquivo ELF inteiro na memória. Aqui está um exemplo do ARM que eu criei com esse método .kernel7.img
, assim como o QEMU-kernel
.Apenas para fins educacionais, aqui está um exemplo C mínimo de um estágio :
main.c
entry.S
linker.ld
corre
Biblioteca padrão C
As coisas ficam mais divertidas se você também quiser usar a biblioteca padrão C, 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:
Escreva o seu próprio. É apenas um monte de cabeçalhos e arquivos C no final, certo? Certo??
Newlib
Exemplo detalhado em: /electronics/223929/c-standard-libraries-on-bare-metal/223931
Implementos newlib todas as coisas chatas não-OS específicos para você, por exemplo
memcmp
,memcpy
, etc.Em seguida, fornece alguns stubs para você implementar os syscalls necessários.
Por exemplo, podemos implementar
exit()
no ARM através de semi-hospedagem com:como mostrado neste exemplo .
Por exemplo, você pode redirecionar
printf
para os sistemas UART ou ARM ou implementarexit()
com semi - hospedagem .sistemas operacionais incorporados como FreeRTOS e Zephyr .
Esses sistemas operacionais normalmente permitem desativar o agendamento preventivo, fornecendo controle total sobre o tempo de execução do programa.
Eles podem ser vistos como uma espécie de Newlib pré-implementado.
Inicialização múltipla GNU GRUB
Os setores de inicialização são simples, mas não são muito convenientes:
É 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 meu 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.
Resultado do QEMU:
T430:
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 distribuições 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
.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:
Firmwares conhecidos incluem:
O firmware faz coisas como:
faça um loop sobre cada disco rígido, USB, rede etc. até encontrar algo inicializável.
Quando rodamos o QEMU,
-hda
diz quemain.img
é um disco rígido conectado ao hardware, ehda
é o primeiro a ser testado, e é usado.carregue os primeiros 512 bytes no endereço de memória RAM
0x7c00
, coloque o RIP da CPU lá e deixe-o executarmostrar 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 :
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.
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 nomake isoimage
usoisohybrid
.BRAÇO
No ARM, as idéias gerais são as mesmas.
Não há firmware pré-instalado semi-padronizado amplamente disponível como o BIOS para usar no IO; portanto, os dois tipos mais simples de IO que podemos fazer são:
Eu enviei:
alguns exemplos simples de QEMU C + Newlib e assembly bruto aqui no GitHub .
O exemplo prompt.c, por exemplo, recebe entrada do terminal host e retorna a saída durante todo o UART simulado:
Veja também: Como criar programas ARM bare metal e executá-los no QEMU?
uma configuração totalmente automatizada de blinker Raspberry Pi em: https://github.com/cirosantilli/raspberry-pi-bare-metal-blinker
Consulte também: Como executar um programa C sem SO no Raspberry Pi?
Para "ver" os LEDs no QEMU, é necessário compilar o QEMU da fonte com um sinalizador de depuração: /raspberrypi/56373/is-it-possible-to-get-the-state-of- the-leds-and-gpios-in-a-qemu-emulation-like-t
Em seguida, tente um olá mundo UART. Você pode começar pelo exemplo do pisca-pisca e substituir o kernel por este: https://github.com/dwelch67/raspberrypi/tree/bce377230c2cdd8ff1e40919fdedbc2533ef5a00/uart01
Primeiro, faça com que o UART trabalhe com o Raspbian, como expliquei em: /raspberrypi/38/prepare-for-ssh-without-a-screen/54394#54394 Será algo parecido com isto:
Certifique-se de usar os pinos corretos, ou você pode gravar o seu conversor UART para USB, já o fiz duas vezes em curto-circuito e 5V ...
Finalmente, conecte-se ao serial do host com:
Para o Raspberry Pi, usamos um cartão Micro SD em vez de um pendrive para conter nosso executável, para o qual você normalmente precisa de um adaptador para conectar-se ao seu computador:
Não se esqueça de desbloquear o adaptador SD, como mostrado em: /ubuntu/213889/microsd-card-is-set-to-read-only-state-how-can-i-write-data # 814585 # 814585
https://github.com/dwelch67/raspberrypi parece o tutorial mais popular sobre Raspberry Pi bare metal disponível hoje.
Algumas diferenças do x86 incluem:
IO é feito escrevendo diretamente para endereços de mágica, não há
in
eout
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.
Recursos
fonte
Sistema operacional como inspiração
O sistema operacional também é um programa , portanto, também podemos criar nosso próprio programa criando do zero ou alterando (limitando ou adicionando) recursos de um dos pequenos sistemas operacionais e, em seguida, executá-lo durante o processo de inicialização (usando uma imagem ISO ) .
Por exemplo, esta página pode ser usada como ponto de partida:
Como escrever um sistema operacional simples
Aqui, todo o sistema operacional se encaixa totalmente em um setor de inicialização de 512 bytes ( MBR )!
Esse SO simples ou similar pode ser usado para criar uma estrutura simples que nos permita:
Existem muitas possibilidades, no entanto. Por exemplo, para ver um sistema operacional de linguagem assembly x86 maior , podemos explorar o sistema operacional MykeOS , x86, que é uma ferramenta de aprendizado para mostrar que os sistemas operacionais simples de modo real de 16 bits funcionam com: código bem comentado e extensa documentação .
Boot Loader como inspiração
Outro tipo comum de programas que são executados sem o sistema operacional também são os Carregadores de Inicialização . Podemos criar um programa inspirado nesse conceito, por exemplo, usando este site:
Como desenvolver seu próprio Boot Loader
O artigo acima apresenta também a arquitetura básica de tais programas :
Como podemos ver, essa arquitetura é muito flexível e nos permite implementar qualquer programa , não necessariamente um gerenciador de inicialização.
Em particular, mostra como usar a técnica "código misto", graças à qual é possível combinar construções de alto nível (de C ou C ++ ) com comandos de baixo nível (do Assembler ). Este é um método muito útil, mas precisamos lembrar que:
O artigo mostra também como ver o programa criado em ação e como executar seus testes e depuração.
Aplicações UEFI como inspiração
Os exemplos acima usaram o fato de carregar o MBR do setor no suporte de dados. No entanto, podemos nos aprofundar mais, trabalhando, por exemplo, com os aplicativos UEFI :
Se gostaríamos de começar a criar esses programas , podemos, por exemplo, começar com estes sites:
Programando para EFI: Criando um Programa "Olá, Mundo" / Programação UEFI - Primeiros Passos
Explorando problemas de segurança como inspiração
É sabido que existe todo um grupo de softwares mal-intencionados (que são programas) que estão sendo executados antes do início do sistema operacional .
Um grande grupo deles opera no setor de MBR ou em aplicativos UEFI, assim como as soluções acima, mas também existem aqueles que usam outro ponto de entrada, como o Volume Boot Record (VBR) ou o BIOS :
ou talvez outro também.
Ataques antes da inicialização do sistema
Diferentes maneiras de inicializar
Eu também acho que, nesse contexto, também vale a pena mencionar que existem várias formas de inicializar o sistema operacional (ou o programa executável destinado a isso) . Existem muitos, mas gostaria de prestar atenção ao carregamento do código da rede usando a opção Network Boot ( PXE ), que nos permite executar o programa no computador, independentemente do sistema operacional e mesmo de qualquer mídia de armazenamento que seja. diretamente conectado ao computador:
O que é o Boot de Rede (PXE) e como você pode usá-lo?
fonte