{"id":19260521,"url":"https://github.com/andersonrezende/rust_os","last_synced_at":"2025-02-23T18:23:55.122Z","repository":{"id":248065166,"uuid":"827141705","full_name":"AndersonRezende/rust_os","owner":"AndersonRezende","description":"Simples kernel escrito em rust","archived":false,"fork":false,"pushed_at":"2024-08-26T00:30:00.000Z","size":35,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-01-05T09:43:44.336Z","etag":null,"topics":["operating-system","operational-systems","rust","so"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/AndersonRezende.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-07-11T05:06:09.000Z","updated_at":"2024-09-19T20:54:01.000Z","dependencies_parsed_at":"2024-07-12T06:48:15.599Z","dependency_job_id":"2896c0ab-f04a-4df0-a085-73118eb67ca4","html_url":"https://github.com/AndersonRezende/rust_os","commit_stats":null,"previous_names":["andersonrezende/rust_os"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AndersonRezende%2Frust_os","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AndersonRezende%2Frust_os/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AndersonRezende%2Frust_os/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AndersonRezende%2Frust_os/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/AndersonRezende","download_url":"https://codeload.github.com/AndersonRezende/rust_os/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":240357289,"owners_count":19788690,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["operating-system","operational-systems","rust","so"],"created_at":"2024-11-09T19:21:28.358Z","updated_at":"2025-02-23T18:23:55.003Z","avatar_url":"https://github.com/AndersonRezende.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Sistema operacional RUST_OS\n\n## Simples projeto de kernel utilizando a linguagem Rust.\n\n## Funcionamento\n### Processo de inicialização\n\u003cul\u003e\n\u003cli\u003eBIOS - Basic Input/Output System (Legacy)\u003c/li\u003e\n\u003cli\u003eUEFI - Unified Extensible Firmware Interface\u003c/li\u003e\n\u003c/ul\u003e\n\n### Processo de boot\nAo iniciar o computador, a CPU é colocada no modo de compatibilidade 16 bits, \ntambém chamado de modo real, com isso, bootloaders antigos (BIOS) podem ser carregados.\nO processo inicial consistem em carregar o BIOS de alguma memória flash localizada na \nplaca mãe. O BIOS executa rotinas de teste (POST - Power On Self Test) e inicialização\nde hardware, então ele procura por discos inicializáveis. Se ele encontrar um disco\ninicializável, o controle é transferido para seu bootloader, que é uma porção de 512\nbytes de código executável localizado no primeiro setor e na primeira trilha do disco.\nNormalmente, os bootloaders são maiores do que 512 bytes, então é comum os dividir em um\npequeno estágio que se encaixa nos primeiros 512 bytes e um segundo estágio que é carregado\nem sequência ao primeiro estágio.\n\nO bootloader tem que determinar a localização da imagem do kernel no disco e carregá-la \nna memória. Ele também precisa alterar a CPU do modo real de 16 bits para o modo protegido\nde 32 bits e, em seguida, para o modo longo de 64 bits, onde os registradores de  bits \ne a memória principal completa estão disponíveis. Sua terceira tarefa é consultar\ncertas informações (como mapa de memória) do BIOS e passá-las para o kernel do SO.\n\n### Multiboot\nPara evitar que cada sistema operacional implemente seu próprio bootloader compatível\napenas com um único SO, existe um padrão de bootloader aberto chamado de Multiboot.\nEsse padrão define uma interface entre o bootloader e o sistema operacional, de modo \nque qualquer bootloader compatível com Multiboot pode carregar qualquer sistema \noperacional compatível com Multiboot.\n\nPara tornar o kernel compatível com Multiboot, é preciso apenas inserir um cabeçalho\nchamado Multiboot no início do arquivo kernel. Isso torna muito fácil inicializar um SO\na partir do GRUB.\n\n\n## Configuração\n### Versão do Rust\nPrecisamos de recursos experimentais do rust, então através do gerenciador \u003cstrong\u003erustup\u003c/strong\u003e\nno diretório do projeto rodamos o comando\n``$ rustup override set nightly`` ou adicionar um arquivo ``rust-toolchain`` com ``nightly`` como \nconteúdo.\nCom isso, agora podemos habilitar a macro ``asm!\"`` e ``#![feature(asm)]``.\n\n### Especificação de Alvo\nO cargo suporta diferentes sistemas de destino por meio do parâmetro \u003cstrong\u003e--target\u003c/strong\u003e\nO destino é descrito por um chamado target triple, que descreve a arquitetura da CPU, o fornecedor,\no SO e o ABI.\n\nPara o nosso sistema de destino, no entanto, precisamos de alguns parâmetros de confiugrações\nespeciais (por exemplo, nenhum SO subjacente), então nenhum dos triplos de destino existentes \nse encaixa. Felizmente, o Rust nos permite definir nosso próprio destino por meio de um arquivo\nJSON.\n\n\u003cul\u003e\n\u003cli\u003e\"llvm-target\" e \"os: \"none\" =\u003e porque serão executados em bare metal.\u003c/li\u003e\n\u003cli\u003e\"linker-flavor\": \"ld.lld\", \"linker\": \"rust-lld\" =\u003e utilizaremos o vinculador LLD multiplataforma\nque é fornecido com o Rust para vincular o kernel.\n\u003c/li\u003e\n\u003cli\u003e\"panic-strategy\" : \"abort\" =\u003e semelhante ao que faz a configuração no cargo (podendo ser removida),\nespecifica que o alvo não suporta desenrolamento de pilha em panic, logo o programa\ndeverá ser abortado imediatamente.\n\u003c/li\u003e\n\u003cli\u003e\"disable-redzone\": \"true\" =\u003e precisamos lidar com interrupções em algum momento, para\nfazer isso com segurança temos que desabilitar otimização de ponteiro de pilha, pois \ncausaria corrupção de pilha.\n\u003c/li\u003e\n\u003cli\u003e\"features\": \"-mmx,-sse,+soft-float\" =\u003e Features habilita/desabilita recursos de destino.\nDesabilitamos o mmx e sse prefixando com sinal de \"-\" e habilitamos o soft-float com sinal de \"+\".\nOs recursos mmxe ssedeterminam o suporte para instruções Single Instruction Multiple Data (SIMD), \nque muitas vezes podem acelerar programas significativamente.\n\u003c/li\u003e\n\u003c/ul\u003e\n\n### Build-std-option\nA biblioteca principal é distribuida juinto com o compilador Rust com uma biblioteca\npré-compilada. Etnão ela é válida apenas para ambientes de target triple de hosts suportados,\nmas não nosso alvo personalizado. \n\nA build-std-option permite recompilar a biblioteca padrão sob demanda. Este é um dos recursos\ninstáveis disponíveis na versão nightly.\n\nPara utilizar o recurso, é necessário criar um arquivo de configuração do cargo local localizado\nem \"./cargo/config.toml\", sendo:\n\u003cul\u003e\n\u003cli\u003ebuild-std = [\"core\", \"compiler_builtins\"] =\u003e para dizer que deve recompilar as bibliotecas\ncore e compiler_builtins, sendo esta uma dependência do core.\n\u003c/li\u003e\n\u003c/ul\u003e\n\n### Intrínsecos relacionados à memória\nO compilador Rust assume que um certo conjunto de funções internas está disponível para todos \nos sistemas. A maioria dessas funções é fornecida pelo crate compiler_builtins que acabamos de\nrecompilar. No entante, há algumas funções relacionadas à memória nesse crate que nào são \nhabilitadas por padrão porque são normalmente fornecidas pela biblioteca em C no sistema.\nEssas funções incluem memset, que define todos os bytes em um bloco de memória para um \ndeterminado valor, memcpy, que cópia um bloco de memória para outro, e memcmp, que compara dois\nblocos de memória.\n\nComo não podemos víncular à biblioteca C do SO, precisamos de uma maneira alternativa de \nfornecer essas funções ao compilador. Uma abordagem poderia ser implementar nossas próprias \nfunções e aplicar o \"#[no_mangle]\" para evitar renomeação. Porém, devido à alta possibilidade\nde comportamentos indefinidos, é mais interessante reutilizar implementações já existentes.\n\nA crate compiler_builtins já contém implementações para as funções necessárias, elas são \ndesabilitadas por padrão para não conflitar com as implementações do C. Para habilitar, \ndefinimos:\n\u003cul\u003e\n\u003cli\u003ebuild-std-features = [\"compiler-builtins-mem\"]\u003c/li\u003e\n\u003cli\u003ebuild-std = [\"core\", \"compiler_builtins\"]\u003c/li\u003e\n\u003c/ul\u003e\n\nPara evitar passar o --target, podemos definir no arquivo .cargo/config.toml o seguinte \ntrecho:\n```\n[build]\ntarget = \"x86_64-blog_os.json\"\n```\n\n## Impressão na tela\nA maneira mais fácil para imprimir um texto na tela neste estágio é o \u003cstrong\u003eVGA text mode\u003c/strong\u003e. \nÉ uma área na memória especial mapeada para o hardware VGA que contém o conteúdo exibido na tela.\nNormalmente consistem em 25 linhas, cada uma contendo 80 células de caracteres. Cada célula de caractere\nexibe um caractere ASCII com algumas cores de texto e de fundo.\n\n### Executando o kernel\nPara transformar o kernel compilado em uma imagem de disco inicializável, precisamos transformar\no kernel compilado em uma imagem de disco inicializável, vinculando-o a um bootloader.\n\n### Criando uma Bootimage\nPara transformar o kernel compilado em uma imagem de disco inicializável, precisamos vinculá-lo a \num bootloader.\n\nPara poupar o trabalho de escrever o próprio bootloader, usamos a crate \"bootloader\". Este crate\nimplementa um bootloader básico de BIOS sem dependências de C, apenas Rust e assembly.\nPara usá-lo para inicializar o kernel, precisamos adicionar a dependência:\n```\n[dependencies]\nbootloader = \"0.9\"\n```\nSerá necessário também vincular o bootloader com o kernel após a compilação. Para isso utilizaremos\na ferramenta \"bootimage\" que primeiro compila o kernel e o bootloader e depois cria uma imagem\ninicializável. A ferramenta pode ser instalada com o seguinte comando:\n``$ cargo install bootimage``.\n\nPara executar o \"bootimage\" e construir o bootloader, é necessário ter o componente rustc \n\"llvm-tools-preview\" que pode ser instalado através do seguinte comando:\n``$ rustup component add llvm-tools-preview``.\n\nApós intalar o \"bootimage\" e adicionar o componente \"llvm-tools-preview\", você pode criar o disco\ninicializável através do comando: ``$ cargo bootimage``.\nA ferramenta compila o kernel usando o cargo build, então pega automaticamente quaisquer alterações feitas.\nDepois, ela compila o bootloader. Por último, ela combina o bootloader com o kernel em uma imagem de disco\ninicializável.\n\nApós executar o comando, você deve ver a imagem de disco inicializável chamada bootimage-rust-os.bin na pasta\n\"target/x86-64-rust-os/debug\". Você pode inicializá-la em uma máquina virtual, como o qemu, ou\ngravar em uma unidade USB.\n\nO bootimage compila o kernel em um formato ELF (executable and linkable format), depois\ncompila a dependência do bootloader como um executável autônomo e, por último, víncula os bytes do arquivo\nELF do kernel ao bootloader.\n\nQuando inicializado, o bootloader lê e analisa o arquivo ELF anexado. Ele então mapeia os segmentos do programa\npara endereços virtuais nas tabelas de páginas, zera a seção \".bss\" e configura uma pilha. Finalmente, ele\nlê o endereço do ponto de entrada (_start) e pula para ele.\n\n### Inicializando no QEMU\nPara executar com o qemu, basta executar o seguinte comando:\n``$ qemu-system-x86_64 -drive format=raw,file=target/x86_64-rust_os/debug/bootimage-rust_os.bin``\n\n### Máquina Real\nPara gravá-lo em um pen-drive e inicializá-lo em uma máquina real basta executar o seguinte comando\nadaptando para o caso específico, onde sdX deve ser a unidade do pen-drive:\n``dd if=target/x86_64-blog_os/debug/bootimage-blog_os.bin of=/dev/sdX \u0026\u0026 sync``\n\n### Usando cargo run\nPara facilitar a execução do kernel no qemu, podemos definir a chave runner de configuração para o cargo:\n```\n[target.'cfg(target_os = \"none\")']\nrunner = \"bootimage runner\"\n```\n\n## Modo de texto VGA\nÉ o modo mais simples de exibir texto em tela.\n### Buffer de texto VGA\nPara escrever um caractere na tela no modo VGA é necessário escrever no buffer de texto do hardware VGA.\nO buffer de texto VGA é uma matriz bidimensional com 80 colunas e 25 linhas que é renderizado diretamente na tela.\nCada entrada na matriz descreve um único caractere através do seguinte formato:\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n\u003cth\u003eBit(s)\u003c/th\u003e\n\u003cth\u003eValor\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003cth\u003e0-7\u003c/th\u003e\n\u003ctd\u003eCódigo ASCII do caractere\u003c/td\u003e\n\u003ctr\u003e\n\u003ctd\u003e8-11\u003c/td\u003e\n\u003ctd\u003eCor do caractere\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e12-14\u003c/td\u003e\n\u003ctd\u003eCor de fundo\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e15\u003c/td\u003e\n\u003ctd\u003ePiscar\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\nO buffer de texto é acessível através de Memory-mapped I/O (MMIO) no endereço 0xb8000. Isso significa que as leituras e \ngravações nesse endereço não acessam a memória RAM, mas sim o buffer no hardware do VGA.\n\n\n## Testando\n### Testando em Rust\nO Rust possui uma framework interna capaz de realizar testes unitários sem a necessidade de configurar nada. É apenas \nnecessário criar uma função que verifique se assertations são válidas. Essas funções possuem o atributo #[test] na \ndeclaração da função. Com isso, o comando ```$ cargo test``` irá automaticamente realizar os testes nessas funções.\n\nPorém, como estamos utilizando um ambiente \u003cstrong\u003eno_std\u003c/strong\u003e, é necessário realizar alguns processos a mais para\nconfigurar uma framework de testes. Isso se dá por conta que a framework de testes do Rust utiliza dependências da \nbiblioteca padrão (std).\n\n### Framework de teste customizada\nO Rust suporta a substituição da framework padrão através de ```custom_test_frameworks```. Essa feature não requer \nbibliotecas externas, logo funciona em ambientes ```no_std```. Ela funciona coletando todas as funções com a anotação\n```#[test_case]``` e então as invoca por meio de uma função runner ``#![test_runner(crate::test_runner)]`` com a lista \nde testes a serem executados como argumento.\n\nA implementação da framework de testes customizada se dá por meio das seguintes anotações: \n\u003cul\u003e\n\u003cli\u003e\"#![feature(custom_test_frameworks)]\": implementa a própria framework de testes\u003c/li\u003e\n\u003cli\u003e\"#![test_runner(crate::test_runner)]\": invoca a própria função executora test_runner\u003c/li\u003e\n\u003c/ul\u003e\nA função executora recebe uma lista de argumentos que são os testes e executa cada um deles.\n\nA desvantagem em comparação com o framework de teste padrão é que recursos avançados, como ``should_panic`` não estão \ndisponíveis. Em vez disso, será necessário implementar esses recursos por conta própria.\n\n### Portas I/O\nPara testar com apoio do qemu é necessário configurar uma comunicação entre o guest e o host. Essa comunicação pode ser \nfeita por meio de memória mapeada de I/O ou portas mapeadas de I/O. Já foi utilizado o mapeamento de memória com o VGA \nbuffer através do endereço 0xb8000. Esse endereço não é mapeado para a RAM, mas sim para a memória do dispositivo VGA.\n\nEm contraste, a comunicação por portas mapeadas I/O utiliza uma \"trilha\" de comunicação separada. Essas trilhas se \nconectam a diferentes periféricos que possuem uma ou mais portas acessadas por meio de seus números. A comunicação com \nesses dispositivos é feito por meio das instruções assembly ```in``` e ```out```, onde cada um leva um número de porta e\ndados.\n\nEssa comunicação é necessária para enviar um comando para o qemu para que o mesmo seja encerrado após o término dos testes.\n\n### Imprimindo no console\nPara ver o resultado dos testes no console é necessário enviar dados entre o guest e o host. Uma maneira de realizar essa\ncomunicação é por meio de portas seriais.\nExiste uma interface chamada de UART. Essa interface utiliza implementações de portas I/O. A primeira porta padrão serial \né a de número 0x3F8.\n\nUtilizamos o crate \"uart_16550\" para inicializar a UART e enviar dados através da porta serial. No arquivo \"src/serial.rs\"\ninicializamos a crate e definimos macros para facilitar a utilização da porta serial.\n\n### Teste de integração\nA convenção para definição de testes integrados em Rust é colocá-los em um diretório \"tests\" na raiz do projeto. Os testes\ndesses diretórios são identificados automaticamente.\n\nCada teste de integração deve ser autoexecutável e é separado do \"main.rs\". Isso significa que precisamos definir uma \nfunção de ponto de entrada para cada um. Por serem executáveis separados, precisamos definir alguns atributos:\n\u003cul\u003e\n\u003cli\u003e#![no_std]: não utiliza a biblioteca padrão.\u003c/li\u003e\n\u003cli\u003e#![no_main]: não utilizamos o ponto de entrada padrão.\u003c/li\u003e\n\u003cli\u003e#![feature(custom_test_frameworks)]: informamos que é uma framework de teste customizada. Funciona coletando os #[test_case]\u003c/li\u003e\n\u003cli\u003e#![test_runner(crate::test_runner)]: função executora que receberá a lista de funções de testes a serem executadas.\u003c/li\u003e\n\u003cli\u003e#![reexport_test_harness_main = \"test_main\"]: definimos o nome da função de entrada.\u003c/li\u003e\n\u003c/ul\u003e\n\n\n## Exceções da CPU\nExceções de CPU podem ocorrer em várias situações como acessar um endereço de memória inválido ou divisão por zero. Para \nreagir a cada uma dessas opções temos que configurar uma tabela de descritores de interrupção que forneça funções de \nmanipulador.\n\n### Visão geral\nUma exceção sinaliza que algo está errado na instrução atual. Quando ocorre uma exceção, a CPU interrompe o seu trabalho\natual e chama uma função manipuladora específica para o tipo de exceção lançada.\nNo x86 há cerca de 20 diferentes tipos de exceção de CPU.\n\n### Tabela de descritores de interrupção\nPara capturar e manipular exceções temos que configurar uma IDT (interrupt descriptor table). Nessa tabela podemos \nespecificar uma função de manipulador para cada exceção da CPU.\n\nQuando ocorre uma exceção, a CPU faz basicamente o seguinte:\n\u003col\u003e\n\u003cli\u003eEmpilhar alguns registradores na pilha, incluindo o ponteiro de instrução e o registrador RFLAGS.\u003c/li\u003e\n\u003cli\u003eLer a entrada correspondente ao IDT, por exemplo, a CPU lê a entrada 14 quando ocorre um page fault.\u003c/li\u003e\n\u003cli\u003eVerifica se a entrada está presente, se não estiver registra uma dupla falta.\u003c/li\u003e\n\u003cli\u003eDesabilita interrupções de hardware se a entrada for uma porta de interrupção.\u003c/li\u003e\n\u003cli\u003eCarrega o seletor GDT especificado no CS (code segment).\u003c/li\u003e\n\u003cli\u003eIr para a função manipuladora especificada.\u003c/li\u003e\n\u003c/ol\u003e\n\n### Convenção para chamadas de interrupção\nExceções se assemelham a funções, a CPU salta para um endereço que será executado e retorna posteriormente a execução.\nNo entanto, há uma grande diferença entre exceções e funções, uma chamada de função é invocada voluntariamente por uma \ninstrução ``call`` enquanto uma exceção pode ocorrer em qualquer instrução.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandersonrezende%2Frust_os","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fandersonrezende%2Frust_os","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandersonrezende%2Frust_os/lists"}