Add Portuguese (pt-BR) translation (12 posts + base files) (#1443)

This PR adds an initial **Portuguese (pt-BR)** translation of the blog.

- Translated all 12 existing posts.
- Translated configuration files (`config.toml`) and main `_index.md`
pages.
- Some parts may still need review or adjustment - I’ll be happy to fix
(me or someone else) any issues pointed out.
- Related issue: #1442

Thanks for this amazing project! I hope this helps make it more
accessible to Portuguese-speaking learners.

> Note: I tried to keep all technical terms (like “kernel”,
“bootloader”, “interrupts”) in English where appropriate, following the
convention.
This commit is contained in:
Philipp Oppermann
2025-12-08 16:39:46 +01:00
committed by GitHub
18 changed files with 10045 additions and 1 deletions

View File

@@ -34,7 +34,19 @@ skip_anchor_prefixes = [
subtitle = "Philipp Oppermann's blog"
author = { name = "Philipp Oppermann" }
default_language = "en"
languages = ["en", "zh-CN", "zh-TW", "fr", "ja", "fa", "ru", "ko", "ar", "es"]
languages = [
"en",
"ar",
"es",
"fa",
"fr",
"ja",
"ko",
"pt-BR",
"ru",
"zh-CN",
"zh-TW",
]
[translations]
lang_name = "English (original)"
@@ -279,3 +291,28 @@ support_me = """
comment_note = """
¿Tienes algún problema, quieres compartir comentarios o discutir más ideas? ¡No dudes en dejar un comentario aquí! Por favor, utiliza inglés y sigue el <a href=\"https://www.rust-lang.org/policies/code-of-conduct\">código de conducta</a> de Rust. Este hilo de comentarios se vincula directamente con una <a href=\"_discussion_url_\"><em>discusión en GitHub</em></a>, así que también puedes comentar allí si lo prefieres.
"""
# Portuguese (Brazil)
[languages.pt-BR]
title = "Escrevendo um OS em Rust"
description = "Esta série de blog cria um pequeno sistema operacional na linguagem de programação Rust. Cada post é um pequeno tutorial e inclui todo o código necessário."
[languages.pt-BR.translations]
lang_name = "Português (Brasil)"
toc = "Tabela de Conteúdos"
all_posts = "« Todos os Posts"
comments = "Comentários"
comments_notice = "Por favor, deixe seus comentários em inglês se possível."
readmore = "ler&nbsp;mais&nbsp;»"
not_translated = "(Esta postagem ainda não foi traduzida.)"
translated_content = "Conteúdo Traduzido:"
translated_content_notice = "Esta é uma tradução comunitária do post <strong><a href=\"_original.permalink_\">_original.title_</a></strong>. Pode estar incompleta, desatualizada ou conter erros. Por favor, reporte qualquer problema!"
translated_by = "Traduzido por"
translation_contributors = "Com contribuições de"
word_separator = "e"
support_me = """
<h2>Apoie-me</h2>
<p>Criar e manter este blog e as bibliotecas associadas dá muito trabalho, mas eu realmente gosto de fazê-lo. Ao me apoiar, você me permite investir mais tempo em novo conteúdo, novos recursos e manutenção contínua. A melhor forma de me apoiar é <a href="https://github.com/sponsors/phil-opp"><em>me patrocinar no GitHub</em></a>. Obrigado!</p>
"""
comment_note = """
Teve algum problema, quer deixar um feedback ou discutir mais ideias? Fique à vontade para deixar um comentário aqui! Por favor, use o inglês e siga o <a href="https://www.rust-lang.org/policies/code-of-conduct">código de conduta</a> do Rust. Este tópico de comentários está diretamente vinculado a uma <a href="_discussion_url_"><em>discussão no GitHub</em></a>, então você também pode comentar lá se preferir.
"""

View File

@@ -0,0 +1,13 @@
+++
template = "edition-2/index.html"
+++
<h1 style="visibility: hidden; height: 0px; margin: 0px; padding: 0px;">Escrevendo um OS em Rust</h1>
<div class="front-page-introduction">
Esta série de posts do blog cria um pequeno sistema operacional na [linguagem de programação Rust](https://www.rust-lang.org/). Cada post é um pequeno tutorial e inclui todo o código necessário, então você pode acompanhar se quiser. O código-fonte também está disponível no [repositório Github](https://github.com/phil-opp/blog_os) correspondente.
Último post: <!-- latest-post -->
</div>

View File

@@ -0,0 +1,599 @@
+++
title = "Um Binário Rust Independente"
weight = 1
path = "pt-BR/freestanding-rust-binary"
date = 2018-02-10
[extra]
chapter = "O Básico"
# Please update this when updating the translation
translation_based_on_commit = "624f0b7663daca1ce67f297f1c450420fbb4d040"
# GitHub usernames of the people that translated this post
translators = ["richarddalves"]
+++
O primeiro passo para criar nosso próprio kernel de sistema operacional é criar um executável Rust que não vincule a biblioteca padrão. Isso torna possível executar o código Rust no [bare metal] sem um sistema operacional subjacente.
[bare metal]: https://en.wikipedia.org/wiki/Bare_machine
<!-- more -->
Este blog é desenvolvido abertamente no [GitHub]. Se você tiver algum problema ou dúvida, abra um issue lá. Você também pode deixar comentários [na parte inferior]. O código-fonte completo desta publicação pode ser encontrado na banch [`post-01`][post branch].
[GitHub]: https://github.com/phil-opp/blog_os
[na parte inferior]: #comments
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
[post branch]: https://github.com/phil-opp/blog_os/tree/post-01
<!-- toc -->
## Introdução
Para escrever um kernel de sistema operacional, precisamos de código que não dependa de nenhum recurso do sistema operacional. Isso significa que não podemos usar threads, arquivos, memória heap, rede, números aleatórios, saída padrão ou qualquer outro recurso que exija abstrações do sistema operacional ou hardware específico. O que faz sentido, já que estamos tentando escrever nosso próprio sistema operacional e nossos próprios drivers.
Isso significa que não podemos usar a maior parte da [biblioteca padrão do Rust], mas há muitos recursos do Rust que _podemos_ usar. Por exemplo, podemos usar [iteradores], [closures], [pattern matching], [option] e [result], [formatação de string] e, claro, o [sistema de ownership]. Esses recursos tornam possível escrever um kernel de uma maneira muito expressiva e de alto nível, sem nos preocuparmos com [undefined behavior] ou [memory safety].
[option]: https://doc.rust-lang.org/core/option/
[result]:https://doc.rust-lang.org/core/result/
[Rust standard library]: https://doc.rust-lang.org/std/
[iteradores]: https://doc.rust-lang.org/book/ch13-02-iterators.html
[closures]: https://doc.rust-lang.org/book/ch13-01-closures.html
[pattern matching]: https://doc.rust-lang.org/book/ch06-00-enums.html
[formatação de string]: https://doc.rust-lang.org/core/macro.write.html
[sistema de ownership]: https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html
[undefined behavior]: https://www.nayuki.io/page/undefined-behavior-in-c-and-cplusplus-programs
[memory safety]: https://tonyarcieri.com/it-s-time-for-a-memory-safety-intervention
Para criar um kernel de sistema operacional em Rust, precisamos criar um executável que possa ser executado sem um sistema operacional subjacente. Esse executável é frequentemente chamado de executável “autônomo” ou “bare-metal”.
Este post descreve as etapas necessárias para criar um binário Rust independente e explica por que essas etapas são necessárias. Se você estiver interessado apenas em um exemplo mínimo, pode **[ir para o resumo](#resumo)**.
## Desativando a biblioteca padrão
Por padrão, todos as crates Rust vinculam a [biblioteca padrão], que depende do sistema operacional para recursos como threads, arquivos ou rede. Ela também depende da biblioteca padrão C `libc`, que interage intimamente com os serviços do sistema operacional. Como nosso plano é escrever um sistema operacional, não podemos usar nenhuma biblioteca dependente de um sistema operacional.
Portanto, temos que desativar a inclusão automática da biblioteca padrão por meio do [atributo `no_std`].
[biblioteca padrão]: https://doc.rust-lang.org/std/
[atributo `no_std`]: https://doc.rust-lang.org/1.30.0/book/first-edition/using-rust-without-the-standard-library.html
Começamos criando um novo projeto de binário cargo. A maneira mais fácil de fazer isso é através da linha de comando:
```
cargo new blog_os --bin --edition 2024
```
Eu nommei o projeto `blog_os`, mas claro que você pode escolher o seu próprio nome. A flag `--bin` especifica que queremos criar um executável binário (em contraste com uma biblioteca) e a flag `--edition 2024` especifica que queremos usar a [edição 2024] de Rust para nossa crate. Quando executamos o comando, o cargo cria a seguinte estrutura de diretório para nós:
[edição 2024]: https://doc.rust-lang.org/nightly/edition-guide/rust-2024/index.html
```
blog_os
├── Cargo.toml
└── src
└── main.rs
```
O `Cargo.toml` contém a configuração da crate, por exemplo o nome da crate, o autor, o número da [versão semântica] e dependências. O arquivo `src/main.rs` contém o módulo raiz da nossa crate e nossa função `main`. Você pode compilar sua crate através de `cargo build` e então executar o binário compilado `blog_os` na subpasta `target/debug`.
[versão semântica]: https://semver.org/
### O Atributo `no_std`
Agora nossa crate implicitamente vincula a biblioteca padrão. Vamos tentar desativar isso adicionando o [atributo `no_std`]:
```rust
// main.rs
#![no_std]
fn main() {
println!("Olá, mundo!");
}
```
Quando tentamos compilá-lo agora (executando `cargo build`), o seguinte erro ocorre:
```
error: cannot find macro `println!` in this scope
--> src/main.rs:4:5
|
4 | println!("Olá, mundo!");
| ^^^^^^^
```
A razão deste erro é que a [macro `println`] é parte da biblioteca padrão, que não incluímos mais. Então não conseguimos mais imprimir coisas. Isso faz sentido, já que `println` escreve no [standard output], que é um descritor de arquivo especial fornecido pelo sistema operacional.
[macro `println`]: https://doc.rust-lang.org/std/macro.println.html
[standard output]: https://en.wikipedia.org/wiki/Standard_streams#Standard_output_.28stdout.29
Então vamos remover o println!() e tentar novamente com uma função main vazia:
```rust
// main.rs
#![no_std]
fn main() {}
```
```
> cargo build
error: `#[panic_handler]` function required, but not found
error: language item required, but not found: `eh_personality`
```
Agora o compilador está pedindo uma função `#[panic_handler]` e um _item de linguagem_.
## Implementação de Panic
O atributo `panic_handler` define a função que o compilador deve invocar quando ocorre um [panic]. A biblioteca padrão fornece sua própria função de tratamento de panic, mas em um ambiente `no_std` precisamos defini-la nós mesmos:
[panic]: https://doc.rust-lang.org/stable/book/ch09-01-unrecoverable-errors-with-panic.html
```rust
// in main.rs
use core::panic::PanicInfo;
/// Esta função é chamada em caso de pânico.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
```
O parâmetro [`PanicInfo`][PanicInfo] contém o arquivo e a linha onde o panic aconteceu e a mensagem de panic opcional. A função nunca deve retornar, então é marcada como uma [função divergente] ao retornar o [tipo “never”] `!`. Não há muito que possamos fazer nesta função por enquanto, então apenas fazemos um loop infinito.
[PanicInfo]: https://doc.rust-lang.org/nightly/core/panic/struct.PanicInfo.html
[função divergente]: https://doc.rust-lang.org/1.30.0/book/first-edition/functions.html#diverging-functions
[tipo “never”]: https://doc.rust-lang.org/nightly/std/primitive.never.html
## O Item de Linguagem `eh_personality`
Items de linguagem são funções e tipos especiais necessários internamente pelo compilador. Por exemplo, a trait [`Copy`] é um item de linguagem que diz ao compilador quais tipos têm [_semântica de cópia_][`Copy`]. Quando olhamos para a [implementação][copy code], vemos que tem o atributo especial `#[lang = "copy"]` que o define como um item de linguagem (_Language Item_ em inglês).
[`Copy`]: https://doc.rust-lang.org/nightly/core/marker/trait.Copy.html
[copy code]: https://github.com/rust-lang/rust/blob/485397e49a02a3b7ff77c17e4a3f16c653925cb3/src/libcore/marker.rs#L296-L299
Enquanto é possível fornecer implementações customizadas de items de linguagem, isso deve ser feito apenas como último recurso. A razão é que items de linguagem são detalhes de implementação altamente instáveis e nem mesmo verificados de tipo (então o compilador não verifica se uma função tem os tipos de argumento corretos). Felizmente, há uma forma mais estável de corrigir o erro de item de linguagem acima.
O [item de linguagem `eh_personality`] marca uma função que é usada para implementar [stack unwinding]. Por padrão, Rust usa unwinding para executar os destructores de todas as variáveis da stack vivas em caso de [panic]. Isso garante que toda memória usada seja liberada e permite que a thread pai capture o panic e continue a execução. Unwinding, no entanto, é um processo complicado e requer algumas bibliotecas específicas do SO (por exemplo, [libunwind] no Linux ou [tratamento estruturado de exceção] no Windows), então não queremos usá-lo para nosso sistema operacional.
[item de linguagem `eh_personality`]: https://github.com/rust-lang/rust/blob/edb368491551a77d77a48446d4ee88b35490c565/src/libpanic_unwind/gcc.rs#L11-L45
[stack unwinding]: https://www.bogotobogo.com/cplusplus/stackunwinding.php
[libunwind]: https://www.nongnu.org/libunwind/
[tratamento estruturado de exceção]: https://docs.microsoft.com/en-us/windows/win32/debug/structured-exception-handling
### Desativando o Unwinding
Existem outros casos de uso também para os quais unwinding é indesejável, então Rust fornece uma opção para [abortar no panic] em vez disso. Isso desativa a geração de informações de símbolo de desenrolar e reduz consideravelmente o tamanho do binário. Há múltiplos locais onde podemos desativar o unwinding. A forma mais fácil é adicionar as seguintes linhas ao nosso `Cargo.toml`:
```toml
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
```
Isso define a estratégia de panic para `abort` tanto para o perfil `dev` (usado para `cargo build`) quanto para o perfil `release` (usado para `cargo build --release`). Agora o item de linguagem `eh_personality` não deve mais ser necessário.
[abortar no panic]: https://github.com/rust-lang/rust/pull/32900
Agora corrigimos ambos os erros acima. No entanto, se tentarmos compilar agora, outro erro ocorre:
```
> cargo build
error: requires `start` lang_item
```
Está faltando o item de linguagem `start` no nosso programa, que define o ponto de entrada.
## O Atributo `start`
Alguém pode pensar que a função `main` é a primeira função chamada quando você executa um programa. No entanto, a maioria das linguagens tem um [sistema de runtime], que é responsável por coisas como coleta de lixo (por exemplo, em Java) ou threads de software (por exemplo, goroutines em Go). Este runtime precisa ser chamado antes de `main`, já que ele precisa se inicializar a si mesmo.
[sistema de runtime]: https://en.wikipedia.org/wiki/Runtime_system
Em um binário Rust típico que vincula a biblioteca padrão, a execução começa em uma biblioteca de runtime C chamada `crt0` ("C runtime zero"), que configura o ambiente para uma aplicação C. Isso inclui criar um stack e colocar os argumentos nos registradores certos. O runtime C então invoca o [ponto de entrada do runtime Rust][rt::lang_start], que é marcado pelo item de linguagem `start`. Rust tem apenas um runtime muito mínimo, que cuida de algumas poucas coisas como configurar guardas de estouro do stack ou imprimir um backtrace ao fazer panic. O runtime então finalmente chama a função `main`.
[rt::lang_start]: https://github.com/rust-lang/rust/blob/bb4d1491466d8239a7a5fd68bd605e3276e97afb/src/libstd/rt.rs#L32-L73
Nosso executável independente não tem acesso ao runtime Rust e ao `crt0`, então precisamos definir nosso próprio ponto de entrada. Implementar o item de linguagem `start` não ajudaria, já que ainda exigiria `crt0`. Em vez disso, precisamos sobrescrever diretamente o ponto de entrada `crt0`.
### Sobrescrevendo o Ponto de Entrada (Entry Point)
Para dizer ao compilador Rust que não queremos usar a cadeia normal de ponto de entrada, adicionamos o atributo `#![no_main]`.
```rust
#![no_std]
#![no_main]
use core::panic::PanicInfo;
/// Esta função é chamada em caso de pânico.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
```
Você pode notar que removemos a função `main`. A razão é que um `main` não faz sentido sem um runtime subjacente que o chame. Em vez disso, estamos agora sobrescrevendo o ponto de entrada do sistema operacional com nossa própria função `_start`:
```rust
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
loop {}
}
```
Ao usar o atributo `#[unsafe(no_mangle)]`, desativamos [mangling de nomes] para garantir que o compilador Rust realmente produza uma função com o nome `_start`. Sem o atributo, o compilador geraria algum símbolo criptografado como `_ZN3blog_os4_start7hb173fedf945531caE` para dar a cada função um nome único. O atributo é necessário porque precisamos dizer o nome da função do ponto de entrada ao linker no próximo passo.
Também temos que marcar a função como `extern "C"` para dizer ao compilador que ele deve usar a [convenção de chamada C] para esta função (em vez da convenção de chamada Rust não especificada). A razão de nomear a função `_start` é que este é o nome do ponto de entrada padrão para a maioria dos sistemas.
[mangling de nomes]: https://en.wikipedia.org/wiki/Name_mangling
[convenção de chamada C]: https://en.wikipedia.org/wiki/Calling_convention
O tipo de retorno `!` significa que a função é divergente, ou seja, não é permitida retornar nunca. Isso é necessário porque o ponto de entrada não é chamado por nenhuma função, mas invocado diretamente pelo sistema operacional ou bootloader. Então em vez de retornar, o ponto de entrada deve por exemplo invocar a [chamada de sistema `exit`] do sistema operacional. No nosso caso, desligar a máquina poderia ser uma ação razoável, já que não há nada mais a fazer se um binário independente retorna. Por enquanto, cumprimos o requisito fazendo um loop infinito.
[chamada de sistema `exit`]: https://en.wikipedia.org/wiki/Exit_(system_call)
Quando executamos `cargo build` agora, recebemos um feio erro de _linker_.
## Erros do Linker
O linker é um programa que combina o código gerado em um executável. Como o formato executável difere entre Linux, Windows e macOS, cada sistema tem seu próprio linker que lança um erro diferente. A causa fundamental dos erros é a mesma: a configuração padrão do linker assume que nosso programa depende do runtime C, o que não é o caso.
Para resolver os erros, precisamos dizer ao linker que ele não deve incluir o runtime C. Podemos fazer isso passando um certo conjunto de argumentos ao linker ou compilando para um alvo bare metal.
### Compilando para um Alvo Bare Metal
Por padrão, Rust tenta construir um executável que seja capaz de executar no seu ambiente de sistema atual. Por exemplo, se você estiver usando Windows em `x86_64`, Rust tenta construir um executável `.exe` Windows que usa instruções `x86_64`. Este ambiente é chamado seu sistema "host".
Para descrever ambientes diferentes, Rust usa uma string chamada [_target triple_]. Você pode ver o target triple do seu sistema host executando `rustc --version --verbose`:
[_target triple_]: https://clang.llvm.org/docs/CrossCompilation.html#target-triple
```
rustc 1.91.0 (f8297e351 2025-10-28)
binary: rustc
commit-hash: f8297e351a40c1439a467bbbb6879088047f50b3
commit-date: 2025-10-28
host: x86_64-unknown-linux-gnu
release: 1.91.0
LLVM version: 21.1.2
```
A saída acima é de um sistema Linux `x86_64`. Vemos que o triple `host` é `x86_64-unknown-linux-gnu`, que inclui a arquitetura de CPU (`x86_64`), o vendor (`unknown`), o sistema operacional (`linux`), e a [ABI] (`gnu`).
[ABI]: https://en.wikipedia.org/wiki/Application_binary_interface
Ao compilar para nosso triple host, o compilador Rust e o linker assumem que há um sistema operacional subjacente como Linux ou Windows que usa o runtime C por padrão, o que causa os erros do linker. Então, para evitar os erros do linker, podemos compilar para um ambiente diferente sem nenhum sistema operacional subjacente.
Um exemplo de tal ambiente bare metal é o target triple `thumbv7em-none-eabihf`, que descreve um sistema [embarcado] [ARM]. Os detalhes não são importantes, tudo o que importa é que o target triple não tem nenhum sistema operacional subjacente, o que é indicado pelo `none` no target triple. Para ser capaz de compilar para este alvo, precisamos adicioná-lo em rustup:
[embarcado]: https://en.wikipedia.org/wiki/Embedded_system
[ARM]: https://en.wikipedia.org/wiki/ARM_architecture
```
rustup target add thumbv7em-none-eabihf
```
Isso baixa uma cópia da biblioteca padrão std (e core) para o sistema. Agora podemos compilar nosso executável independente para este alvo:
```
cargo build --target thumbv7em-none-eabihf
```
Ao passar um argumento `--target`, nós fazemos uma compilação [cross compile] nosso executável para um sistema alvo bare metal. Como o sistema alvo não tem sistema operacional, o linker não tenta vincular o runtime C e nossa compilação é bem-sucedida sem nenhum erro de linker.
[cross compile]: https://en.wikipedia.org/wiki/Cross_compiler
Esta é a abordagem que usaremos para construir nosso kernel de SO. Em vez de `thumbv7em-none-eabihf`, usaremos um [alvo customizado] que descreve um ambiente bare metal `x86_64`. Os detalhes serão explicados no próximo post.
[alvo customizado]: https://doc.rust-lang.org/rustc/targets/custom.html
### Argumentos do Linker
Em vez de compilar para um sistema bare metal, também é possível resolver os erros do linker passando um certo conjunto de argumentos ao linker. Esta não é a abordagem que usaremos para nosso kernel, portanto esta seção é opcional e fornecida apenas para completude. Clique em _"Argumentos do Linker"_ abaixo para mostrar o conteúdo opcional.
<details>
<summary>Argumentos do Linker</summary>
Nesta seção discutimos os erros do linker que ocorrem no Linux, Windows e macOS, e explicamos como resolvê-los passando argumentos adicionais ao linker. Note que o formato executável e o linker diferem entre sistemas operacionais, então que um conjunto diferente de argumentos é necessário para cada sistema.
#### Linux
No Linux, o seguinte erro de linker ocorre (encurtado):
```
error: linking with `cc` failed: exit code: 1
|
= note: "cc" […]
= note: /usr/lib/gcc/../x86_64-linux-gnu/Scrt1.o: In function `_start':
(.text+0x12): undefined reference to `__libc_csu_fini'
/usr/lib/gcc/../x86_64-linux-gnu/Scrt1.o: In function `_start':
(.text+0x19): undefined reference to `__libc_csu_init'
/usr/lib/gcc/../x86_64-linux-gnu/Scrt1.o: In function `_start':
(.text+0x25): undefined reference to `__libc_start_main'
collect2: error: ld returned 1 exit status
```
O problema é que o linker inclui a rotina de inicialização do runtime C por padrão, que também é chamada `_start`. Ela requer alguns símbolos da biblioteca padrão C `libc` que não incluímos devido ao atributo `no_std`, portanto o linker não consegue resolver estas referências. Para resolver isso, podemos dizer ao linker que ele não deve vincular a rotina de inicialização C passando a flag `-nostartfiles`.
Uma forma de passar atributos de linker via cargo é o comando `cargo rustc`. O comando se comporta exatamente como `cargo build`, mas permite passar opções para `rustc`, o compilador Rust subjacente. `rustc` tem a flag `-C link-arg`, que passa um argumento ao linker. Combinados, nosso novo comando de compilação se parece com isso:
```
cargo rustc -- -C link-arg=-nostartfiles
```
Agora nossa crate compilada como um executável independente no Linux!
Não precisávamos especificar o nome da nossa função de ponto de entrada explicitamente, já que o linker procura por uma função com o nome `_start` por padrão.
#### Windows
No Windows, um erro de linker diferente ocorre (encurtado):
```
error: linking with `link.exe` failed: exit code: 1561
|
= note: "C:\\Program Files (x86)\\…\\link.exe" […]
= note: LINK : fatal error LNK1561: entry point must be defined
```
O erro "entry point must be defined" (ponto de entrada deve ser definido) significa que o linker não consegue encontrar o ponto de entrada. No Windows, o nome do ponto de entrada padrão [depende do subsistema usado][windows-subsystems]. Para o subsistema `CONSOLE`, o linker procura por uma função nomeada `mainCRTStartup` e para o subsistema `WINDOWS`, ele procura por uma função nomeada `WinMainCRTStartup`. Para sobrescrever o padrão e dizer ao linker para procurar por nossa função `_start` em vez disso, podemos passar um argumento `/ENTRY` ao linker:
[windows-subsystems]: https://docs.microsoft.com/en-us/cpp/build/reference/entry-entry-point-symbol
```
cargo rustc -- -C link-arg=/ENTRY:_start
```
Do formato de argumento diferente, vemos claramente que o linker Windows é um programa completamente diferente do linker Linux.
Agora um erro de linker diferente ocorre:
```
error: linking with `link.exe` failed: exit code: 1221
|
= note: "C:\\Program Files (x86)\\…\\link.exe" […]
= note: LINK : fatal error LNK1221: a subsystem can't be inferred and must be
defined
```
Este erro ocorre porque os executáveis Windows podem usar [subsistemas] diferentes[windows-subsystems]. Para programas normais, eles são inferidos dependendo do nome do ponto de entrada: Se o ponto de entrada é nomeado `main`, o subsistema `CONSOLE` é usado, e se o ponto de entrada é nomeado `WinMain`, o subsistema `WINDOWS` é usado. Como nossa função `_start` tem um nome diferente, precisamos especificar o subsistema explicitamente:
```
cargo rustc -- -C link-args="/ENTRY:_start /SUBSYSTEM:console"
```
Usamos o subsistema `CONSOLE` aqui, mas o subsistema `WINDOWS` funcionaria também. Em vez de passar `-C link-arg` múltiplas vezes, usamos `-C link-args` que leva uma lista de argumentos separados por espaço.
Com este comando, nosso executável deve compilar com sucesso no Windows.
#### macOS
No macOS, o seguinte erro de linker ocorre (encurtado):
```
error: linking with `cc` failed: exit code: 1
|
= note: "cc" […]
= note: ld: entry point (_main) undefined. for architecture x86_64
clang: error: linker command failed with exit code 1 […]
```
Esta mensagem de erro nos diz que o linker não consegue encontrar uma função de ponto de entrada com o nome padrão `main` (por alguma razão, todas as funções são prefixadas com um `_` no macOS). Para definir o ponto de entrada para nossa função `_start`, passamos o argumento de linker `-e`:
```
cargo rustc -- -C link-args="-e __start"
```
A flag `-e` especifica o nome da função de ponto de entrada. Como todas as funções têm um `_` adicional prefixado no macOS, precisamos definir o ponto de entrada para `__start` em vez de `_start`.
Agora o seguinte erro de linker ocorre:
```
error: linking with `cc` failed: exit code: 1
|
= note: "cc" […]
= note: ld: dynamic main executables must link with libSystem.dylib
for architecture x86_64
clang: error: linker command failed with exit code 1 […]
```
macOS [não oferece suporte oficial a binários vinculados estaticamente] e requer que programas vinculem a biblioteca `libSystem` por padrão. Para sobrescrever isto e vincular um binário estático, passamos a flag `-static` ao linker:
[não oferece suporte oficial a binários vinculados estaticamente]: https://developer.apple.com/library/archive/qa/qa1118/_index.html
```
cargo rustc -- -C link-args="-e __start -static"
```
Isso ainda não é suficiente, pois um terceiro erro de linker ocorre:
```
error: linking with `cc` failed: exit code: 1
|
= note: "cc" […]
= note: ld: library not found for -lcrt0.o
clang: error: linker command failed with exit code 1 […]
```
Este erro ocorre porque programas no macOS vinculam a `crt0` ("C runtime zero") por padrão. Isto é similar ao erro que tivemos no Linux e também pode ser resolvido adicionando o argumento de linker `-nostartfiles`:
```
cargo rustc -- -C link-args="-e __start -static -nostartfiles"
```
Agora nosso programa deve compilar com sucesso no macOS.
#### Unificando os Comandos de Compilação
Agora temos diferentes comandos de compilação dependendo da plataforma host, o que não é o ideal. Para evitar isto, podemos criar um arquivo nomeado `.cargo/config.toml` que contém os argumentos específicos de plataforma:
```toml
# in .cargo/config.toml
[target.'cfg(target_os = "linux")']
rustflags = ["-C", "link-arg=-nostartfiles"]
[target.'cfg(target_os = "windows")']
rustflags = ["-C", "link-args=/ENTRY:_start /SUBSYSTEM:console"]
[target.'cfg(target_os = "macos")']
rustflags = ["-C", "link-args=-e __start -static -nostartfiles"]
```
A key `rustflags` contém argumentos que são automaticamente adicionados a cada invocação de `rustc`. Para mais informações sobre o arquivo `.cargo/config.toml`, veja a [documentação oficial](https://doc.rust-lang.org/cargo/reference/config.html).
Agora nosso programa deve ser compilável em todas as três plataformas com um simples `cargo build`.
#### Você Deveria Fazer Isto?
Enquanto é possível construir um executável independente para Linux, Windows e macOS, provavelmente não é uma boa ideia. A razão é que nosso executável ainda espera por várias coisas, por exemplo que uma pilha seja inicializada quando a função `_start` é chamada. Sem o runtime C, alguns desses requisitos podem não ser atendidos, o que pode causar nosso programa falhar, por exemplo através de um segmentation fault.
Se você quiser criar um binário mínimo que execute em cima de um sistema operacional existente, incluindo `libc` e definindo o atributo `#[start]` conforme descrito [aqui](https://doc.rust-lang.org/1.16.0/book/no-stdlib.html) é provavelmente uma melhor ideia.
</details>
## Resumo
Um binário Rust independente mínimo se parece com isto:
`src/main.rs`:
```rust
#![no_std] // Não vincule a biblioteca padrão do Rust
#![no_main] // desativar todos os pontos de entrada no nível Rust
use core::panic::PanicInfo;
#[unsafe(no_mangle)] // não altere (mangle) o nome desta função
pub extern "C" fn _start() -> ! {
// essa função é o ponto de entrada, já que o vinculador procura uma função
// denominado `_start` por padrão
loop {}
}
/// Esta função é chamada em caso de pânico.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
```
`Cargo.toml`:
```toml
[package]
name = "crate_name"
version = "0.1.0"
authors = ["Author Name <author@example.com>"]
# o perfil usado para `cargo build`
[profile.dev]
panic = "abort" # desativar o unwinding do stack em caso de pânico
# o perfil usado para `cargo build --release`
[profile.release]
panic = "abort" # desativar o unwinding do stack em caso de pânico
```
Para construir este binário, precisamos compilar para um alvo bare metal como `thumbv7em-none-eabihf`:
```
cargo build --target thumbv7em-none-eabihf
```
Alternativamente, podemos compilá-lo para o sistema host passando argumentos adicionais de linker:
```bash
# Linux
cargo rustc -- -C link-arg=-nostartfiles
# Windows
cargo rustc -- -C link-args="/ENTRY:_start /SUBSYSTEM:console"
# macOS
cargo rustc -- -C link-args="-e __start -static -nostartfiles"
```
Note que isto é apenas um exemplo mínimo de um binário Rust independente. Este binário espera por várias coisas, por exemplo, que um stack seja inicializado quando a função `_start` é chamada. **Portanto para qualquer uso real de tal binário, mais passos são necessários**.
## Deixando `rust-analyzer` Feliz
O projeto [`rust-analyzer`](https://rust-analyzer.github.io/) é uma ótima forma de obter autocompletar e suporte "ir para definição" (e muitos outros recursos) para código Rust no seu editor.
Funciona muito bem para projetos `#![no_std]` também, então recomendo usá-lo para desenvolvimento de kernel!
Se você estiver usando a funcionalidade [`checkOnSave`](https://rust-analyzer.github.io/book/configuration.html#checkOnSave) de `rust-analyzer` (habilitada por padrão), ela pode relatar um erro para a função panic do nosso kernel:
```
found duplicate lang item `panic_impl`
```
A razão para este erro é que `rust-analyzer` invoca `cargo check --all-targets` por padrão, que também tenta construir o binário em modo [teste](https://doc.rust-lang.org/book/ch11-01-writing-tests.html) e [benchmark](https://doc.rust-lang.org/rustc/tests/index.html#benchmarks).
<div class="note">
### Os dois significados de "target"
A flag `--all-targets` é completamente não relacionada ao argumento `--target`.
Há dois significados diferentes do termo "target" no `cargo`:
- A flag `--target` especifica o [_alvo de compilação_] que deve ser passado ao compilador `rustc`. Isso deve ser definido como o [target triple] da máquina que deve executar nosso código.
- A flag `--all-targets` referencia o [_alvo do package] do Cargo. Pacotes Cargo podem ser uma biblioteca e binário ao mesmo tempo, então você pode especificar de qual forma você gostaria de construir sua crate. Além disso, Cargo também tem alvos de package para [exemplos](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#examples), [testes](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#tests), e [benchmarks](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#benchmarks). Esses alvos de pacote podem coexistir, então você pode construir/verificar a mesma crate por exemplo em modo biblioteca ou modo teste.
[_alvo de compilação_]: https://doc.rust-lang.org/rustc/targets/index.html
[target triple]: https://clang.llvm.org/docs/CrossCompilation.html#target-triple
[_alvo do package]: https://doc.rust-lang.org/cargo/reference/cargo-targets.html
</div>
Por padrão, `cargo check` apenas constrói o _biblioteca_ e os alvos de pacote _binário_.
No entanto, `rust-analyzer` escolhe verificar todos os alvos de pacote por padrão quando [`checkOnSave`](https://rust-analyzer.github.io/book/configuration.html#checkOnSave) é habilitado.
Esta é a razão pela qual `rust-analyzer` relata o erro de `lang item` acima que não vemos em `cargo check`.
Se executarmos `cargo check --all-targets`, vemos o erro também:
```
error[E0152]: found duplicate lang item `panic_impl`
--> src/main.rs:13:1
|
13 | / fn panic(_info: &PanicInfo) -> ! {
14 | | loop {}
15 | | }
| |_^
|
= note: the lang item is first defined in crate `std` (which `test` depends on)
= note: first definition in `std` loaded from /home/[...]/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libstd-8df6be531efb3fd0.rlib
= note: second definition in the local crate (`blog_os`)
```
A primeira `note` nos diz que o item de linguagem panic já está definido na crate `std`, que é uma dependência da crate `test`.
A crate `test` é automaticamente incluída ao construir uma crate em [modo teste](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#tests).
Isso não faz sentido para nosso kernel `#![no_std]` já que não há forma de suportar a biblioteca padrão em bare metal.
Então este erro não é relevante para nosso projeto e podemos seguramente ignorá-lo.
A forma apropriada de evitar este erro é especificar em nosso `Cargo.toml` que nosso binário não suporta construção em modos `test` e `bench`.
Podemos fazer isso adicionando uma seção `[[bin]]` em nosso `Cargo.toml` para [configurar a construção](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#configuring-a-target) do nosso binário:
```toml
# no Cargo.toml
[[bin]]
name = "blog_os"
test = false
bench = false
```
Os colchetes duplos ao redor de `bin` não é um erro, isto é como o formato TOML define chaves que podem aparecer múltiplas vezes.
Como uma crate pode ter múltiplos binários, a seção `[[bin]]` pode aparecer múltiplas vezes em `Cargo.toml` também.
Esta é também a razão para o campo `name` obrigatório, que precisa corresponder ao nome do binário (para que `cargo` saiba quais configurações devem ser aplicadas a qual binário).
Ao definir os campos [`test`](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-test-field) e [`bench` ](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-bench-field) para `false`, instruímos `cargo` a não construir nosso binário em modo teste ou benchmark.
Agora `cargo check --all-targets` não deve lançar mais erros, e a implementação de `checkOnSave` de `rust-analyzer` também deve estar feliz.
## O que vem a seguir?
O [próximo post] explica os passos necessários para transformar nosso binário independente em um kernel mínimo do sistema operacional. Isso inclui criar um alvo customizado, combinar nosso executável com um bootloader, e aprender como imprimir algo na tela.
[próximo post]: @/edition-2/posts/02-minimal-rust-kernel/index.md

View File

@@ -0,0 +1,35 @@
+++
title = "Desabilitando a Red Zone"
weight = 1
path = "pt-BR/red-zone"
template = "edition-2/extra.html"
[extra]
# Please update this when updating the translation
translation_based_on_commit = "9d079e6d3e03359469d6cf1759bb1a196d8a11ac"
# GitHub usernames of the people that translated this post
translators = ["richarddalves"]
+++
A [red zone] é uma otimização da [System V ABI] que permite que funções usem temporariamente os 128 bytes abaixo do seu stack frame sem ajustar o ponteiro de pilha:
[red zone]: https://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64#the-red-zone
[System V ABI]: https://wiki.osdev.org/System_V_ABI
<!-- more -->
![stack frame com red zone](red-zone.svg)
A imagem mostra o stack frame de uma função com `n` variáveis locais. Na entrada da função, o ponteiro de pilha é ajustado para abrir espaço na pilha para o endereço de retorno e as variáveis locais.
A red zone é definida como os 128 bytes abaixo do ponteiro de pilha ajustado. A função pode usar esta área para dados temporários que não são necessários entre chamadas de função. Assim, as duas instruções para ajustar o ponteiro de pilha podem ser evitadas em alguns casos (por exemplo, em pequenas funções folha).
No entanto, esta otimização leva a problemas enormes com exceções ou interrupções de hardware. Vamos assumir que uma exceção ocorre enquanto uma função usa a red zone:
![red zone sobrescrita pelo handler de exceção](red-zone-overwrite.svg)
A CPU e o handler de exceção sobrescrevem os dados na red zone. Mas estes dados ainda são necessários pela função interrompida. Então a função não funcionará mais corretamente quando retornarmos do handler de exceção. Isso pode levar a bugs estranhos que [levam semanas para depurar].
[levam semanas para depurar]: https://forum.osdev.org/viewtopic.php?t=21720
Para evitar tais bugs quando implementarmos tratamento de exceções no futuro, desabilitamos a red zone logo de início. Isso é alcançado adicionando a linha `"disable-redzone": true` ao nosso arquivo de configuração de alvo.

View File

@@ -0,0 +1,50 @@
+++
title = "Desabilitando SIMD"
weight = 2
path = "pt-BR/disable-simd"
template = "edition-2/extra.html"
[extra]
# Please update this when updating the translation
translation_based_on_commit = "9d079e6d3e03359469d6cf1759bb1a196d8a11ac"
# GitHub usernames of the people that translated this post
translators = ["richarddalves"]
+++
Instruções [Single Instruction Multiple Data (SIMD)] são capazes de realizar uma operação (por exemplo, adição) simultaneamente em múltiplas palavras de dados, o que pode acelerar programas significativamente. A arquitetura `x86_64` suporta vários padrões SIMD:
[Single Instruction Multiple Data (SIMD)]: https://en.wikipedia.org/wiki/SIMD
<!-- more -->
- [MMX]: O conjunto de instruções _Multi Media Extension_ foi introduzido em 1997 e define oito registradores de 64 bits chamados `mm0` até `mm7`. Esses registradores são apenas aliases para os registradores da [unidade de ponto flutuante x87].
- [SSE]: O conjunto de instruções _Streaming SIMD Extensions_ foi introduzido em 1999. Em vez de reutilizar os registradores de ponto flutuante, ele adiciona um conjunto de registradores completamente novo. Os dezesseis novos registradores são chamados `xmm0` até `xmm15` e têm 128 bits cada.
- [AVX]: As _Advanced Vector Extensions_ são extensões que aumentam ainda mais o tamanho dos registradores multimídia. Os novos registradores são chamados `ymm0` até `ymm15` e têm 256 bits cada. Eles estendem os registradores `xmm`, então por exemplo `xmm0` é a metade inferior de `ymm0`.
[MMX]: https://en.wikipedia.org/wiki/MMX_(instruction_set)
[unidade de ponto flutuante x87]: https://en.wikipedia.org/wiki/X87
[SSE]: https://en.wikipedia.org/wiki/Streaming_SIMD_Extensions
[AVX]: https://en.wikipedia.org/wiki/Advanced_Vector_Extensions
Ao usar tais padrões SIMD, programas frequentemente podem acelerar significativamente. Bons compiladores são capazes de transformar loops normais em tal código SIMD automaticamente através de um processo chamado [auto-vetorização].
[auto-vetorização]: https://en.wikipedia.org/wiki/Automatic_vectorization
No entanto, os grandes registradores SIMD levam a problemas em kernels de SO. A razão é que o kernel tem que fazer backup de todos os registradores que usa para a memória em cada interrupção de hardware, porque eles precisam ter seus valores originais quando o programa interrompido continua. Então, se o kernel usa registradores SIMD, ele tem que fazer backup de muito mais dados (512-1600 bytes), o que diminui notavelmente o desempenho. Para evitar esta perda de desempenho, queremos desabilitar os recursos `sse` e `mmx` (o recurso `avx` é desabilitado por padrão).
Podemos fazer isso através do campo `features` na nossa especificação de alvo. Para desabilitar os recursos `mmx` e `sse`, nós os adicionamos prefixados com um menos:
```json
"features": "-mmx,-sse"
```
## Ponto Flutuante
Infelizmente para nós, a arquitetura `x86_64` usa registradores SSE para operações de ponto flutuante. Assim, todo uso de ponto flutuante com SSE desabilitado causa um erro no LLVM. O problema é que a biblioteca core do Rust já usa floats (por exemplo, ela implementa traits para `f32` e `f64`), então evitar floats no nosso kernel não é suficiente.
Felizmente, o LLVM tem suporte para um recurso `soft-float` que emula todas as operações de ponto flutuante através de funções de software baseadas em inteiros normais. Isso torna possível usar floats no nosso kernel sem SSE; será apenas um pouco mais lento.
Para ativar o recurso `soft-float` para o nosso kernel, nós o adicionamos à linha `features` na nossa especificação de alvo, prefixado com um mais:
```json
"features": "-mmx,-sse,+soft-float"
```

View File

@@ -0,0 +1,504 @@
+++
title = "Um Kernel Rust Mínimo"
weight = 2
path = "pt-BR/minimal-rust-kernel"
date = 2018-02-10
[extra]
chapter = "O Básico"
# Please update this when updating the translation
translation_based_on_commit = "95d4fbd54c6b0e5a874981558c0cc1fe85d31606"
# GitHub usernames of the people that translated this post
translators = ["richarddalves"]
+++
Neste post, criamos um kernel Rust mínimo de 64 bits para a arquitetura x86. Construímos sobre o [binário Rust independente] do post anterior para criar uma imagem de disco inicializável que imprime algo na tela.
[binário Rust independente]: @/edition-2/posts/01-freestanding-rust-binary/index.pt-BR.md
<!-- more -->
Este blog é desenvolvido abertamente no [GitHub]. Se você tiver algum problema ou dúvida, abra um issue lá. Você também pode deixar comentários [na parte inferior]. O código-fonte completo desta publicação pode ser encontrado na branch [`post-02`][post branch].
[GitHub]: https://github.com/phil-opp/blog_os
[na parte inferior]: #comments
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
[post branch]: https://github.com/phil-opp/blog_os/tree/post-02
<!-- toc -->
## O Processo de Boot
Quando você liga um computador, ele começa a executar código de firmware que está armazenado na [ROM] da placa-mãe. Este código executa um [teste automático de inicialização], detecta a RAM disponível e pré-inicializa a CPU e o hardware. Depois, ele procura por um disco inicializável e começa a inicializar o kernel do sistema operacional.
[ROM]: https://en.wikipedia.org/wiki/Read-only_memory
[teste automático de inicialização]: https://en.wikipedia.org/wiki/Power-on_self-test
No x86, existem dois padrões de firmware: o "Basic Input/Output System" (**[BIOS]**) e o mais novo "Unified Extensible Firmware Interface" (**[UEFI]**). O padrão BIOS é antigo e ultrapassado, mas simples e bem suportado em qualquer máquina x86 desde os anos 1980. UEFI, em contraste, é mais moderno e tem muito mais recursos, mas é mais complexo de configurar (na minha opinião, pelo menos).
[BIOS]: https://en.wikipedia.org/wiki/BIOS
[UEFI]: https://en.wikipedia.org/wiki/Unified_Extensible_Firmware_Interface
Atualmente, fornecemos apenas suporte para BIOS, mas suporte para UEFI também está planejado. Se você gostaria de nos ajudar com isso, confira o [issue no Github](https://github.com/phil-opp/blog_os/issues/349).
### Boot BIOS
Quase todos os sistemas x86 têm suporte para boot BIOS, incluindo máquinas mais novas baseadas em UEFI que usam um BIOS emulado. Isso é ótimo, porque você pode usar a mesma lógica de boot em todas as máquinas do último século. Mas essa ampla compatibilidade é ao mesmo tempo a maior desvantagem do boot BIOS, porque significa que a CPU é colocada em um modo de compatibilidade de 16 bits chamado [modo real] antes do boot, para que bootloaders arcaicos dos anos 1980 ainda funcionem.
Mas vamos começar do início:
Quando você liga um computador, ele carrega o BIOS de uma memória flash especial localizada na placa-mãe. O BIOS executa rotinas de teste automático e inicialização do hardware, então procura por discos inicializáveis. Se ele encontra um, o controle é transferido para seu _bootloader_, que é uma porção de 512 bytes de código executável armazenado no início do disco. A maioria dos bootloaders é maior que 512 bytes, então os bootloaders são comumente divididos em um primeiro estágio pequeno, que cabe em 512 bytes, e um segundo estágio, que é subsequentemente carregado pelo primeiro estágio.
O bootloader tem que determinar a localização da imagem do kernel no disco e carregá-la na memória. Ele também precisa mudar a CPU do [modo real] de 16 bits primeiro para o [modo protegido] de 32 bits, e então para o [modo longo] de 64 bits, onde registradores de 64 bits e a memória principal completa estão disponíveis. Seu terceiro trabalho é consultar certas informações (como um mapa de memória) do BIOS e passá-las ao kernel do SO.
[modo real]: https://en.wikipedia.org/wiki/Real_mode
[modo protegido]: https://en.wikipedia.org/wiki/Protected_mode
[modo longo]: https://en.wikipedia.org/wiki/Long_mode
[segmentação de memória]: https://en.wikipedia.org/wiki/X86_memory_segmentation
Escrever um bootloader é um pouco trabalhoso, pois requer linguagem assembly e muitos passos pouco intuitivos como "escrever este valor mágico neste registrador do processador". Portanto, não cobrimos a criação de bootloader neste post e em vez disso fornecemos uma ferramenta chamada [bootimage] que anexa automaticamente um bootloader ao seu kernel.
[bootimage]: https://github.com/rust-osdev/bootimage
Se você estiver interessado em construir seu próprio bootloader: Fique ligado, um conjunto de posts sobre este tópico já está planejado! <!-- , confira nossos posts "_[Writing a Bootloader]_", onde explicamos em detalhes como um bootloader é construído. -->
#### O Padrão Multiboot
Para evitar que todo sistema operacional implemente seu próprio bootloader, que é compatível apenas com um único SO, a [Free Software Foundation] criou um padrão de bootloader aberto chamado [Multiboot] em 1995. O padrão define uma interface entre o bootloader e o sistema operacional, para que qualquer bootloader compatível com Multiboot possa carregar qualquer sistema operacional compatível com Multiboot. A implementação de referência é o [GNU GRUB], que é o bootloader mais popular para sistemas Linux.
[Free Software Foundation]: https://en.wikipedia.org/wiki/Free_Software_Foundation
[Multiboot]: https://wiki.osdev.org/Multiboot
[GNU GRUB]: https://en.wikipedia.org/wiki/GNU_GRUB
Para tornar um kernel compatível com Multiboot, basta inserir um chamado [cabeçalho Multiboot] no início do arquivo do kernel. Isso torna muito fácil inicializar um SO a partir do GRUB. No entanto, o GRUB e o padrão Multiboot também têm alguns problemas:
[cabeçalho Multiboot]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#OS-image-format
- Eles suportam apenas o modo protegido de 32 bits. Isso significa que você ainda tem que fazer a configuração da CPU para mudar para o modo longo de 64 bits.
- Eles são projetados para tornar o bootloader simples em vez do kernel. Por exemplo, o kernel precisa ser vinculado com um [tamanho de página padrão ajustado], porque o GRUB não consegue encontrar o cabeçalho Multiboot caso contrário. Outro exemplo é que as [informações de boot], que são passadas ao kernel, contêm muitas estruturas dependentes de arquitetura em vez de fornecer abstrações limpas.
- Tanto o GRUB quanto o padrão Multiboot são documentados apenas esparsamente.
- O GRUB precisa estar instalado no sistema host para criar uma imagem de disco inicializável a partir do arquivo do kernel. Isso torna o desenvolvimento no Windows ou Mac mais difícil.
[tamanho de página padrão ajustado]: https://wiki.osdev.org/Multiboot#Multiboot_2
[informações de boot]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#Boot-information-format
Por causa dessas desvantagens, decidimos não usar o GRUB ou o padrão Multiboot. No entanto, planejamos adicionar suporte Multiboot à nossa ferramenta [bootimage], para que seja possível carregar seu kernel em um sistema GRUB também. Se você estiver interessado em escrever um kernel compatível com Multiboot, confira a [primeira edição] desta série de blog.
[primeira edição]: @/edition-1/_index.md
### UEFI
(Não fornecemos suporte UEFI no momento, mas adoraríamos! Se você gostaria de ajudar, por favor nos diga no [issue do Github](https://github.com/phil-opp/blog_os/issues/349).)
## Um Kernel Mínimo
Agora que sabemos aproximadamente como um computador inicializa, é hora de criar nosso próprio kernel mínimo. Nosso objetivo é criar uma imagem de disco que imprima um "Hello World!" na tela quando inicializada. Fazemos isso estendendo o [binário Rust independente] do post anterior.
Como você deve se lembrar, construímos o binário independente através do `cargo`, mas dependendo do sistema operacional, precisávamos de nomes de ponto de entrada e flags de compilação diferentes. Isso ocorre porque o `cargo` compila para o _sistema host_ por padrão, ou seja, o sistema em que você está executando. Isso não é algo que queremos para nosso kernel, porque um kernel que executa em cima de, por exemplo, Windows, não faz muito sentido. Em vez disso, queremos compilar para um _sistema alvo_ claramente definido.
### Instalando o Rust Nightly
O Rust tem três canais de lançamento: _stable_, _beta_ e _nightly_. O Livro do Rust explica a diferença entre esses canais muito bem, então dê uma olhada [aqui](https://doc.rust-lang.org/book/appendix-07-nightly-rust.html#choo-choo-release-channels-and-riding-the-trains). Para construir um sistema operacional, precisaremos de alguns recursos experimentais que estão disponíveis apenas no canal nightly, então precisamos instalar uma versão nightly do Rust.
Para gerenciar instalações do Rust, eu recomendo fortemente o [rustup]. Ele permite instalar compiladores nightly, beta e stable lado a lado e facilita a atualização deles. Com rustup, você pode usar um compilador nightly para o diretório atual executando `rustup override set nightly`. Alternativamente, você pode adicionar um arquivo chamado `rust-toolchain` com o conteúdo `nightly` ao diretório raiz do projeto. Você pode verificar que tem uma versão nightly instalada executando `rustc --version`: O número da versão deve conter `-nightly` no final.
[rustup]: https://www.rustup.rs/
O compilador nightly nos permite optar por vários recursos experimentais usando as chamadas _feature flags_ no topo do nosso arquivo. Por exemplo, poderíamos habilitar a [macro `asm!`] experimental para assembly inline adicionando `#![feature(asm)]` no topo do nosso `main.rs`. Note que tais recursos experimentais são completamente instáveis, o que significa que versões futuras do Rust podem alterá-los ou removê-los sem aviso prévio. Por esta razão, só os usaremos se absolutamente necessário.
[macro `asm!`]: https://doc.rust-lang.org/stable/reference/inline-assembly.html
### Especificação de Alvo
O Cargo suporta diferentes sistemas alvo através do parâmetro `--target`. O alvo é descrito por uma chamada _[target triple]_, que descreve a arquitetura da CPU, o vendor, o sistema operacional e a [ABI]. Por exemplo, o target triple `x86_64-unknown-linux-gnu` descreve um sistema com uma CPU `x86_64`, sem vendor claro, e um sistema operacional Linux com a ABI GNU. O Rust suporta [muitos target triples diferentes][platform-support], incluindo `arm-linux-androideabi` para Android ou [`wasm32-unknown-unknown` para WebAssembly](https://www.hellorust.com/setup/wasm-target/).
[target triple]: https://clang.llvm.org/docs/CrossCompilation.html#target-triple
[ABI]: https://stackoverflow.com/a/2456882
[platform-support]: https://forge.rust-lang.org/release/platform-support.html
[custom-targets]: https://doc.rust-lang.org/nightly/rustc/targets/custom.html
Para nosso sistema alvo, no entanto, precisamos de alguns parâmetros de configuração especiais (por exemplo, nenhum SO subjacente), então nenhum dos [target triples existentes][platform-support] se encaixa. Felizmente, o Rust nos permite definir [nosso próprio alvo][custom-targets] através de um arquivo JSON. Por exemplo, um arquivo JSON que descreve o target `x86_64-unknown-linux-gnu` se parece com isto:
```json
{
"llvm-target": "x86_64-unknown-linux-gnu",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": 64,
"target-c-int-width": 32,
"os": "linux",
"executables": true,
"linker-flavor": "gcc",
"pre-link-args": ["-m64"],
"morestack": false
}
```
A maioria dos campos é exigida pelo LLVM para gerar código para aquela plataforma. Por exemplo, o campo [`data-layout`] define o tamanho de vários tipos integer, floating point e pointer. Então há campos que o Rust usa para compilação condicional, como `target-pointer-width`. O terceiro tipo de campo define como a crate deve ser construída. Por exemplo, o campo `pre-link-args` especifica argumentos passados ao [linker].
[`data-layout`]: https://llvm.org/docs/LangRef.html#data-layout
[linker]: https://en.wikipedia.org/wiki/Linker_(computing)
Também visamos sistemas `x86_64` com nosso kernel, então nossa especificação de alvo será muito similar à acima. Vamos começar criando um arquivo `x86_64-blog_os.json` (escolha qualquer nome que você goste) com o conteúdo comum:
```json
{
"llvm-target": "x86_64-unknown-none",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": 64,
"target-c-int-width": 32,
"os": "none",
"executables": true
}
```
Note que mudamos o SO no `llvm-target` e no campo `os` para `none`, porque executaremos em bare metal.
Adicionamos as seguintes entradas relacionadas à compilação:
```json
"linker-flavor": "ld.lld",
"linker": "rust-lld",
```
Em vez de usar o linker padrão da plataforma (que pode não suportar alvos Linux), usamos o linker multiplataforma [LLD] que vem com o Rust para vincular nosso kernel.
[LLD]: https://lld.llvm.org/
```json
"panic-strategy": "abort",
```
Esta configuração especifica que o alvo não suporta [stack unwinding] no panic, então em vez disso o programa deve abortar diretamente. Isso tem o mesmo efeito que a opção `panic = "abort"` no nosso Cargo.toml, então podemos removê-la de lá. (Note que, em contraste com a opção Cargo.toml, esta opção de alvo também se aplica quando recompilamos a biblioteca `core` mais adiante neste post. Então, mesmo se você preferir manter a opção Cargo.toml, certifique-se de incluir esta opção.)
[stack unwinding]: https://www.bogotobogo.com/cplusplus/stackunwinding.php
```json
"disable-redzone": true,
```
Estamos escrevendo um kernel, então precisaremos lidar com interrupções em algum momento. Para fazer isso com segurança, temos que desabilitar uma certa otimização do ponteiro de stack chamada _"red zone"_, porque ela causaria corrupção do stack caso contrário. Para mais informações, veja nosso post separado sobre [desabilitando a red zone].
[desabilitando a red zone]: @/edition-2/posts/02-minimal-rust-kernel/disable-red-zone/index.md
```json
"features": "-mmx,-sse,+soft-float",
```
O campo `features` habilita/desabilita recursos do alvo. Desabilitamos os recursos `mmx` e `sse` prefixando-os com um menos e habilitamos o recurso `soft-float` prefixando-o com um mais. Note que não deve haver espaços entre flags diferentes, caso contrário o LLVM falha ao interpretar a string de features.
Os recursos `mmx` e `sse` determinam suporte para instruções [Single Instruction Multiple Data (SIMD)], que frequentemente podem acelerar programas significativamente. No entanto, usar os grandes registradores SIMD em kernels de SO leva a problemas de desempenho. A razão é que o kernel precisa restaurar todos os registradores ao seu estado original antes de continuar um programa interrompido. Isso significa que o kernel tem que salvar o estado SIMD completo na memória principal em cada chamada de sistema ou interrupção de hardware. Como o estado SIMD é muito grande (512-1600 bytes) e interrupções podem ocorrer com muita frequência, essas operações adicionais de salvar/restaurar prejudicam consideravelmente o desempenho. Para evitar isso, desabilitamos SIMD para nosso kernel (não para aplicações executando em cima!).
[Single Instruction Multiple Data (SIMD)]: https://en.wikipedia.org/wiki/SIMD
Um problema com desabilitar SIMD é que operações de ponto flutuante em `x86_64` exigem registradores SIMD por padrão. Para resolver este problema, adicionamos o recurso `soft-float`, que emula todas as operações de ponto flutuante através de funções de software baseadas em inteiros normais.
Para mais informações, veja nosso post sobre [desabilitando SIMD](@/edition-2/posts/02-minimal-rust-kernel/disable-simd/index.md).
```json
"rustc-abi": "x86-softfloat"
```
Como queremos usar o recurso `soft-float`, também precisamos dizer ao compilador Rust `rustc` que queremos usar a ABI correspondente. Podemos fazer isso definindo o campo `rustc-abi` para `x86-softfloat`.
#### Juntando Tudo
Nosso arquivo de especificação de alvo agora se parece com isto:
```json
{
"llvm-target": "x86_64-unknown-none",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": 64,
"target-c-int-width": 32,
"os": "none",
"executables": true,
"linker-flavor": "ld.lld",
"linker": "rust-lld",
"panic-strategy": "abort",
"disable-redzone": true,
"features": "-mmx,-sse,+soft-float",
"rustc-abi": "x86-softfloat"
}
```
### Construindo nosso Kernel
Compilar para nosso novo alvo usará convenções Linux, já que o linker-flavor ld.lld instrui o llvm a compilar com a flag `-flavor gnu` (para mais opções de linker, veja [a documentação do rustc](https://doc.rust-lang.org/rustc/codegen-options/index.html#linker-flavor)). Isso significa que precisamos de um ponto de entrada chamado `_start` como descrito no [post anterior]:
[post anterior]: @/edition-2/posts/01-freestanding-rust-binary/index.pt-BR.md
```rust
// src/main.rs
#![no_std] // não vincule a biblioteca padrão do Rust
#![no_main] // desativar todos os pontos de entrada no nível Rust
use core::panic::PanicInfo;
/// Esta função é chamada em caso de pânico.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
#[unsafe(no_mangle)] // não altere (mangle) o nome desta função
pub extern "C" fn _start() -> ! {
// essa função é o ponto de entrada, já que o vinculador procura uma função
// denominado `_start` por padrão
loop {}
}
```
Note que o ponto de entrada precisa ser chamado `_start` independentemente do seu SO host.
Agora podemos construir o kernel para nosso novo alvo passando o nome do arquivo JSON como `--target`:
```
> cargo build --target x86_64-blog_os.json
error[E0463]: can't find crate for `core`
```
Falha! O erro nos diz que o compilador Rust não consegue mais encontrar a [biblioteca `core`]. Esta biblioteca contém tipos básicos do Rust como `Result`, `Option` e iteradores, e é implicitamente vinculada a todas as crates `no_std`.
[biblioteca `core`]: https://doc.rust-lang.org/nightly/core/index.html
O problema é que a biblioteca core é distribuída junto com o compilador Rust como uma biblioteca _pré-compilada_. Então ela é válida apenas para target triples host suportados (por exemplo, `x86_64-unknown-linux-gnu`) mas não para nosso alvo customizado. Se quisermos compilar código para outros alvos, precisamos recompilar `core` para esses alvos primeiro.
#### A Opção `build-std`
É aí que entra o [recurso `build-std`] do cargo. Ele permite recompilar `core` e outras crates da biblioteca padrão sob demanda, em vez de usar as versões pré-compiladas enviadas com a instalação do Rust. Este recurso é muito novo e ainda não está finalizado, então é marcado como "unstable" e disponível apenas em [compiladores Rust nightly].
[recurso `build-std`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std
[compiladores Rust nightly]: #instalando-o-rust-nightly
Para usar o recurso, precisamos criar um arquivo de [configuração cargo] local em `.cargo/config.toml` (a pasta `.cargo` deve estar ao lado da sua pasta `src`) com o seguinte conteúdo:
```toml
# em .cargo/config.toml
[unstable]
build-std = ["core", "compiler_builtins"]
```
Isso diz ao cargo que ele deve recompilar as bibliotecas `core` e `compiler_builtins`. Esta última é necessária porque é uma dependência de `core`. Para recompilar essas bibliotecas, o cargo precisa de acesso ao código-fonte do rust, que podemos instalar com `rustup component add rust-src`.
<div class="note">
**Nota:** A chave de configuração `unstable.build-std` requer pelo menos o Rust nightly de 15-07-2020.
</div>
Depois de definir a chave de configuração `unstable.build-std` e instalar o componente `rust-src`, podemos executar novamente nosso comando de compilação:
```
> cargo build --target x86_64-blog_os.json
Compiling core v0.0.0 (/…/rust/src/libcore)
Compiling rustc-std-workspace-core v1.99.0 (/…/rust/src/tools/rustc-std-workspace-core)
Compiling compiler_builtins v0.1.32
Compiling blog_os v0.1.0 (/…/blog_os)
Finished dev [unoptimized + debuginfo] target(s) in 0.29 secs
```
Vemos que `cargo build` agora recompila as bibliotecas `core`, `rustc-std-workspace-core` (uma dependência de `compiler_builtins`) e `compiler_builtins` para nosso alvo customizado.
#### Intrínsecos Relacionados a Memória
O compilador Rust assume que um certo conjunto de funções embutidas está disponível para todos os sistemas. A maioria dessas funções é fornecida pela crate `compiler_builtins` que acabamos de recompilar. No entanto, existem algumas funções relacionadas a memória nessa crate que não são habilitadas por padrão porque normalmente são fornecidas pela biblioteca C no sistema. Essas funções incluem `memset`, que define todos os bytes em um bloco de memória para um valor dado, `memcpy`, que copia um bloco de memória para outro, e `memcmp`, que compara dois blocos de memória. Embora não precisássemos de nenhuma dessas funções para compilar nosso kernel agora, elas serão necessárias assim que adicionarmos mais código a ele (por exemplo, ao copiar structs).
Como não podemos vincular à biblioteca C do sistema operacional, precisamos de uma maneira alternativa de fornecer essas funções ao compilador. Uma possível abordagem para isso poderia ser implementar nossas próprias funções `memset` etc. e aplicar o atributo `#[unsafe(no_mangle)]` a elas (para evitar a renomeação automática durante a compilação). No entanto, isso é perigoso, pois o menor erro na implementação dessas funções pode levar a undefined behavior. Por exemplo, implementar `memcpy` com um loop `for` pode resultar em recursão infinita porque loops `for` implicitamente chamam o método da trait [`IntoIterator::into_iter`], que pode chamar `memcpy` novamente. Então é uma boa ideia reutilizar implementações existentes e bem testadas em vez disso.
[`IntoIterator::into_iter`]: https://doc.rust-lang.org/stable/core/iter/trait.IntoIterator.html#tymethod.into_iter
Felizmente, a crate `compiler_builtins` já contém implementações para todas as funções necessárias, elas estão apenas desabilitadas por padrão para não colidir com as implementações da biblioteca C. Podemos habilitá-las definindo a flag [`build-std-features`] do cargo para `["compiler-builtins-mem"]`. Como a flag `build-std`, esta flag pode ser passada na linha de comando como uma flag `-Z` ou configurada na tabela `unstable` no arquivo `.cargo/config.toml`. Como queremos sempre compilar com esta flag, a opção do arquivo de configuração faz mais sentido para nós:
[`build-std-features`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std-features
```toml
# em .cargo/config.toml
[unstable]
build-std-features = ["compiler-builtins-mem"]
build-std = ["core", "compiler_builtins"]
```
(O suporte para o recurso `compiler-builtins-mem` foi [adicionado muito recentemente](https://github.com/rust-lang/rust/pull/77284), então você precisa pelo menos do Rust nightly `2020-09-30` para ele.)
Nos bastidores, esta flag habilita o [recurso `mem`] da crate `compiler_builtins`. O efeito disso é que o atributo `#[unsafe(no_mangle)]` é aplicado às [implementações `memcpy` etc.] da crate, o que as torna disponíveis ao linker.
[recurso `mem`]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/Cargo.toml#L54-L55
[implementações `memcpy` etc.]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/src/mem.rs#L12-L69
Com esta mudança, nosso kernel tem implementações válidas para todas as funções exigidas pelo compilador, então ele continuará a compilar mesmo se nosso código ficar mais complexo.
#### Definir um Alvo Padrão
Para evitar passar o parâmetro `--target` em cada invocação de `cargo build`, podemos sobrescrever o alvo padrão. Para fazer isso, adicionamos o seguinte ao nosso arquivo de [configuração cargo] em `.cargo/config.toml`:
[configuração cargo]: https://doc.rust-lang.org/cargo/reference/config.html
```toml
# em .cargo/config.toml
[build]
target = "x86_64-blog_os.json"
```
Isso diz ao `cargo` para usar nosso alvo `x86_64-blog_os.json` quando nenhum argumento `--target` explícito é passado. Isso significa que agora podemos construir nosso kernel com um simples `cargo build`. Para mais informações sobre opções de configuração do cargo, confira a [documentação oficial][configuração cargo].
Agora podemos construir nosso kernel para um alvo bare metal com um simples `cargo build`. No entanto, nosso ponto de entrada `_start`, que será chamado pelo bootloader, ainda está vazio. É hora de mostrar algo na tela a partir dele.
### Imprimindo na Tela
A maneira mais fácil de imprimir texto na tela neste estágio é o [buffer de texto VGA]. É uma área de memória especial mapeada para o hardware VGA que contém o conteúdo exibido na tela. Normalmente consiste em 25 linhas que cada uma contém 80 células de caractere. Cada célula de caractere exibe um caractere ASCII com algumas cores de primeiro plano e fundo. A saída da tela se parece com isto:
[buffer de texto VGA]: https://en.wikipedia.org/wiki/VGA-compatible_text_mode
![saída de tela para caracteres ASCII comuns](https://upload.wikimedia.org/wikipedia/commons/f/f8/Codepage-437.png)
Discutiremos o layout exato do buffer VGA no próximo post, onde escreveremos um primeiro pequeno driver para ele. Para imprimir "Hello World!", só precisamos saber que o buffer está localizado no endereço `0xb8000` e que cada célula de caractere consiste em um byte ASCII e um byte de cor.
A implementação se parece com isto:
```rust
static HELLO: &[u8] = b"Hello World!";
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
let vga_buffer = 0xb8000 as *mut u8;
for (i, &byte) in HELLO.iter().enumerate() {
unsafe {
*vga_buffer.offset(i as isize * 2) = byte;
*vga_buffer.offset(i as isize * 2 + 1) = 0xb;
}
}
loop {}
}
```
Primeiro, convertemos o inteiro `0xb8000` em um [ponteiro bruto]. Então [iteramos] sobre os bytes da [byte string] [static] `HELLO`. Usamos o método [`enumerate`] para obter adicionalmente uma variável em execução `i`. No corpo do loop for, usamos o método [`offset`] para escrever o byte da string e o byte de cor correspondente (`0xb` é um ciano claro).
[iterar]: https://doc.rust-lang.org/stable/book/ch13-02-iterators.html
[static]: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#the-static-lifetime
[`enumerate`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate
[byte string]: https://doc.rust-lang.org/reference/tokens.html#byte-string-literals
[ponteiro bruto]: https://doc.rust-lang.org/stable/book/ch19-01-unsafe-rust.html#dereferencing-a-raw-pointer
[`offset`]: https://doc.rust-lang.org/std/primitive.pointer.html#method.offset
Note que há um bloco [`unsafe`] em torno de todas as escritas de memória. A razão é que o compilador Rust não pode provar que os ponteiros brutos que criamos são válidos. Eles poderiam apontar para qualquer lugar e levar à corrupção de dados. Ao colocá-los em um bloco `unsafe`, estamos basicamente dizendo ao compilador que temos absoluta certeza de que as operações são válidas. Note que um bloco `unsafe` não desativa as verificações de segurança do Rust. Ele apenas permite que você faça [cinco coisas adicionais].
[`unsafe`]: https://doc.rust-lang.org/stable/book/ch19-01-unsafe-rust.html
[cinco coisas adicionais]: https://doc.rust-lang.org/stable/book/ch19-01-unsafe-rust.html#unsafe-superpowers
Quero enfatizar que **esta não é a maneira como queremos fazer as coisas em Rust!** É muito fácil bagunçar ao trabalhar com ponteiros brutos dentro de blocos unsafe. Por exemplo, poderíamos facilmente escrever além do fim do buffer se não tivermos cuidado.
Então queremos minimizar o uso de `unsafe` o máximo possível. O Rust nos dá a capacidade de fazer isso criando abstrações seguras. Por exemplo, poderíamos criar um tipo de buffer VGA que encapsula toda a unsafety e garante que seja _impossível_ fazer algo errado de fora. Desta forma, precisaríamos apenas de quantidades mínimas de código `unsafe` e poderíamos ter certeza de que não violamos [memory safety]. Criaremos tal abstração de buffer VGA segura no próximo post.
[memory safety]: https://en.wikipedia.org/wiki/Memory_safety
## Executando nosso Kernel
Agora que temos um executável que faz algo perceptível, é hora de executá-lo. Primeiro, precisamos transformar nosso kernel compilado em uma imagem de disco inicializável vinculando-o com um bootloader. Então podemos executar a imagem de disco na máquina virtual [QEMU] ou inicializá-la em hardware real usando um pendrive USB.
### Criando uma Bootimage
Para transformar nosso kernel compilado em uma imagem de disco inicializável, precisamos vinculá-lo com um bootloader. Como aprendemos na [seção sobre boot], o bootloader é responsável por inicializar a CPU e carregar nosso kernel.
[seção sobre boot]: #o-processo-de-boot
Em vez de escrever nosso próprio bootloader, que é um projeto por si só, usamos a crate [`bootloader`]. Esta crate implementa um bootloader BIOS básico sem nenhuma dependência C, apenas Rust e assembly inline. Para usá-lo para inicializar nosso kernel, precisamos adicionar uma dependência nele:
[`bootloader`]: https://crates.io/crates/bootloader
```toml
# em Cargo.toml
[dependencies]
bootloader = "0.9"
```
**Nota:** Este post é compatível apenas com `bootloader v0.9`. Versões mais novas usam um sistema de compilação diferente e resultarão em erros de compilação ao seguir este post.
Adicionar o bootloader como uma dependência não é suficiente para realmente criar uma imagem de disco inicializável. O problema é que precisamos vincular nosso kernel com o bootloader após a compilação, mas o cargo não tem suporte para [scripts pós-compilação].
[scripts pós-compilação]: https://github.com/rust-lang/cargo/issues/545
Para resolver este problema, criamos uma ferramenta chamada `bootimage` que primeiro compila o kernel e o bootloader, e então os vincula juntos para criar uma imagem de disco inicializável. Para instalar a ferramenta, vá para seu diretório home (ou qualquer diretório fora do seu projeto cargo) e execute o seguinte comando no seu terminal:
```
cargo install bootimage
```
Para executar `bootimage` e construir o bootloader, você precisa ter o componente rustup `llvm-tools-preview` instalado. Você pode fazer isso executando `rustup component add llvm-tools-preview`.
Depois de instalar `bootimage` e adicionar o componente `llvm-tools-preview`, você pode criar uma imagem de disco inicializável voltando para o diretório do seu projeto cargo e executando:
```
> cargo bootimage
```
Vemos que a ferramenta recompila nosso kernel usando `cargo build`, então automaticamente pegará quaisquer mudanças que você fizer. Depois, ela compila o bootloader, o que pode demorar um pouco. Como todas as dependências de crate, ele é compilado apenas uma vez e então armazenado em cache, então compilações subsequentes serão muito mais rápidas. Finalmente, `bootimage` combina o bootloader e seu kernel em uma imagem de disco inicializável.
Após executar o comando, você deve ver uma imagem de disco inicializável chamada `bootimage-blog_os.bin` no seu diretório `target/x86_64-blog_os/debug`. Você pode inicializá-la em uma máquina virtual ou copiá-la para um pendrive USB para inicializá-la em hardware real. (Note que este não é uma imagem de CD, que tem um formato diferente, então gravá-la em um CD não funciona).
#### Como funciona?
A ferramenta `bootimage` executa os seguintes passos nos bastidores:
- Ela compila nosso kernel para um arquivo [ELF].
- Ela compila a dependência do bootloader como um executável autônomo.
- Ela vincula os bytes do arquivo ELF do kernel ao bootloader.
[ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
[rust-osdev/bootloader]: https://github.com/rust-osdev/bootloader
Quando inicializado, o bootloader lê e analisa o arquivo ELF anexado. Ele então mapeia os segmentos do programa para endereços virtuais nas tabelas de página, zera a seção `.bss` e configura um stack. Finalmente, ele lê o endereço do ponto de entrada (nossa função `_start`) e salta para ele.
### Inicializando no QEMU
Agora podemos inicializar a imagem de disco em uma máquina virtual. Para inicializá-la no [QEMU], execute o seguinte comando:
[QEMU]: https://www.qemu.org/
```
> qemu-system-x86_64 -drive format=raw,file=target/x86_64-blog_os/debug/bootimage-blog_os.bin
```
Isso abre uma janela separada que deve se parecer com isto:
![QEMU mostrando "Hello World!"](qemu.png)
Vemos que nosso "Hello World!" está visível na tela.
### Máquina Real
Também é possível escrevê-lo em um pendrive USB e inicializá-lo em uma máquina real, **mas tenha cuidado** para escolher o nome correto do dispositivo, porque **tudo naquele dispositivo será sobrescrito**:
```
> dd if=target/x86_64-blog_os/debug/bootimage-blog_os.bin of=/dev/sdX && sync
```
Onde `sdX` é o nome do dispositivo do seu pendrive USB.
Depois de escrever a imagem no pendrive USB, você pode executá-la em hardware real inicializando a partir dele. Você provavelmente precisará usar um menu de boot especial ou alterar a ordem de boot na configuração do BIOS para inicializar a partir do pendrive USB. Note que atualmente não funciona para máquinas UEFI, já que a crate `bootloader` ainda não tem suporte UEFI.
### Usando `cargo run`
Para facilitar a execução do nosso kernel no QEMU, podemos definir a chave de configuração `runner` para o cargo:
```toml
# em .cargo/config.toml
[target.'cfg(target_os = "none")']
runner = "bootimage runner"
```
A tabela `target.'cfg(target_os = "none")'` se aplica a todos os alvos cujo campo `"os"` do arquivo de configuração de alvo está definido como `"none"`. Isso inclui nosso alvo `x86_64-blog_os.json`. A chave `runner` especifica o comando que deve ser invocado para `cargo run`. O comando é executado após uma compilação bem-sucedida com o caminho do executável passado como o primeiro argumento. Veja a [documentação do cargo][configuração cargo] para mais detalhes.
O comando `bootimage runner` é especificamente projetado para ser utilizável como um executável `runner`. Ele vincula o executável dado com a dependência do bootloader do projeto e então lança o QEMU. Veja o [Readme do `bootimage`] para mais detalhes e opções de configuração possíveis.
[Readme do `bootimage`]: https://github.com/rust-osdev/bootimage
Agora podemos usar `cargo run` para compilar nosso kernel e inicializá-lo no QEMU.
## O que vem a seguir?
No próximo post, exploraremos o buffer de texto VGA em mais detalhes e escreveremos uma interface segura para ele. Também adicionaremos suporte para a macro `println`.

View File

@@ -0,0 +1,703 @@
+++
title = "Modo de Texto VGA"
weight = 3
path = "pt-BR/vga-text-mode"
date = 2018-02-26
[extra]
chapter = "O Básico"
# Please update this when updating the translation
translation_based_on_commit = "9753695744854686a6b80012c89b0d850a44b4b0"
# GitHub usernames of the people that translated this post
translators = ["richarddalves"]
+++
O [modo de texto VGA] é uma maneira simples de imprimir texto na tela. Neste post, criamos uma interface que torna seu uso seguro e simples ao encapsular toda a unsafety em um módulo separado. Também implementamos suporte para as [macros de formatação] do Rust.
[modo de texto VGA]: https://en.wikipedia.org/wiki/VGA-compatible_text_mode
[macros de formatação]: https://doc.rust-lang.org/std/fmt/#related-macros
<!-- more -->
Este blog é desenvolvido abertamente no [GitHub]. Se você tiver algum problema ou dúvida, abra um issue lá. Você também pode deixar comentários [na parte inferior]. O código-fonte completo desta publicação pode ser encontrado na branch [`post-03`][post branch].
[GitHub]: https://github.com/phil-opp/blog_os
[na parte inferior]: #comments
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
[post branch]: https://github.com/phil-opp/blog_os/tree/post-03
<!-- toc -->
## O Buffer de Texto VGA
Para imprimir um caractere na tela em modo de texto VGA, é preciso escrevê-lo no buffer de texto do hardware VGA. O buffer de texto VGA é um array bidimensional com tipicamente 25 linhas e 80 colunas, que é renderizado diretamente na tela. Cada entrada do array descreve um único caractere da tela através do seguinte formato:
Bit(s) | Valor
------ | ----------------
0-7 | Ponto de código ASCII
8-11 | Cor do primeiro plano
12-14 | Cor do fundo
15 | Piscar
O primeiro byte representa o caractere que deve ser impresso na [codificação ASCII]. Para ser mais específico, não é exatamente ASCII, mas um conjunto de caracteres chamado [_página de código 437_] com alguns caracteres adicionais e pequenas modificações. Para simplificar, continuaremos chamando-o de caractere ASCII neste post.
[codificação ASCII]: https://en.wikipedia.org/wiki/ASCII
[_página de código 437_]: https://en.wikipedia.org/wiki/Code_page_437
O segundo byte define como o caractere é exibido. Os primeiros quatro bits definem a cor do primeiro plano, os próximos três bits a cor do fundo, e o último bit se o caractere deve piscar. As seguintes cores estão disponíveis:
Número | Cor | Número + Bit Brilhante | Cor Brilhante
------ | ------------- | ---------------------- | --------------
0x0 | Preto | 0x8 | Cinza Escuro
0x1 | Azul | 0x9 | Azul Claro
0x2 | Verde | 0xa | Verde Claro
0x3 | Ciano | 0xb | Ciano Claro
0x4 | Vermelho | 0xc | Vermelho Claro
0x5 | Magenta | 0xd | Rosa
0x6 | Marrom | 0xe | Amarelo
0x7 | Cinza Claro | 0xf | Branco
O bit 4 é o _bit brilhante_, que transforma, por exemplo, azul em azul claro. Para a cor de fundo, este bit é reaproveitado como o bit de piscar.
O buffer de texto VGA é acessível via [I/O mapeado em memória] no endereço `0xb8000`. Isso significa que leituras e escritas naquele endereço não acessam a RAM, mas acessam diretamente o buffer de texto no hardware VGA. Isso significa que podemos lê-lo e escrevê-lo através de operações normais de memória naquele endereço.
[I/O mapeado em memória]: https://en.wikipedia.org/wiki/Memory-mapped_I/O
Note que hardware mapeado em memória pode não suportar todas as operações normais de RAM. Por exemplo, um dispositivo poderia suportar apenas leituras byte a byte e retornar lixo quando um `u64` é lido. Felizmente, o buffer de texto [suporta leituras e escritas normais], então não precisamos tratá-lo de maneira especial.
[suporta leituras e escritas normais]: https://web.stanford.edu/class/cs140/projects/pintos/specs/freevga/vga/vgamem.htm#manip
## Um Módulo Rust
Agora que sabemos como o buffer VGA funciona, podemos criar um módulo Rust para lidar com a impressão:
```rust
// em src/main.rs
mod vga_buffer;
```
Para o conteúdo deste módulo, criamos um novo arquivo `src/vga_buffer.rs`. Todo o código abaixo vai para nosso novo módulo (a menos que especificado o contrário).
### Cores
Primeiro, representamos as diferentes cores usando um enum:
```rust
// em src/vga_buffer.rs
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Color {
Black = 0,
Blue = 1,
Green = 2,
Cyan = 3,
Red = 4,
Magenta = 5,
Brown = 6,
LightGray = 7,
DarkGray = 8,
LightBlue = 9,
LightGreen = 10,
LightCyan = 11,
LightRed = 12,
Pink = 13,
Yellow = 14,
White = 15,
}
```
Usamos um [enum estilo C] aqui para especificar explicitamente o número para cada cor. Por causa do atributo `repr(u8)`, cada variante do enum é armazenada como um `u8`. Na verdade, 4 bits seriam suficientes, mas Rust não tem um tipo `u4`.
[enum estilo C]: https://doc.rust-lang.org/rust-by-example/custom_types/enum/c_like.html
Normalmente o compilador emitiria um aviso para cada variante não utilizada. Ao usar o atributo `#[allow(dead_code)]`, desabilitamos esses avisos para o enum `Color`.
Ao [derivar] as traits [`Copy`], [`Clone`], [`Debug`], [`PartialEq`] e [`Eq`], habilitamos [semântica de cópia] para o tipo e o tornamos imprimível e comparável.
[derivar]: https://doc.rust-lang.org/rust-by-example/trait/derive.html
[`Copy`]: https://doc.rust-lang.org/nightly/core/marker/trait.Copy.html
[`Clone`]: https://doc.rust-lang.org/nightly/core/clone/trait.Clone.html
[`Debug`]: https://doc.rust-lang.org/nightly/core/fmt/trait.Debug.html
[`PartialEq`]: https://doc.rust-lang.org/nightly/core/cmp/trait.PartialEq.html
[`Eq`]: https://doc.rust-lang.org/nightly/core/cmp/trait.Eq.html
[semântica de cópia]: https://doc.rust-lang.org/1.30.0/book/first-edition/ownership.html#copy-types
Para representar um código de cor completo que especifica as cores de primeiro plano e de fundo, criamos um [newtype] em cima de `u8`:
[newtype]: https://doc.rust-lang.org/rust-by-example/generics/new_types.html
```rust
// em src/vga_buffer.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(transparent)]
struct ColorCode(u8);
impl ColorCode {
fn new(foreground: Color, background: Color) -> ColorCode {
ColorCode((background as u8) << 4 | (foreground as u8))
}
}
```
A struct `ColorCode` contém o byte de cor completo, contendo as cores de primeiro plano e de fundo. Como antes, derivamos as traits `Copy` e `Debug` para ela. Para garantir que o `ColorCode` tenha exatamente o mesmo layout de dados que um `u8`, usamos o atributo [`repr(transparent)`].
[`repr(transparent)`]: https://doc.rust-lang.org/nomicon/other-reprs.html#reprtransparent
### Buffer de Texto
Agora podemos adicionar estruturas para representar um caractere da tela e o buffer de texto:
```rust
// em src/vga_buffer.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
struct ScreenChar {
ascii_character: u8,
color_code: ColorCode,
}
const BUFFER_HEIGHT: usize = 25;
const BUFFER_WIDTH: usize = 80;
#[repr(transparent)]
struct Buffer {
chars: [[ScreenChar; BUFFER_WIDTH]; BUFFER_HEIGHT],
}
```
Como a ordenação dos campos em structs padrão é indefinida em Rust, precisamos do atributo [`repr(C)`]. Ele garante que os campos da struct sejam dispostos exatamente como em uma struct C e, portanto, garante a ordenação correta dos campos. Para a struct `Buffer`, usamos [`repr(transparent)`] novamente para garantir que ela tenha o mesmo layout de memória que seu único campo.
[`repr(C)`]: https://doc.rust-lang.org/nightly/nomicon/other-reprs.html#reprc
Para realmente escrever na tela, agora criamos um tipo writer:
```rust
// em src/vga_buffer.rs
pub struct Writer {
column_position: usize,
color_code: ColorCode,
buffer: &'static mut Buffer,
}
```
O writer sempre escreverá na última linha e deslocará as linhas para cima quando uma linha estiver cheia (ou no `\n`). O campo `column_position` acompanha a posição atual na última linha. As cores de primeiro plano e de fundo atuais são especificadas por `color_code` e uma referência ao buffer VGA é armazenada em `buffer`. Note que precisamos de um [lifetime explícito] aqui para dizer ao compilador por quanto tempo a referência é válida. O lifetime [`'static`] especifica que a referência é válida durante toda a execução do programa (o que é verdade para o buffer de texto VGA).
[lifetime explícito]: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#lifetime-annotation-syntax
[`'static`]: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#the-static-lifetime
### Impressão
Agora podemos usar o `Writer` para modificar os caracteres do buffer. Primeiro criamos um método para escrever um único byte ASCII:
```rust
// em src/vga_buffer.rs
impl Writer {
pub fn write_byte(&mut self, byte: u8) {
match byte {
b'\n' => self.new_line(),
byte => {
if self.column_position >= BUFFER_WIDTH {
self.new_line();
}
let row = BUFFER_HEIGHT - 1;
let col = self.column_position;
let color_code = self.color_code;
self.buffer.chars[row][col] = ScreenChar {
ascii_character: byte,
color_code,
};
self.column_position += 1;
}
}
}
fn new_line(&mut self) {/* TODO */}
}
```
Se o byte é o byte de [newline] `\n`, o writer não imprime nada. Em vez disso, ele chama um método `new_line`, que implementaremos mais tarde. Outros bytes são impressos na tela no segundo caso `match`.
[newline]: https://en.wikipedia.org/wiki/Newline
Ao imprimir um byte, o writer verifica se a linha atual está cheia. Nesse caso, uma chamada `new_line` é usada para quebrar a linha. Então ele escreve um novo `ScreenChar` no buffer na posição atual. Finalmente, a posição da coluna atual é avançada.
Para imprimir strings inteiras, podemos convertê-las em bytes e imprimi-los um por um:
```rust
// em src/vga_buffer.rs
impl Writer {
pub fn write_string(&mut self, s: &str) {
for byte in s.bytes() {
match byte {
// byte ASCII imprimível ou newline
0x20..=0x7e | b'\n' => self.write_byte(byte),
// não faz parte da faixa ASCII imprimível
_ => self.write_byte(0xfe),
}
}
}
}
```
O buffer de texto VGA suporta apenas ASCII e os bytes adicionais da [página de código 437]. Strings Rust são [UTF-8] por padrão, então podem conter bytes que não são suportados pelo buffer de texto VGA. Usamos um `match` para diferenciar bytes ASCII imprimíveis (um newline ou qualquer coisa entre um caractere de espaço e um caractere `~`) e bytes não imprimíveis. Para bytes não imprimíveis, imprimimos um caractere `■`, que tem o código hexadecimal `0xfe` no hardware VGA.
[página de código 437]: https://en.wikipedia.org/wiki/Code_page_437
[UTF-8]: https://www.fileformat.info/info/unicode/utf8.htm
#### Experimente!
Para escrever alguns caracteres na tela, você pode criar uma função temporária:
```rust
// em src/vga_buffer.rs
pub fn print_something() {
let mut writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
writer.write_byte(b'H');
writer.write_string("ello ");
writer.write_string("Wörld!");
}
```
Primeiro ele cria um novo Writer que aponta para o buffer VGA em `0xb8000`. A sintaxe para isso pode parecer um pouco estranha: Primeiro, convertemos o inteiro `0xb8000` como um [ponteiro bruto] mutável. Então o convertemos em uma referência mutável ao desreferenciá-lo (através de `*`) e imediatamente emprestar novamente (através de `&mut`). Esta conversão requer um [bloco `unsafe`], pois o compilador não pode garantir que o ponteiro bruto é válido.
[ponteiro bruto]: https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html#dereferencing-a-raw-pointer
[bloco `unsafe`]: https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html
Então ele escreve o byte `b'H'` nele. O prefixo `b` cria um [byte literal], que representa um caractere ASCII. Ao escrever as strings `"ello "` e `"Wörld!"`, testamos nosso método `write_string` e o tratamento de caracteres não imprimíveis. Para ver a saída, precisamos chamar a função `print_something` da nossa função `_start`:
```rust
// em src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
vga_buffer::print_something();
loop {}
}
```
Quando executamos nosso projeto agora, um `Hello W■■rld!` deve ser impresso no canto inferior _esquerdo_ da tela em amarelo:
[byte literal]: https://doc.rust-lang.org/reference/tokens.html#byte-literals
![QEMU exibindo um `Hello W■■rld!` amarelo no canto inferior esquerdo](vga-hello.png)
Note que o `ö` é impresso como dois caracteres `■`. Isso ocorre porque `ö` é representado por dois bytes em [UTF-8], que ambos não se enquadram na faixa ASCII imprimível. Na verdade, esta é uma propriedade fundamental do UTF-8: os bytes individuais de valores multi-byte nunca são ASCII válido.
### Volatile
Acabamos de ver que nossa mensagem foi impressa corretamente. No entanto, pode não funcionar com futuros compiladores Rust que otimizam de forma mais agressiva.
O problema é que escrevemos apenas no `Buffer` e nunca lemos dele novamente. O compilador não sabe que realmente acessamos memória do buffer VGA (em vez de RAM normal) e não sabe nada sobre o efeito colateral de que alguns caracteres aparecem na tela. Então ele pode decidir que essas escritas são desnecessárias e podem ser omitidas. Para evitar esta otimização errônea, precisamos especificar essas escritas como _[volatile]_. Isso diz ao compilador que a escrita tem efeitos colaterais e não deve ser otimizada.
[volatile]: https://en.wikipedia.org/wiki/Volatile_(computer_programming)
Para usar escritas volatile para o buffer VGA, usamos a biblioteca [volatile][volatile crate]. Esta _crate_ (é assim que os pacotes são chamados no mundo Rust) fornece um tipo wrapper `Volatile` com métodos `read` e `write`. Esses métodos usam internamente as funções [read_volatile] e [write_volatile] da biblioteca core e, portanto, garantem que as leituras/escritas não sejam otimizadas.
[volatile crate]: https://docs.rs/volatile
[read_volatile]: https://doc.rust-lang.org/nightly/core/ptr/fn.read_volatile.html
[write_volatile]: https://doc.rust-lang.org/nightly/core/ptr/fn.write_volatile.html
Podemos adicionar uma dependência na crate `volatile` adicionando-a à seção `dependencies` do nosso `Cargo.toml`:
```toml
# em Cargo.toml
[dependencies]
volatile = "0.2.6"
```
Certifique-se de especificar a versão `0.2.6` do `volatile`. Versões mais novas da crate não são compatíveis com este post.
`0.2.6` é o número de versão [semântico]. Para mais informações, veja o guia [Specifying Dependencies] da documentação do cargo.
[semântico]: https://semver.org/
[Specifying Dependencies]: https://doc.crates.io/specifying-dependencies.html
Vamos usá-lo para tornar as escritas no buffer VGA volatile. Atualizamos nosso tipo `Buffer` da seguinte forma:
```rust
// em src/vga_buffer.rs
use volatile::Volatile;
struct Buffer {
chars: [[Volatile<ScreenChar>; BUFFER_WIDTH]; BUFFER_HEIGHT],
}
```
Em vez de um `ScreenChar`, agora estamos usando um `Volatile<ScreenChar>`. (O tipo `Volatile` é [genérico] e pode envolver (quase) qualquer tipo). Isso garante que não possamos escrever nele "normalmente" acidentalmente. Em vez disso, temos que usar o método `write` agora.
[genérico]: https://doc.rust-lang.org/book/ch10-01-syntax.html
Isso significa que temos que atualizar nosso método `Writer::write_byte`:
```rust
// em src/vga_buffer.rs
impl Writer {
pub fn write_byte(&mut self, byte: u8) {
match byte {
b'\n' => self.new_line(),
byte => {
...
self.buffer.chars[row][col].write(ScreenChar {
ascii_character: byte,
color_code,
});
...
}
}
}
...
}
```
Em vez de uma atribuição típica usando `=`, agora estamos usando o método `write`. Agora podemos garantir que o compilador nunca otimizará esta escrita.
### Macros de Formatação
Seria bom suportar as macros de formatação do Rust também. Dessa forma, podemos facilmente imprimir diferentes tipos, como inteiros ou floats. Para suportá-las, precisamos implementar a trait [`core::fmt::Write`]. O único método necessário desta trait é `write_str`, que se parece bastante com nosso método `write_string`, apenas com um tipo de retorno `fmt::Result`:
[`core::fmt::Write`]: https://doc.rust-lang.org/nightly/core/fmt/trait.Write.html
```rust
// em src/vga_buffer.rs
use core::fmt;
impl fmt::Write for Writer {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.write_string(s);
Ok(())
}
}
```
O `Ok(())` é apenas um Result `Ok` contendo o tipo `()`.
Agora podemos usar as macros de formatação `write!`/`writeln!` embutidas do Rust:
```rust
// em src/vga_buffer.rs
pub fn print_something() {
use core::fmt::Write;
let mut writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
writer.write_byte(b'H');
writer.write_string("ello! ");
write!(writer, "The numbers are {} and {}", 42, 1.0/3.0).unwrap();
}
```
Agora você deve ver um `Hello! The numbers are 42 and 0.3333333333333333` na parte inferior da tela. A chamada `write!` retorna um `Result` que causa um aviso se não usado, então chamamos a função [`unwrap`] nele, que entra em panic se ocorrer um erro. Isso não é um problema no nosso caso, pois escritas no buffer VGA nunca falham.
[`unwrap`]: https://doc.rust-lang.org/core/result/enum.Result.html#method.unwrap
### Newlines
Agora, simplesmente ignoramos newlines e caracteres que não cabem mais na linha. Em vez disso, queremos mover cada caractere uma linha para cima (a linha superior é excluída) e começar no início da última linha novamente. Para fazer isso, adicionamos uma implementação para o método `new_line` do `Writer`:
```rust
// em src/vga_buffer.rs
impl Writer {
fn new_line(&mut self) {
for row in 1..BUFFER_HEIGHT {
for col in 0..BUFFER_WIDTH {
let character = self.buffer.chars[row][col].read();
self.buffer.chars[row - 1][col].write(character);
}
}
self.clear_row(BUFFER_HEIGHT - 1);
self.column_position = 0;
}
fn clear_row(&mut self, row: usize) {/* TODO */}
}
```
Iteramos sobre todos os caracteres da tela e movemos cada caractere uma linha para cima. Note que o limite superior da notação de intervalo (`..`) é exclusivo. Também omitimos a linha 0 (o primeiro intervalo começa em `1`) porque é a linha que é deslocada para fora da tela.
Para finalizar o código de newline, adicionamos o método `clear_row`:
```rust
// em src/vga_buffer.rs
impl Writer {
fn clear_row(&mut self, row: usize) {
let blank = ScreenChar {
ascii_character: b' ',
color_code: self.color_code,
};
for col in 0..BUFFER_WIDTH {
self.buffer.chars[row][col].write(blank);
}
}
}
```
Este método limpa uma linha sobrescrevendo todos os seus caracteres com um caractere de espaço.
## Uma Interface Global
Para fornecer um writer global que possa ser usado como uma interface de outros módulos sem carregar uma instância `Writer` por aí, tentamos criar um `WRITER` static:
```rust
// em src/vga_buffer.rs
pub static WRITER: Writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
```
No entanto, se tentarmos compilá-lo agora, os seguintes erros ocorrem:
```
error[E0015]: calls in statics are limited to constant functions, tuple structs and tuple variants
--> src/vga_buffer.rs:7:17
|
7 | color_code: ColorCode::new(Color::Yellow, Color::Black),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error[E0396]: raw pointers cannot be dereferenced in statics
--> src/vga_buffer.rs:8:22
|
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ dereference of raw pointer in constant
error[E0017]: references in statics may only refer to immutable values
--> src/vga_buffer.rs:8:22
|
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics require immutable values
error[E0017]: references in statics may only refer to immutable values
--> src/vga_buffer.rs:8:13
|
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics require immutable values
```
Para entender o que está acontecendo aqui, precisamos saber que statics são inicializados em tempo de compilação, ao contrário de variáveis normais que são inicializadas em tempo de execução. O componente do compilador Rust que avalia tais expressões de inicialização é chamado de "[const evaluator]". Sua funcionalidade ainda é limitada, mas há trabalho contínuo para expandi-la, por exemplo no RFC "[Allow panicking in constants]".
[const evaluator]: https://rustc-dev-guide.rust-lang.org/const-eval.html
[Allow panicking in constants]: https://github.com/rust-lang/rfcs/pull/2345
O problema com `ColorCode::new` seria solucionável usando [funções `const`], mas o problema fundamental aqui é que o const evaluator do Rust não é capaz de converter ponteiros brutos em referências em tempo de compilação. Talvez funcione algum dia, mas até lá, precisamos encontrar outra solução.
[funções `const`]: https://doc.rust-lang.org/reference/const_eval.html#const-functions
### Lazy Statics
A inicialização única de statics com funções não const é um problema comum em Rust. Felizmente, já existe uma boa solução em uma crate chamada [lazy_static]. Esta crate fornece uma macro `lazy_static!` que define um `static` inicializado lazily. Em vez de calcular seu valor em tempo de compilação, o `static` se inicializa lazily quando é acessado pela primeira vez. Assim, a inicialização acontece em tempo de execução, então código de inicialização arbitrariamente complexo é possível.
[lazy_static]: https://docs.rs/lazy_static/1.0.1/lazy_static/
Vamos adicionar a crate `lazy_static` ao nosso projeto:
```toml
# em Cargo.toml
[dependencies.lazy_static]
version = "1.0"
features = ["spin_no_std"]
```
Precisamos do recurso `spin_no_std`, já que não vinculamos a biblioteca padrão.
Com `lazy_static`, podemos definir nosso `WRITER` static sem problemas:
```rust
// em src/vga_buffer.rs
use lazy_static::lazy_static;
lazy_static! {
pub static ref WRITER: Writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
}
```
No entanto, este `WRITER` é praticamente inútil, pois é imutável. Isso significa que não podemos escrever nada nele (já que todos os métodos de escrita recebem `&mut self`). Uma solução possível seria usar um [static mutável]. Mas então cada leitura e escrita nele seria unsafe, pois poderia facilmente introduzir data races e outras coisas ruins. Usar `static mut` é altamente desencorajado. Até houve propostas para [removê-lo][remove static mut]. Mas quais são as alternativas? Poderíamos tentar usar um static imutável com um tipo de célula como [RefCell] ou até [UnsafeCell] que fornece [mutabilidade interior]. Mas esses tipos não são [Sync] \(com boa razão), então não podemos usá-los em statics.
[static mutável]: https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html#accessing-or-modifying-a-mutable-static-variable
[remove static mut]: https://internals.rust-lang.org/t/pre-rfc-remove-static-mut/1437
[RefCell]: https://doc.rust-lang.org/book/ch15-05-interior-mutability.html#keeping-track-of-borrows-at-runtime-with-refcellt
[UnsafeCell]: https://doc.rust-lang.org/nightly/core/cell/struct.UnsafeCell.html
[mutabilidade interior]: https://doc.rust-lang.org/book/ch15-05-interior-mutability.html
[Sync]: https://doc.rust-lang.org/nightly/core/marker/trait.Sync.html
### Spinlocks
Para obter mutabilidade interior sincronizada, usuários da biblioteca padrão podem usar [Mutex]. Ele fornece exclusão mútua bloqueando threads quando o recurso já está bloqueado. Mas nosso kernel básico não tem nenhum suporte de bloqueio ou mesmo um conceito de threads, então também não podemos usá-lo. No entanto, há um tipo realmente básico de mutex na ciência da computação que não requer nenhum recurso de sistema operacional: o [spinlock]. Em vez de bloquear, as threads simplesmente tentam bloqueá-lo novamente e novamente em um loop apertado, queimando assim tempo de CPU até que o mutex esteja livre novamente.
[Mutex]: https://doc.rust-lang.org/nightly/std/sync/struct.Mutex.html
[spinlock]: https://en.wikipedia.org/wiki/Spinlock
Para usar um spinning mutex, podemos adicionar a [crate spin] como uma dependência:
[crate spin]: https://crates.io/crates/spin
```toml
# em Cargo.toml
[dependencies]
spin = "0.5.2"
```
Então podemos usar o spinning mutex para adicionar [mutabilidade interior] segura ao nosso `WRITER` static:
```rust
// em src/vga_buffer.rs
use spin::Mutex;
...
lazy_static! {
pub static ref WRITER: Mutex<Writer> = Mutex::new(Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
});
}
```
Agora podemos deletar a função `print_something` e imprimir diretamente da nossa função `_start`:
```rust
// em src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
use core::fmt::Write;
vga_buffer::WRITER.lock().write_str("Hello again").unwrap();
write!(vga_buffer::WRITER.lock(), ", some numbers: {} {}", 42, 1.337).unwrap();
loop {}
}
```
Precisamos importar a trait `fmt::Write` para poder usar suas funções.
### Segurança
Note que temos apenas um bloco unsafe no nosso código, que é necessário para criar uma referência `Buffer` apontando para `0xb8000`. Depois disso, todas as operações são seguras. Rust usa verificação de limites para acessos a arrays por padrão, então não podemos escrever acidentalmente fora do buffer. Assim, codificamos as condições necessárias no sistema de tipos e somos capazes de fornecer uma interface segura para o exterior.
### Uma Macro println
Agora que temos um writer global, podemos adicionar uma macro `println` que pode ser usada de qualquer lugar na base de código. A [sintaxe de macro] do Rust é um pouco estranha, então não tentaremos escrever uma macro do zero. Em vez disso, olhamos para o código-fonte da [macro `println!`] na biblioteca padrão:
[sintaxe de macro]: https://doc.rust-lang.org/nightly/book/ch19-06-macros.html#declarative-macros-with-macro_rules-for-general-metaprogramming
[macro `println!`]: https://doc.rust-lang.org/nightly/std/macro.println!.html
```rust
#[macro_export]
macro_rules! println {
() => (print!("\n"));
($($arg:tt)*) => (print!("{}\n", format_args!($($arg)*)));
}
```
Macros são definidas através de uma ou mais regras, semelhantes a braços `match`. A macro `println` tem duas regras: A primeira regra é para invocações sem argumentos, por exemplo, `println!()`, que é expandida para `print!("\n")` e, portanto, apenas imprime um newline. A segunda regra é para invocações com parâmetros como `println!("Hello")` ou `println!("Number: {}", 4)`. Ela também é expandida para uma invocação da macro `print!`, passando todos os argumentos e um newline `\n` adicional no final.
O atributo `#[macro_export]` torna a macro disponível para toda a crate (não apenas o módulo em que é definida) e crates externas. Ele também coloca a macro na raiz da crate, o que significa que temos que importar a macro através de `use std::println` em vez de `std::macros::println`.
A [macro `print!`] é definida como:
[macro `print!`]: https://doc.rust-lang.org/nightly/std/macro.print!.html
```rust
#[macro_export]
macro_rules! print {
($($arg:tt)*) => ($crate::io::_print(format_args!($($arg)*)));
}
```
A macro se expande para uma chamada da [função `_print`] no módulo `io`. A [variável `$crate`] garante que a macro também funcione de fora da crate `std` ao se expandir para `std` quando é usada em outras crates.
A [macro `format_args`] constrói um tipo [fmt::Arguments] dos argumentos passados, que é passado para `_print`. A [função `_print`] da libstd chama `print_to`, que é bastante complicado porque suporta diferentes dispositivos `Stdout`. Não precisamos dessa complexidade, pois só queremos imprimir no buffer VGA.
[função `_print`]: https://github.com/rust-lang/rust/blob/29f5c699b11a6a148f097f82eaa05202f8799bbc/src/libstd/io/stdio.rs#L698
[variável `$crate`]: https://doc.rust-lang.org/1.30.0/book/first-edition/macros.html#the-variable-crate
[macro `format_args`]: https://doc.rust-lang.org/nightly/std/macro.format_args.html
[fmt::Arguments]: https://doc.rust-lang.org/nightly/core/fmt/struct.Arguments.html
Para imprimir no buffer VGA, apenas copiamos as macros `println!` e `print!`, mas as modificamos para usar nossa própria função `_print`:
```rust
// em src/vga_buffer.rs
#[macro_export]
macro_rules! print {
($($arg:tt)*) => ($crate::vga_buffer::_print(format_args!($($arg)*)));
}
#[macro_export]
macro_rules! println {
() => ($crate::print!("\n"));
($($arg:tt)*) => ($crate::print!("{}\n", format_args!($($arg)*)));
}
#[doc(hidden)]
pub fn _print(args: fmt::Arguments) {
use core::fmt::Write;
WRITER.lock().write_fmt(args).unwrap();
}
```
Uma coisa que mudamos da definição original de `println` é que também prefixamos as invocações da macro `print!` com `$crate`. Isso garante que não precisamos importar a macro `print!` também se quisermos usar apenas `println`.
Como na biblioteca padrão, adicionamos o atributo `#[macro_export]` a ambas as macros para torná-las disponíveis em todo lugar na nossa crate. Note que isso coloca as macros no namespace raiz da crate, então importá-las via `use crate::vga_buffer::println` não funciona. Em vez disso, temos que fazer `use crate::println`.
A função `_print` bloqueia nosso `WRITER` static e chama o método `write_fmt` nele. Este método é da trait `Write`, que precisamos importar. O `unwrap()` adicional no final entra em panic se a impressão não for bem-sucedida. Mas como sempre retornamos `Ok` em `write_str`, isso não deve acontecer.
Como as macros precisam ser capazes de chamar `_print` de fora do módulo, a função precisa ser pública. No entanto, como consideramos isso um detalhe de implementação privado, adicionamos o [atributo `doc(hidden)`] para ocultá-la da documentação gerada.
[atributo `doc(hidden)`]: https://doc.rust-lang.org/nightly/rustdoc/write-documentation/the-doc-attribute.html#hidden
### Hello World usando `println`
Agora podemos usar `println` na nossa função `_start`:
```rust
// em src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
println!("Hello World{}", "!");
loop {}
}
```
Note que não precisamos importar a macro na função main, porque ela já vive no namespace raiz.
Como esperado, agora vemos um _"Hello World!"_ na tela:
![QEMU imprimindo "Hello World!"](vga-hello-world.png)
### Imprimindo Mensagens de Panic
Agora que temos uma macro `println`, podemos usá-la na nossa função panic para imprimir a mensagem de panic e a localização do panic:
```rust
// em main.rs
/// Esta função é chamada em caso de pânico.
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("{}", info);
loop {}
}
```
Quando agora inserimos `panic!("Some panic message");` na nossa função `_start`, obtemos a seguinte saída:
![QEMU imprimindo "panicked at 'Some panic message', src/main.rs:28:5](vga-panic.png)
Então sabemos não apenas que um panic ocorreu, mas também a mensagem de panic e onde no código aconteceu.
## Resumo
Neste post, aprendemos sobre a estrutura do buffer de texto VGA e como ele pode ser escrito através do mapeamento de memória no endereço `0xb8000`. Criamos um módulo Rust que encapsula a unsafety de escrever neste buffer mapeado em memória e apresenta uma interface segura e conveniente para o exterior.
Graças ao cargo, também vimos como é fácil adicionar dependências em bibliotecas de terceiros. As duas dependências que adicionamos, `lazy_static` e `spin`, são muito úteis no desenvolvimento de SO e as usaremos em mais lugares em posts futuros.
## O que vem a seguir?
O próximo post explica como configurar o framework de testes unitários embutido do Rust. Criaremos então alguns testes unitários básicos para o módulo de buffer VGA deste post.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,474 @@
+++
title = "Exceções de CPU"
weight = 5
path = "pt-BR/cpu-exceptions"
date = 2018-06-17
[extra]
chapter = "Interrupções"
# Please update this when updating the translation
translation_based_on_commit = "9753695744854686a6b80012c89b0d850a44b4b0"
# GitHub usernames of the people that translated this post
translators = ["richarddalves"]
+++
Exceções de CPU ocorrem em várias situações errôneas, por exemplo, ao acessar um endereço de memória inválido ou ao dividir por zero. Para reagir a elas, precisamos configurar uma _tabela de descritores de interrupção_ que fornece funções manipuladoras. Ao final desta postagem, nosso kernel será capaz de capturar [exceções de breakpoint] e retomar a execução normal posteriormente.
[exceções de breakpoint]: https://wiki.osdev.org/Exceptions#Breakpoint
<!-- more -->
Este blog é desenvolvido abertamente no [GitHub]. Se você tiver algum problema ou dúvida, abra um issue lá. Você também pode deixar comentários [na parte inferior]. O código-fonte completo desta publicação pode ser encontrado na branch [`post-05`][post branch].
[GitHub]: https://github.com/phil-opp/blog_os
[na parte inferior]: #comments
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
[post branch]: https://github.com/phil-opp/blog_os/tree/post-05
<!-- toc -->
## Visão Geral
Uma exceção sinaliza que algo está errado com a instrução atual. Por exemplo, a CPU emite uma exceção se a instrução atual tenta dividir por 0. Quando uma exceção ocorre, a CPU interrompe seu trabalho atual e imediatamente chama uma função manipuladora de exceção específica, dependendo do tipo de exceção.
No x86, existem cerca de 20 tipos diferentes de exceções de CPU. As mais importantes são:
- **Page Fault**: Um page fault ocorre em acessos ilegais à memória. Por exemplo, se a instrução atual tenta ler de uma página não mapeada ou tenta escrever em uma página somente leitura.
- **Invalid Opcode**: Esta exceção ocorre quando a instrução atual é inválida, por exemplo, quando tentamos usar novas [instruções SSE] em uma CPU antiga que não as suporta.
- **General Protection Fault**: Esta é a exceção com a gama mais ampla de causas. Ela ocorre em vários tipos de violações de acesso, como tentar executar uma instrução privilegiada em código de nível de usuário ou escrever em campos reservados de registradores de configuração.
- **Double Fault**: Quando uma exceção ocorre, a CPU tenta chamar a função manipuladora correspondente. Se outra exceção ocorre _enquanto chama o manipulador de exceção_, a CPU levanta uma exceção de double fault. Esta exceção também ocorre quando não há função manipuladora registrada para uma exceção.
- **Triple Fault**: Se uma exceção ocorre enquanto a CPU tenta chamar a função manipuladora de double fault, ela emite um _triple fault_ fatal. Não podemos capturar ou manipular um triple fault. A maioria dos processadores reage redefinindo-se e reinicializando o sistema operacional.
[instruções SSE]: https://en.wikipedia.org/wiki/Streaming_SIMD_Extensions
Para a lista completa de exceções, consulte a [wiki do OSDev][exceptions].
[exceptions]: https://wiki.osdev.org/Exceptions
### A Tabela de Descritores de Interrupção
Para capturar e manipular exceções, precisamos configurar uma chamada _Tabela de Descritores de Interrupção_ (IDT - Interrupt Descriptor Table). Nesta tabela, podemos especificar uma função manipuladora para cada exceção de CPU. O hardware usa esta tabela diretamente, então precisamos seguir um formato predefinido. Cada entrada deve ter a seguinte estrutura de 16 bytes:
Tipo| Nome | Descrição
----|--------------------------|-----------------------------------
u16 | Function Pointer [0:15] | Os bits inferiores do ponteiro para a função manipuladora.
u16 | GDT selector | Seletor de um segmento de código na [tabela de descritores globais].
u16 | Options | (veja abaixo)
u16 | Function Pointer [16:31] | Os bits do meio do ponteiro para a função manipuladora.
u32 | Function Pointer [32:63] | Os bits restantes do ponteiro para a função manipuladora.
u32 | Reserved |
[tabela de descritores globais]: https://en.wikipedia.org/wiki/Global_Descriptor_Table
O campo options tem o seguinte formato:
Bits | Nome | Descrição
------|-----------------------------------|-----------------------------------
0-2 | Interrupt Stack Table Index | 0: Não troca stacks, 1-7: Troca para a n-ésima stack na Interrupt Stack Table quando este manipulador é chamado.
3-7 | Reserved |
8 | 0: Interrupt Gate, 1: Trap Gate | Se este bit é 0, as interrupções são desativadas quando este manipulador é chamado.
9-11 | must be one |
12 | must be zero |
1314 | Descriptor Privilege Level (DPL) | O nível mínimo de privilégio necessário para chamar este manipulador.
15 | Present |
Cada exceção tem um índice predefinido na IDT. Por exemplo, a exceção invalid opcode tem índice de tabela 6 e a exceção page fault tem índice de tabela 14. Assim, o hardware pode automaticamente carregar a entrada IDT correspondente para cada exceção. A [Tabela de Exceções][exceptions] na wiki do OSDev mostra os índices IDT de todas as exceções na coluna "Vector nr.".
Quando uma exceção ocorre, a CPU aproximadamente faz o seguinte:
1. Empurra alguns registradores na pilha, incluindo o ponteiro de instrução e o registrador [RFLAGS]. (Usaremos esses valores mais tarde nesta postagem.)
2. Lê a entrada correspondente da Tabela de Descritores de Interrupção (IDT). Por exemplo, a CPU lê a 14ª entrada quando ocorre um page fault.
3. Verifica se a entrada está presente e, se não estiver, levanta um double fault.
4. Desativa interrupções de hardware se a entrada é um interrupt gate (bit 40 não está definido).
5. Carrega o seletor [GDT] especificado no CS (segmento de código).
6. Pula para a função manipuladora especificada.
[RFLAGS]: https://en.wikipedia.org/wiki/FLAGS_register
[GDT]: https://en.wikipedia.org/wiki/Global_Descriptor_Table
Não se preocupe com os passos 4 e 5 por enquanto; aprenderemos sobre a tabela de descritores globais e interrupções de hardware em postagens futuras.
## Um Tipo IDT
Em vez de criar nosso próprio tipo IDT, usaremos a [struct `InterruptDescriptorTable`] da crate `x86_64`, que se parece com isto:
[struct `InterruptDescriptorTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html
``` rust
#[repr(C)]
pub struct InterruptDescriptorTable {
pub divide_by_zero: Entry<HandlerFunc>,
pub debug: Entry<HandlerFunc>,
pub non_maskable_interrupt: Entry<HandlerFunc>,
pub breakpoint: Entry<HandlerFunc>,
pub overflow: Entry<HandlerFunc>,
pub bound_range_exceeded: Entry<HandlerFunc>,
pub invalid_opcode: Entry<HandlerFunc>,
pub device_not_available: Entry<HandlerFunc>,
pub double_fault: Entry<HandlerFuncWithErrCode>,
pub invalid_tss: Entry<HandlerFuncWithErrCode>,
pub segment_not_present: Entry<HandlerFuncWithErrCode>,
pub stack_segment_fault: Entry<HandlerFuncWithErrCode>,
pub general_protection_fault: Entry<HandlerFuncWithErrCode>,
pub page_fault: Entry<PageFaultHandlerFunc>,
pub x87_floating_point: Entry<HandlerFunc>,
pub alignment_check: Entry<HandlerFuncWithErrCode>,
pub machine_check: Entry<HandlerFunc>,
pub simd_floating_point: Entry<HandlerFunc>,
pub virtualization: Entry<HandlerFunc>,
pub security_exception: Entry<HandlerFuncWithErrCode>,
// alguns campos omitidos
}
```
Os campos têm o tipo [`idt::Entry<F>`], que é uma struct que representa os campos de uma entrada IDT (veja a tabela acima). O parâmetro de tipo `F` define o tipo de função manipuladora esperado. Vemos que algumas entradas requerem uma [`HandlerFunc`] e algumas entradas requerem uma [`HandlerFuncWithErrCode`]. O page fault tem até seu próprio tipo especial: [`PageFaultHandlerFunc`].
[`idt::Entry<F>`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.Entry.html
[`HandlerFunc`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/type.HandlerFunc.html
[`HandlerFuncWithErrCode`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/type.HandlerFuncWithErrCode.html
[`PageFaultHandlerFunc`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/type.PageFaultHandlerFunc.html
Vamos olhar primeiro para o tipo `HandlerFunc`:
```rust
type HandlerFunc = extern "x86-interrupt" fn(_: InterruptStackFrame);
```
É um [type alias] para um tipo `extern "x86-interrupt" fn`. A palavra-chave `extern` define uma função com uma [convenção de chamada estrangeira] e é frequentemente usada para se comunicar com código C (`extern "C" fn`). Mas o que é a convenção de chamada `x86-interrupt`?
[type alias]: https://doc.rust-lang.org/book/ch19-04-advanced-types.html#creating-type-synonyms-with-type-aliases
[convenção de chamada estrangeira]: https://doc.rust-lang.org/nomicon/ffi.html#foreign-calling-conventions
## A Convenção de Chamada de Interrupção
Exceções são bastante similares a chamadas de função: A CPU pula para a primeira instrução da função chamada e a executa. Depois, a CPU pula para o endereço de retorno e continua a execução da função pai.
No entanto, há uma diferença importante entre exceções e chamadas de função: Uma chamada de função é invocada voluntariamente por uma instrução `call` inserida pelo compilador, enquanto uma exceção pode ocorrer em _qualquer_ instrução. Para entender as consequências desta diferença, precisamos examinar as chamadas de função em mais detalhes.
[Convenções de chamada] especificam os detalhes de uma chamada de função. Por exemplo, elas especificam onde os parâmetros da função são colocados (por exemplo, em registradores ou na pilha) e como os resultados são retornados. No x86_64 Linux, as seguintes regras se aplicam para funções C (especificadas no [System V ABI]):
[Convenções de chamada]: https://en.wikipedia.org/wiki/Calling_convention
[System V ABI]: https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf
- os primeiros seis argumentos inteiros são passados nos registradores `rdi`, `rsi`, `rdx`, `rcx`, `r8`, `r9`
- argumentos adicionais são passados na pilha
- resultados são retornados em `rax` e `rdx`
Note que Rust não segue a ABI do C (na verdade, [nem existe uma ABI Rust ainda][rust abi]), então essas regras se aplicam apenas a funções declaradas como `extern "C" fn`.
[rust abi]: https://github.com/rust-lang/rfcs/issues/600
### Registradores Preservados e Scratch
A convenção de chamada divide os registradores em duas partes: registradores _preservados_ e _scratch_.
Os valores dos registradores _preservados_ devem permanecer inalterados entre chamadas de função. Portanto, uma função chamada (a _"callee"_) só tem permissão para sobrescrever esses registradores se restaurar seus valores originais antes de retornar. Portanto, esses registradores são chamados de _"callee-saved"_. Um padrão comum é salvar esses registradores na pilha no início da função e restaurá-los logo antes de retornar.
Em contraste, uma função chamada tem permissão para sobrescrever registradores _scratch_ sem restrições. Se o chamador quiser preservar o valor de um registrador scratch entre uma chamada de função, ele precisa fazer backup e restaurá-lo antes da chamada de função (por exemplo, empurrando-o para a pilha). Portanto, os registradores scratch são _caller-saved_.
No x86_64, a convenção de chamada C especifica os seguintes registradores preservados e scratch:
registradores preservados | registradores scratch
---|---
`rbp`, `rbx`, `rsp`, `r12`, `r13`, `r14`, `r15` | `rax`, `rcx`, `rdx`, `rsi`, `rdi`, `r8`, `r9`, `r10`, `r11`
_callee-saved_ | _caller-saved_
O compilador conhece essas regras, então gera o código de acordo. Por exemplo, a maioria das funções começa com um `push rbp`, que faz backup de `rbp` na pilha (porque é um registrador callee-saved).
### Preservando Todos os Registradores
Em contraste com chamadas de função, exceções podem ocorrer em _qualquer_ instrução. Na maioria dos casos, não sabemos nem em tempo de compilação se o código gerado causará uma exceção. Por exemplo, o compilador não pode saber se uma instrução causa um stack overflow ou um page fault.
Como não sabemos quando uma exceção ocorre, não podemos fazer backup de nenhum registrador antes. Isso significa que não podemos usar uma convenção de chamada que depende de registradores caller-saved para manipuladores de exceção. Em vez disso, precisamos de uma convenção de chamada que preserva _todos os registradores_. A convenção de chamada `x86-interrupt` é tal convenção de chamada, então garante que todos os valores de registrador são restaurados para seus valores originais no retorno da função.
Note que isso não significa que todos os registradores são salvos na pilha na entrada da função. Em vez disso, o compilador apenas faz backup dos registradores que são sobrescritos pela função. Desta forma, código muito eficiente pode ser gerado para funções curtas que usam apenas alguns registradores.
### O Stack Frame de Interrupção
Em uma chamada de função normal (usando a instrução `call`), a CPU empurra o endereço de retorno antes de pular para a função alvo. No retorno da função (usando a instrução `ret`), a CPU retira este endereço de retorno e pula para ele. Então o stack frame de uma chamada de função normal se parece com isto:
![function stack frame](function-stack-frame.svg)
Para manipuladores de exceção e interrupção, no entanto, empurrar um endereço de retorno não seria suficiente, já que manipuladores de interrupção frequentemente executam em um contexto diferente (ponteiro de pilha, flags da CPU, etc.). Em vez disso, a CPU executa os seguintes passos quando uma interrupção ocorre:
0. **Salvando o antigo ponteiro de pilha**: A CPU lê os valores dos registradores ponteiro de pilha (`rsp`) e segmento de pilha (`ss`) e os lembra em um buffer interno.
1. **Alinhando o ponteiro de pilha**: Uma interrupção pode ocorrer em qualquer instrução, então o ponteiro de pilha pode ter qualquer valor também. No entanto, algumas instruções de CPU (por exemplo, algumas instruções SSE) requerem que o ponteiro de pilha esteja alinhado em um limite de 16 bytes, então a CPU realiza tal alinhamento logo após a interrupção.
2. **Trocando pilhas** (em alguns casos): Uma troca de pilha ocorre quando o nível de privilégio da CPU muda, por exemplo, quando uma exceção de CPU ocorre em um programa em modo usuário. Também é possível configurar trocas de pilha para interrupções específicas usando a chamada _Interrupt Stack Table_ (descrita na próxima postagem).
3. **Empurrando o antigo ponteiro de pilha**: A CPU empurra os valores `rsp` e `ss` do passo 0 para a pilha. Isso torna possível restaurar o ponteiro de pilha original ao retornar de um manipulador de interrupção.
4. **Empurrando e atualizando o registrador `RFLAGS`**: O registrador [`RFLAGS`] contém vários bits de controle e status. Na entrada de interrupção, a CPU muda alguns bits e empurra o valor antigo.
5. **Empurrando o ponteiro de instrução**: Antes de pular para a função manipuladora de interrupção, a CPU empurra o ponteiro de instrução (`rip`) e o segmento de código (`cs`). Isso é comparável ao push de endereço de retorno de uma chamada de função normal.
6. **Empurrando um código de erro** (para algumas exceções): Para algumas exceções específicas, como page faults, a CPU empurra um código de erro, que descreve a causa da exceção.
7. **Invocando o manipulador de interrupção**: A CPU lê o endereço e o descritor de segmento da função manipuladora de interrupção do campo correspondente na IDT. Ela então invoca este manipulador carregando os valores nos registradores `rip` e `cs`.
[`RFLAGS`]: https://en.wikipedia.org/wiki/FLAGS_register
Então o _interrupt stack frame_ se parece com isto:
![interrupt stack frame](exception-stack-frame.svg)
Na crate `x86_64`, o interrupt stack frame é representado pela struct [`InterruptStackFrame`]. Ela é passada para manipuladores de interrupção como `&mut` e pode ser usada para recuperar informações adicionais sobre a causa da exceção. A struct não contém campo de código de erro, já que apenas algumas exceções empurram um código de erro. Essas exceções usam o tipo de função [`HandlerFuncWithErrCode`] separado, que tem um argumento adicional `error_code`.
[`InterruptStackFrame`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptStackFrame.html
### Por Trás das Cortinas
A convenção de chamada `x86-interrupt` é uma abstração poderosa que esconde quase todos os detalhes confusos do processo de manipulação de exceção. No entanto, às vezes é útil saber o que está acontecendo por trás das cortinas. Aqui está uma breve visão geral das coisas das quais a convenção de chamada `x86-interrupt` cuida:
- **Recuperando os argumentos**: A maioria das convenções de chamada espera que os argumentos sejam passados em registradores. Isso não é possível para manipuladores de exceção, já que não devemos sobrescrever nenhum valor de registrador antes de fazer backup deles na pilha. Em vez disso, a convenção de chamada `x86-interrupt` está ciente de que os argumentos já estão na pilha em um deslocamento específico.
- **Retornando usando `iretq`**: Como o interrupt stack frame difere completamente dos stack frames de chamadas de função normais, não podemos retornar de funções manipuladoras através da instrução `ret` normal. Então, em vez disso, a instrução `iretq` deve ser usada.
- **Manipulando o código de erro**: O código de erro, que é empurrado para algumas exceções, torna as coisas muito mais complexas. Ele muda o alinhamento da pilha (veja o próximo ponto) e precisa ser retirado da pilha antes de retornar. A convenção de chamada `x86-interrupt` manipula toda essa complexidade. No entanto, ela não sabe qual função manipuladora é usada para qual exceção, então precisa deduzir essa informação do número de argumentos da função. Isso significa que o programador ainda é responsável por usar o tipo de função correto para cada exceção. Felizmente, o tipo `InterruptDescriptorTable` definido pela crate `x86_64` garante que os tipos de função corretos são usados.
- **Alinhando a pilha**: Algumas instruções (especialmente instruções SSE) requerem um alinhamento de pilha de 16 bytes. A CPU garante esse alinhamento sempre que uma exceção ocorre, mas para algumas exceções ela o destrói novamente mais tarde quando empurra um código de erro. A convenção de chamada `x86-interrupt` cuida disso realinhando a pilha neste caso.
Se você estiver interessado em mais detalhes, também temos uma série de postagens que explica a manipulação de exceção usando [funções nuas] vinculadas [no final desta postagem][too-much-magic].
[funções nuas]: https://github.com/rust-lang/rfcs/blob/master/text/1201-naked-fns.md
[too-much-magic]: #muita-magica
## Implementação
Agora que entendemos a teoria, é hora de manipular exceções de CPU em nosso kernel. Começaremos criando um novo módulo interrupts em `src/interrupts.rs`, que primeiro cria uma função `init_idt` que cria uma nova `InterruptDescriptorTable`:
``` rust
// em src/lib.rs
pub mod interrupts;
// em src/interrupts.rs
use x86_64::structures::idt::InterruptDescriptorTable;
pub fn init_idt() {
let mut idt = InterruptDescriptorTable::new();
}
```
Agora podemos adicionar funções manipuladoras. Começamos adicionando um manipulador para a [exceção de breakpoint]. A exceção de breakpoint é a exceção perfeita para testar a manipulação de exceção. Seu único propósito é pausar temporariamente um programa quando a instrução de breakpoint `int3` é executada.
[exceção de breakpoint]: https://wiki.osdev.org/Exceptions#Breakpoint
A exceção de breakpoint é comumente usada em debuggers: Quando o usuário define um breakpoint, o debugger sobrescreve a instrução correspondente com a instrução `int3` para que a CPU lance a exceção de breakpoint quando atinge aquela linha. Quando o usuário quer continuar o programa, o debugger substitui a instrução `int3` pela instrução original novamente e continua o programa. Para mais detalhes, veja a série ["_How debuggers work_"].
["_How debuggers work_"]: https://eli.thegreenplace.net/2011/01/27/how-debuggers-work-part-2-breakpoints
Para nosso caso de uso, não precisamos sobrescrever nenhuma instrução. Em vez disso, queremos apenas imprimir uma mensagem quando a instrução de breakpoint é executada e então continuar o programa. Então vamos criar uma função `breakpoint_handler` simples e adicioná-la à nossa IDT:
```rust
// em src/interrupts.rs
use x86_64::structures::idt::{InterruptDescriptorTable, InterruptStackFrame};
use crate::println;
pub fn init_idt() {
let mut idt = InterruptDescriptorTable::new();
idt.breakpoint.set_handler_fn(breakpoint_handler);
}
extern "x86-interrupt" fn breakpoint_handler(
stack_frame: InterruptStackFrame)
{
println!("EXCEÇÃO: BREAKPOINT\n{:#?}", stack_frame);
}
```
Nosso manipulador apenas produz uma mensagem e imprime de forma bonita o interrupt stack frame.
Quando tentamos compilá-lo, o seguinte erro ocorre:
```
error[E0658]: x86-interrupt ABI is experimental and subject to change (see issue #40180)
--> src/main.rs:53:1
|
53 | / extern "x86-interrupt" fn breakpoint_handler(stack_frame: InterruptStackFrame) {
54 | | println!("EXCEÇÃO: BREAKPOINT\n{:#?}", stack_frame);
55 | | }
| |_^
|
= help: add #![feature(abi_x86_interrupt)] to the crate attributes to enable
```
Este erro ocorre porque a convenção de chamada `x86-interrupt` ainda é instável. Para usá-la de qualquer forma, temos que habilitá-la explicitamente adicionando `#![feature(abi_x86_interrupt)]` no topo do nosso `lib.rs`.
### Carregando a IDT
Para que a CPU use nossa nova tabela de descritores de interrupção, precisamos carregá-la usando a instrução [`lidt`]. A struct `InterruptDescriptorTable` da crate `x86_64` fornece um método [`load`][InterruptDescriptorTable::load] para isso. Vamos tentar usá-lo:
[`lidt`]: https://www.felixcloutier.com/x86/lgdt:lidt
[InterruptDescriptorTable::load]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html#method.load
```rust
// em src/interrupts.rs
pub fn init_idt() {
let mut idt = InterruptDescriptorTable::new();
idt.breakpoint.set_handler_fn(breakpoint_handler);
idt.load();
}
```
Quando tentamos compilar agora, o seguinte erro ocorre:
```
error: `idt` does not live long enough
--> src/interrupts/mod.rs:43:5
|
43 | idt.load();
| ^^^ does not live long enough
44 | }
| - borrowed value only lives until here
|
= note: borrowed value must be valid for the static lifetime...
```
Então o método `load` espera um `&'static self`, isto é, uma referência válida para o tempo de execução completo do programa. A razão é que a CPU acessará esta tabela em cada interrupção até carregarmos uma IDT diferente. Então usar um tempo de vida menor que `'static` poderia levar a bugs de use-after-free.
De fato, isso é exatamente o que acontece aqui. Nossa `idt` é criada na pilha, então ela é válida apenas dentro da função `init`. Depois, a memória da pilha é reutilizada para outras funções, então a CPU interpretaria memória aleatória da pilha como IDT. Felizmente, o método `InterruptDescriptorTable::load` codifica este requisito de tempo de vida em sua definição de função, para que o compilador Rust seja capaz de prevenir este possível bug em tempo de compilação.
Para corrigir este problema, precisamos armazenar nossa `idt` em um lugar onde ela tenha um tempo de vida `'static`. Para conseguir isso, poderíamos alocar nossa IDT no heap usando [`Box`] e então convertê-la para uma referência `'static`, mas estamos escrevendo um kernel de SO e, portanto, não temos um heap (ainda).
[`Box`]: https://doc.rust-lang.org/std/boxed/struct.Box.html
Como alternativa, poderíamos tentar armazenar a IDT como uma `static`:
```rust
static IDT: InterruptDescriptorTable = InterruptDescriptorTable::new();
pub fn init_idt() {
IDT.breakpoint.set_handler_fn(breakpoint_handler);
IDT.load();
}
```
No entanto, há um problema: Statics são imutáveis, então não podemos modificar a entrada de breakpoint da nossa função `init`. Poderíamos resolver este problema usando uma [`static mut`]:
[`static mut`]: https://doc.rust-lang.org/1.30.0/book/second-edition/ch19-01-unsafe-rust.html#accessing-or-modifying-a-mutable-static-variable
```rust
static mut IDT: InterruptDescriptorTable = InterruptDescriptorTable::new();
pub fn init_idt() {
unsafe {
IDT.breakpoint.set_handler_fn(breakpoint_handler);
IDT.load();
}
}
```
Esta variante compila sem erros, mas está longe de ser idiomática. `static mut`s são muito propensas a data races, então precisamos de um [bloco `unsafe`] em cada acesso.
[bloco `unsafe`]: https://doc.rust-lang.org/1.30.0/book/second-edition/ch19-01-unsafe-rust.html#unsafe-superpowers
#### Lazy Statics ao Resgate
Felizmente, a macro `lazy_static` existe. Em vez de avaliar uma `static` em tempo de compilação, a macro realiza a inicialização quando a `static` é referenciada pela primeira vez. Assim, podemos fazer quase tudo no bloco de inicialização e somos até capazes de ler valores de tempo de execução.
Já importamos a crate `lazy_static` quando [criamos uma abstração para o buffer de texto VGA][vga text buffer lazy static]. Então podemos usar diretamente a macro `lazy_static!` para criar nossa IDT estática:
[vga text buffer lazy static]: @/edition-2/posts/03-vga-text-buffer/index.md#lazy-statics
```rust
// em src/interrupts.rs
use lazy_static::lazy_static;
lazy_static! {
static ref IDT: InterruptDescriptorTable = {
let mut idt = InterruptDescriptorTable::new();
idt.breakpoint.set_handler_fn(breakpoint_handler);
idt
};
}
pub fn init_idt() {
IDT.load();
}
```
Note como esta solução não requer blocos `unsafe`. A macro `lazy_static!` usa `unsafe` por trás dos panos, mas é abstraída em uma interface segura.
### Executando
O último passo para fazer exceções funcionarem em nosso kernel é chamar a função `init_idt` do nosso `main.rs`. Em vez de chamá-la diretamente, introduzimos uma função geral `init` em nosso `lib.rs`:
```rust
// em src/lib.rs
pub fn init() {
interrupts::init_idt();
}
```
Com esta função, agora temos um lugar central para rotinas de inicialização que podem ser compartilhadas entre as diferentes funções `_start` em nosso `main.rs`, `lib.rs` e testes de integração.
Agora podemos atualizar a função `_start` do nosso `main.rs` para chamar `init` e então disparar uma exceção de breakpoint:
```rust
// em src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
println!("Olá Mundo{}", "!");
blog_os::init(); // novo
// invoca uma exceção de breakpoint
x86_64::instructions::interrupts::int3(); // novo
// como antes
#[cfg(test)]
test_main();
println!("Não crashou!");
loop {}
}
```
Quando executamos agora no QEMU (usando `cargo run`), vemos o seguinte:
![QEMU printing `EXCEÇÃO: BREAKPOINT` and the interrupt stack frame](qemu-breakpoint-exception.png)
Funciona! A CPU invoca com sucesso nosso manipulador de breakpoint, que imprime a mensagem, e então retorna de volta para a função `_start`, onde a mensagem `Não crashou!` é impressa.
Vemos que o interrupt stack frame nos diz os ponteiros de instrução e pilha no momento em que a exceção ocorreu. Esta informação é muito útil ao depurar exceções inesperadas.
### Adicionando um Teste
Vamos criar um teste que garante que o acima continue funcionando. Primeiro, atualizamos a função `_start` para também chamar `init`:
```rust
// em src/lib.rs
/// Ponto de entrada para `cargo test`
#[cfg(test)]
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
init(); // novo
test_main();
loop {}
}
```
Lembre-se, esta função `_start` é usada quando executamos `cargo test --lib`, já que Rust testa o `lib.rs` completamente independente do `main.rs`. Precisamos chamar `init` aqui para configurar uma IDT antes de executar os testes.
Agora podemos criar um teste `test_breakpoint_exception`:
```rust
// em src/interrupts.rs
#[test_case]
fn test_breakpoint_exception() {
// invoca uma exceção de breakpoint
x86_64::instructions::interrupts::int3();
}
```
O teste invoca a função `int3` para disparar uma exceção de breakpoint. Ao verificar que a execução continua depois, verificamos que nosso manipulador de breakpoint está funcionando corretamente.
Você pode tentar este novo teste executando `cargo test` (todos os testes) ou `cargo test --lib` (apenas testes de `lib.rs` e seus módulos). Você deve ver o seguinte na saída:
```
blog_os::interrupts::test_breakpoint_exception... [ok]
```
## Muita Mágica?
A convenção de chamada `x86-interrupt` e o tipo [`InterruptDescriptorTable`] tornaram o processo de manipulação de exceção relativamente simples e indolor. Se isso foi muita mágica para você e você gostaria de aprender todos os detalhes sórdidos da manipulação de exceção, nós temos você coberto: Nossa série ["Manipulando Exceções com Funções Nuas"] mostra como manipular exceções sem a convenção de chamada `x86-interrupt` e também cria seu próprio tipo IDT. Historicamente, essas postagens eram as principais postagens de manipulação de exceção antes que a convenção de chamada `x86-interrupt` e a crate `x86_64` existissem. Note que essas postagens são baseadas na [primeira edição] deste blog e podem estar desatualizadas.
["Manipulando Exceções com Funções Nuas"]: @/edition-1/extra/naked-exceptions/_index.md
[`InterruptDescriptorTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html
[primeira edição]: @/edition-1/_index.md
## O Que Vem a Seguir?
Capturamos com sucesso nossa primeira exceção e retornamos dela! O próximo passo é garantir que capturemos todas as exceções porque uma exceção não capturada causa um [triple fault] fatal, que leva a uma redefinição do sistema. A próxima postagem explica como podemos evitar isso capturando corretamente [double faults].
[triple fault]: https://wiki.osdev.org/Triple_Fault
[double faults]: https://wiki.osdev.org/Double_Fault#Double_Fault

View File

@@ -0,0 +1,556 @@
+++
title = "Double Faults"
weight = 6
path = "pt-BR/double-fault-exceptions"
date = 2018-06-18
[extra]
chapter = "Interrupções"
# Please update this when updating the translation
translation_based_on_commit = "9753695744854686a6b80012c89b0d850a44b4b0"
# GitHub usernames of the people that translated this post
translators = ["richarddalves"]
+++
Esta postagem explora a exceção de double fault em detalhes, que ocorre quando a CPU falha ao invocar um manipulador de exceção. Ao manipular esta exceção, evitamos _triple faults_ fatais que causam uma redefinição do sistema. Para prevenir triple faults em todos os casos, também configuramos uma _Interrupt Stack Table_ para capturar double faults em uma pilha de kernel separada.
<!-- more -->
Este blog é desenvolvido abertamente no [GitHub]. Se você tiver algum problema ou dúvida, abra um issue lá. Você também pode deixar comentários [na parte inferior]. O código-fonte completo desta publicação pode ser encontrado na branch [`post-06`][post branch].
[GitHub]: https://github.com/phil-opp/blog_os
[na parte inferior]: #comments
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
[post branch]: https://github.com/phil-opp/blog_os/tree/post-06
<!-- toc -->
## O Que é um Double Fault?
Em termos simplificados, um double fault é uma exceção especial que ocorre quando a CPU falha ao invocar um manipulador de exceção. Por exemplo, ele ocorre quando um page fault é disparado mas não há manipulador de page fault registrado na [Tabela de Descritores de Interrupção][IDT] (IDT). Então é meio similar aos blocos catch-all em linguagens de programação com exceções, por exemplo, `catch(...)` em C++ ou `catch(Exception e)` em Java ou C#.
[IDT]: @/edition-2/posts/05-cpu-exceptions/index.md#the-interrupt-descriptor-table
Um double fault se comporta como uma exceção normal. Ele tem o número de vetor `8` e podemos definir uma função manipuladora normal para ele na IDT. É realmente importante fornecer um manipulador de double fault, porque se um double fault não é manipulado, ocorre um _triple fault_ fatal. Triple faults não podem ser capturados, e a maioria do hardware reage com uma redefinição do sistema.
### Disparando um Double Fault
Vamos provocar um double fault disparando uma exceção para a qual não definimos uma função manipuladora:
```rust
// em src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
println!("Olá Mundo{}", "!");
blog_os::init();
// dispara um page fault
unsafe {
*(0xdeadbeef as *mut u8) = 42;
};
// como antes
#[cfg(test)]
test_main();
println!("Não crashou!");
loop {}
}
```
Usamos `unsafe` para escrever no endereço inválido `0xdeadbeef`. O endereço virtual não está mapeado para um endereço físico nas tabelas de página, então ocorre um page fault. Não registramos um manipulador de page fault em nossa [IDT], então ocorre um double fault.
Quando iniciamos nosso kernel agora, vemos que ele entra em um loop de boot infinito. A razão para o loop de boot é a seguinte:
1. A CPU tenta escrever em `0xdeadbeef`, o que causa um page fault.
2. A CPU olha para a entrada correspondente na IDT e vê que nenhuma função manipuladora está especificada. Assim, ela não pode chamar o manipulador de page fault e ocorre um double fault.
3. A CPU olha para a entrada IDT do manipulador de double fault, mas esta entrada também não especifica uma função manipuladora. Assim, ocorre um _triple_ fault.
4. Um triple fault é fatal. QEMU reage a ele como a maioria do hardware real e emite uma redefinição do sistema.
Então, para prevenir este triple fault, precisamos fornecer uma função manipuladora para page faults ou um manipulador de double fault. Queremos evitar triple faults em todos os casos, então vamos começar com um manipulador de double fault que é invocado para todos os tipos de exceção não manipulados.
## Um Manipulador de Double Fault
Um double fault é uma exceção normal com um código de erro, então podemos especificar uma função manipuladora similar ao nosso manipulador de breakpoint:
```rust
// em src/interrupts.rs
lazy_static! {
static ref IDT: InterruptDescriptorTable = {
let mut idt = InterruptDescriptorTable::new();
idt.breakpoint.set_handler_fn(breakpoint_handler);
idt.double_fault.set_handler_fn(double_fault_handler); // novo
idt
};
}
// novo
extern "x86-interrupt" fn double_fault_handler(
stack_frame: InterruptStackFrame, _error_code: u64) -> !
{
panic!("EXCEÇÃO: DOUBLE FAULT\n{:#?}", stack_frame);
}
```
Nosso manipulador imprime uma mensagem de erro curta e despeja o exception stack frame. O código de erro do manipulador de double fault é sempre zero, então não há razão para imprimi-lo. Uma diferença para o manipulador de breakpoint é que o manipulador de double fault é [_divergente_]. A razão é que a arquitetura `x86_64` não permite retornar de uma exceção de double fault.
[_divergente_]: https://doc.rust-lang.org/stable/rust-by-example/fn/diverging.html
Quando iniciamos nosso kernel agora, devemos ver que o manipulador de double fault é invocado:
![QEMU printing `EXCEÇÃO: DOUBLE FAULT` and the exception stack frame](qemu-catch-double-fault.png)
Funcionou! Aqui está o que aconteceu desta vez:
1. A CPU tenta escrever em `0xdeadbeef`, o que causa um page fault.
2. Como antes, a CPU olha para a entrada correspondente na IDT e vê que nenhuma função manipuladora está definida. Assim, ocorre um double fault.
3. A CPU pula para o manipulador de double fault agora presente.
O triple fault (e o loop de boot) não ocorre mais, já que a CPU agora pode chamar o manipulador de double fault.
Isso foi bem direto! Então por que precisamos de uma postagem inteira para este tópico? Bem, agora somos capazes de capturar a _maioria_ dos double faults, mas há alguns casos onde nossa abordagem atual não é suficiente.
## Causas de Double Faults
Antes de olharmos para os casos especiais, precisamos conhecer as causas exatas de double faults. Acima, usamos uma definição bem vaga:
> Um double fault é uma exceção especial que ocorre quando a CPU falha ao invocar um manipulador de exceção.
O que _"falha ao invocar"_ significa exatamente? O manipulador não está presente? O manipulador está [trocado para fora]? E o que acontece se um manipulador causa exceções ele mesmo?
[trocado para fora]: http://pages.cs.wisc.edu/~remzi/OSTEP/vm-beyondphys.pdf
Por exemplo, o que acontece se:
1. uma exceção de breakpoint ocorre, mas a função manipuladora correspondente está trocada para fora?
2. um page fault ocorre, mas o manipulador de page fault está trocado para fora?
3. um manipulador de divide-by-zero causa uma exceção de breakpoint, mas o manipulador de breakpoint está trocado para fora?
4. nosso kernel estoura sua pilha e a _guard page_ é atingida?
Felizmente, o manual AMD64 ([PDF][AMD64 manual]) tem uma definição exata (na Seção 8.2.9). De acordo com ele, uma "exceção de double fault _pode_ ocorrer quando uma segunda exceção ocorre durante a manipulação de um manipulador de exceção anterior (primeira)". O _"pode"_ é importante: Apenas combinações muito específicas de exceções levam a um double fault. Essas combinações são:
| Primeira Exceção | Segunda Exceção |
| --------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| [Divide-by-zero],<br>[Invalid TSS],<br>[Segment Not Present],<br>[Stack-Segment Fault],<br>[General Protection Fault] | [Invalid TSS],<br>[Segment Not Present],<br>[Stack-Segment Fault],<br>[General Protection Fault] |
| [Page Fault] | [Page Fault],<br>[Invalid TSS],<br>[Segment Not Present],<br>[Stack-Segment Fault],<br>[General Protection Fault] |
[Divide-by-zero]: https://wiki.osdev.org/Exceptions#Division_Error
[Invalid TSS]: https://wiki.osdev.org/Exceptions#Invalid_TSS
[Segment Not Present]: https://wiki.osdev.org/Exceptions#Segment_Not_Present
[Stack-Segment Fault]: https://wiki.osdev.org/Exceptions#Stack-Segment_Fault
[General Protection Fault]: https://wiki.osdev.org/Exceptions#General_Protection_Fault
[Page Fault]: https://wiki.osdev.org/Exceptions#Page_Fault
[AMD64 manual]: https://www.amd.com/system/files/TechDocs/24593.pdf
Então, por exemplo, uma falha de divide-by-zero seguida de um page fault está ok (o manipulador de page fault é invocado), mas uma falha de divide-by-zero seguida de um general-protection fault leva a um double fault.
Com a ajuda desta tabela, podemos responder às primeiras três das questões acima:
1. Se uma exceção de breakpoint ocorre e a função manipuladora correspondente está trocada para fora, ocorre um _page fault_ e o _manipulador de page fault_ é invocado.
2. Se um page fault ocorre e o manipulador de page fault está trocado para fora, ocorre um _double fault_ e o _manipulador de double fault_ é invocado.
3. Se um manipulador de divide-by-zero causa uma exceção de breakpoint, a CPU tenta invocar o manipulador de breakpoint. Se o manipulador de breakpoint está trocado para fora, ocorre um _page fault_ e o _manipulador de page fault_ é invocado.
De fato, até o caso de uma exceção sem função manipuladora na IDT segue este esquema: Quando a exceção ocorre, a CPU tenta ler a entrada IDT correspondente. Como a entrada é 0, que não é uma entrada IDT válida, ocorre um _general protection fault_. Não definimos uma função manipuladora para o general protection fault também, então outro general protection fault ocorre. De acordo com a tabela, isso leva a um double fault.
### Kernel Stack Overflow
Vamos olhar para a quarta questão:
> O que acontece se nosso kernel estoura sua pilha e a guard page é atingida?
Uma guard page é uma página de memória especial na parte inferior de uma pilha que torna possível detectar estouros de pilha. A página não está mapeada para nenhum frame físico, então acessá-la causa um page fault em vez de silenciosamente corromper outra memória. O bootloader configura uma guard page para nossa pilha de kernel, então um stack overflow causa um _page fault_.
Quando um page fault ocorre, a CPU olha para o manipulador de page fault na IDT e tenta empurrar o [interrupt stack frame] na pilha. No entanto, o ponteiro de pilha atual ainda aponta para a guard page não presente. Assim, ocorre um segundo page fault, que causa um double fault (de acordo com a tabela acima).
[interrupt stack frame]: @/edition-2/posts/05-cpu-exceptions/index.md#the-interrupt-stack-frame
Então a CPU tenta chamar o _manipulador de double fault_ agora. No entanto, em um double fault, a CPU tenta empurrar o exception stack frame também. O ponteiro de pilha ainda aponta para a guard page, então ocorre um _terceiro_ page fault, que causa um _triple fault_ e uma reinicialização do sistema. Então nosso manipulador de double fault atual não pode evitar um triple fault neste caso.
Vamos tentar nós mesmos! Podemos facilmente provocar um kernel stack overflow chamando uma função que recursa infinitamente:
```rust
// em src/main.rs
#[unsafe(no_mangle)] // não altere (mangle) o nome desta função
pub extern "C" fn _start() -> ! {
println!("Olá Mundo{}", "!");
blog_os::init();
fn stack_overflow() {
stack_overflow(); // para cada recursão, o endereço de retorno é empurrado
}
// dispara um stack overflow
stack_overflow();
[] // test_main(), println(…), e loop {}
}
```
Quando tentamos este código no QEMU, vemos que o sistema entra em um bootloop novamente.
Então como podemos evitar este problema? Não podemos omitir o empurrar do exception stack frame, já que a própria CPU faz isso. Então precisamos garantir de alguma forma que a pilha esteja sempre válida quando uma exceção de double fault ocorre. Felizmente, a arquitetura x86_64 tem uma solução para este problema.
## Trocando Pilhas
A arquitetura x86_64 é capaz de trocar para uma pilha predefinida e conhecida como boa quando uma exceção ocorre. Esta troca acontece em nível de hardware, então pode ser realizada antes que a CPU empurre o exception stack frame.
O mecanismo de troca é implementado como uma _Interrupt Stack Table_ (IST). A IST é uma tabela de 7 ponteiros para pilhas conhecidas como boas. Em pseudocódigo similar a Rust:
```rust
struct InterruptStackTable {
stack_pointers: [Option<StackPointer>; 7],
}
```
Para cada manipulador de exceção, podemos escolher uma pilha da IST através do campo `stack_pointers` na [entrada IDT] correspondente. Por exemplo, nosso manipulador de double fault poderia usar a primeira pilha na IST. Então a CPU automaticamente troca para esta pilha sempre que ocorre um double fault. Esta troca aconteceria antes de qualquer coisa ser empurrada, prevenindo o triple fault.
[entrada IDT]: @/edition-2/posts/05-cpu-exceptions/index.md#the-interrupt-descriptor-table
### A IST e TSS
A Interrupt Stack Table (IST) é parte de uma antiga estrutura legada chamada _[Task State Segment]_ \(TSS). A TSS costumava armazenar várias informações (por exemplo, estado de registradores do processador) sobre uma tarefa no modo de 32 bits e era, por exemplo, usada para [troca de contexto de hardware]. No entanto, a troca de contexto de hardware não é mais suportada no modo de 64 bits e o formato da TSS mudou completamente.
[Task State Segment]: https://en.wikipedia.org/wiki/Task_state_segment
[troca de contexto de hardware]: https://wiki.osdev.org/Context_Switching#Hardware_Context_Switching
No x86_64, a TSS não armazena mais nenhuma informação específica de tarefa. Em vez disso, ela armazena duas tabelas de pilha (a IST é uma delas). O único campo comum entre a TSS de 32 bits e 64 bits é o ponteiro para o [bitmap de permissões de porta I/O].
[bitmap de permissões de porta I/O]: https://en.wikipedia.org/wiki/Task_state_segment#I.2FO_port_permissions
A TSS de 64 bits tem o seguinte formato:
| Campo | Tipo |
| -------------------------------------------- | ---------- |
| <span style="opacity: 0.5">(reservado)</span> | `u32` |
| Privilege Stack Table | `[u64; 3]` |
| <span style="opacity: 0.5">(reservado)</span> | `u64` |
| Interrupt Stack Table | `[u64; 7]` |
| <span style="opacity: 0.5">(reservado)</span> | `u64` |
| <span style="opacity: 0.5">(reservado)</span> | `u16` |
| I/O Map Base Address | `u16` |
A _Privilege Stack Table_ é usada pela CPU quando o nível de privilégio muda. Por exemplo, se uma exceção ocorre enquanto a CPU está em modo usuário (nível de privilégio 3), a CPU normalmente troca para o modo kernel (nível de privilégio 0) antes de invocar o manipulador de exceção. Nesse caso, a CPU trocaria para a 0ª pilha na Privilege Stack Table (já que 0 é o nível de privilégio alvo). Ainda não temos nenhum programa em modo usuário, então ignoraremos esta tabela por enquanto.
### Criando uma TSS
Vamos criar uma nova TSS que contém uma pilha de double fault separada em sua interrupt stack table. Para isso, precisamos de uma struct TSS. Felizmente, a crate `x86_64` já contém uma [struct `TaskStateSegment`] que podemos usar.
[struct `TaskStateSegment`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/tss/struct.TaskStateSegment.html
Criamos a TSS em um novo módulo `gdt` (o nome fará sentido mais tarde):
```rust
// em src/lib.rs
pub mod gdt;
// em src/gdt.rs
use x86_64::VirtAddr;
use x86_64::structures::tss::TaskStateSegment;
use lazy_static::lazy_static;
pub const DOUBLE_FAULT_IST_INDEX: u16 = 0;
lazy_static! {
static ref TSS: TaskStateSegment = {
let mut tss = TaskStateSegment::new();
tss.interrupt_stack_table[DOUBLE_FAULT_IST_INDEX as usize] = {
const STACK_SIZE: usize = 4096 * 5;
static mut STACK: [u8; STACK_SIZE] = [0; STACK_SIZE];
let stack_start = VirtAddr::from_ptr(&raw const STACK);
let stack_end = stack_start + STACK_SIZE;
stack_end
};
tss
};
}
```
Usamos `lazy_static` porque o avaliador const de Rust ainda não é poderoso o suficiente para fazer esta inicialização em tempo de compilação. Definimos que a 0ª entrada IST é a pilha de double fault (qualquer outro índice IST funcionaria também). Então escrevemos o endereço superior de uma pilha de double fault na 0ª entrada. Escrevemos o endereço superior porque pilhas no x86 crescem para baixo, isto é, de endereços altos para endereços baixos.
Ainda não implementamos gerenciamento de memória, então não temos uma forma apropriada de alocar uma nova pilha. Em vez disso, usamos um array `static mut` como armazenamento de pilha por enquanto. É importante que seja uma `static mut` e não uma `static` imutável, porque caso contrário o bootloader a mapearia para uma página somente leitura. Substituiremos isto por uma alocação de pilha apropriada em uma postagem posterior.
Note que esta pilha de double fault não tem guard page que proteja contra stack overflow. Isso significa que não devemos fazer nada intensivo em pilha em nosso manipulador de double fault porque um stack overflow poderia corromper a memória abaixo da pilha.
#### Carregando a TSS
Agora que criamos uma nova TSS, precisamos de uma forma de dizer à CPU que ela deve usá-la. Infelizmente, isto é um pouco trabalhoso, já que a TSS usa o sistema de segmentação (por razões históricas). Em vez de carregar a tabela diretamente, precisamos adicionar um novo descritor de segmento à [Tabela de Descritores Globais] \(GDT). Então podemos carregar nossa TSS invocando a [instrução `ltr`] com o respectivo índice GDT. (Esta é a razão pela qual nomeamos nosso módulo `gdt`.)
[Tabela de Descritores Globais]: https://web.archive.org/web/20190217233448/https://www.flingos.co.uk/docs/reference/Global-Descriptor-Table/
[instrução `ltr`]: https://www.felixcloutier.com/x86/ltr
### A Tabela de Descritores Globais
A Tabela de Descritores Globais (GDT - Global Descriptor Table) é uma relíquia que foi usada para [segmentação de memória] antes de paginação se tornar o padrão de fato. No entanto, ela ainda é necessária no modo de 64 bits para várias coisas, como configuração de modo kernel/usuário ou carregamento de TSS.
[segmentação de memória]: https://en.wikipedia.org/wiki/X86_memory_segmentation
A GDT é uma estrutura que contém os _segmentos_ do programa. Ela foi usada em arquiteturas mais antigas para isolar programas uns dos outros antes de paginação se tornar o padrão. Para mais informações sobre segmentação, confira o capítulo de mesmo nome do livro gratuito ["Three Easy Pieces"]. Embora a segmentação não seja mais suportada no modo de 64 bits, a GDT ainda existe. Ela é usada principalmente para duas coisas: Trocar entre espaço de kernel e espaço de usuário, e carregar uma estrutura TSS.
["Three Easy Pieces"]: http://pages.cs.wisc.edu/~remzi/OSTEP/
#### Criando uma GDT
Vamos criar uma `GDT` estática que inclui um segmento para nossa `TSS` estática:
```rust
// em src/gdt.rs
use x86_64::structures::gdt::{GlobalDescriptorTable, Descriptor};
lazy_static! {
static ref GDT: GlobalDescriptorTable = {
let mut gdt = GlobalDescriptorTable::new();
gdt.add_entry(Descriptor::kernel_code_segment());
gdt.add_entry(Descriptor::tss_segment(&TSS));
gdt
};
}
```
Como antes, usamos `lazy_static` novamente. Criamos uma nova GDT com um segmento de código e um segmento TSS.
#### Carregando a GDT
Para carregar nossa GDT, criamos uma nova função `gdt::init` que chamamos de nossa função `init`:
```rust
// em src/gdt.rs
pub fn init() {
GDT.load();
}
// em src/lib.rs
pub fn init() {
gdt::init();
interrupts::init_idt();
}
```
Agora nossa GDT está carregada (já que a função `_start` chama `init`), mas ainda vemos o loop de boot no stack overflow.
### Os Passos Finais
O problema é que os segmentos GDT ainda não estão ativos porque os registradores de segmento e TSS ainda contêm os valores da GDT antiga. Também precisamos modificar a entrada IDT de double fault para que ela use a nova pilha.
Em resumo, precisamos fazer o seguinte:
1. **Recarregar registrador de segmento de código**: Mudamos nossa GDT, então devemos recarregar `cs`, o registrador de segmento de código. Isso é necessário já que o antigo seletor de segmento poderia agora apontar para um descritor GDT diferente (por exemplo, um descritor TSS).
2. **Carregar a TSS**: Carregamos uma GDT que contém um seletor TSS, mas ainda precisamos dizer à CPU que ela deve usar essa TSS.
3. **Atualizar a entrada IDT**: Assim que nossa TSS é carregada, a CPU tem acesso a uma interrupt stack table (IST) válida. Então podemos dizer à CPU que ela deve usar nossa nova pilha de double fault modificando nossa entrada IDT de double fault.
Para os dois primeiros passos, precisamos de acesso às variáveis `code_selector` e `tss_selector` em nossa função `gdt::init`. Podemos conseguir isso tornando-as parte da static através de uma nova struct `Selectors`:
```rust
// em src/gdt.rs
use x86_64::structures::gdt::SegmentSelector;
lazy_static! {
static ref GDT: (GlobalDescriptorTable, Selectors) = {
let mut gdt = GlobalDescriptorTable::new();
let code_selector = gdt.add_entry(Descriptor::kernel_code_segment());
let tss_selector = gdt.add_entry(Descriptor::tss_segment(&TSS));
(gdt, Selectors { code_selector, tss_selector })
};
}
struct Selectors {
code_selector: SegmentSelector,
tss_selector: SegmentSelector,
}
```
Agora podemos usar os seletores para recarregar o registrador `cs` e carregar nossa `TSS`:
```rust
// em src/gdt.rs
pub fn init() {
use x86_64::instructions::tables::load_tss;
use x86_64::instructions::segmentation::{CS, Segment};
GDT.0.load();
unsafe {
CS::set_reg(GDT.1.code_selector);
load_tss(GDT.1.tss_selector);
}
}
```
Recarregamos o registrador de segmento de código usando [`CS::set_reg`] e carregamos a TSS usando [`load_tss`]. As funções são marcadas como `unsafe`, então precisamos de um bloco `unsafe` para invocá-las. A razão é que pode ser possível quebrar a segurança de memória carregando seletores inválidos.
[`CS::set_reg`]: https://docs.rs/x86_64/0.14.5/x86_64/instructions/segmentation/struct.CS.html#method.set_reg
[`load_tss`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/tables/fn.load_tss.html
Agora que carregamos uma TSS e interrupt stack table válidas, podemos definir o índice de pilha para nosso manipulador de double fault na IDT:
```rust
// em src/interrupts.rs
use crate::gdt;
lazy_static! {
static ref IDT: InterruptDescriptorTable = {
let mut idt = InterruptDescriptorTable::new();
idt.breakpoint.set_handler_fn(breakpoint_handler);
unsafe {
idt.double_fault.set_handler_fn(double_fault_handler)
.set_stack_index(gdt::DOUBLE_FAULT_IST_INDEX); // novo
}
idt
};
}
```
O método `set_stack_index` é unsafe porque o chamador deve garantir que o índice usado é válido e não está já usado para outra exceção.
É isso! Agora a CPU deve trocar para a pilha de double fault sempre que ocorre um double fault. Assim, somos capazes de capturar _todos_ os double faults, incluindo kernel stack overflows:
![QEMU printing `EXCEÇÃO: DOUBLE FAULT` and a dump of the exception stack frame](qemu-double-fault-on-stack-overflow.png)
De agora em diante, nunca devemos ver um triple fault novamente! Para garantir que não quebramos acidentalmente o acima, devemos adicionar um teste para isso.
## Um Teste de Stack Overflow
Para testar nosso novo módulo `gdt` e garantir que o manipulador de double fault é corretamente chamado em um stack overflow, podemos adicionar um teste de integração. A ideia é provocar um double fault na função de teste e verificar que o manipulador de double fault é chamado.
Vamos começar com um esqueleto mínimo:
```rust
// em tests/stack_overflow.rs
#![no_std]
#![no_main]
use core::panic::PanicInfo;
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
unimplemented!();
}
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
blog_os::test_panic_handler(info)
}
```
Como nosso teste `panic_handler`, o teste executará [sem um test harness]. A razão é que não podemos continuar a execução após um double fault, então mais de um teste não faz sentido. Para desativar o test harness para o teste, adicionamos o seguinte ao nosso `Cargo.toml`:
```toml
# em Cargo.toml
[[test]]
name = "stack_overflow"
harness = false
```
[sem um test harness]: @/edition-2/posts/04-testing/index.md#no-harness-tests
Agora `cargo test --test stack_overflow` deve compilar com sucesso. O teste falha, é claro, já que a macro `unimplemented` entra em panic.
### Implementando `_start`
A implementação da função `_start` se parece com isto:
```rust
// em tests/stack_overflow.rs
use blog_os::serial_print;
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
serial_print!("stack_overflow::stack_overflow...\t");
blog_os::gdt::init();
init_test_idt();
// dispara um stack overflow
stack_overflow();
panic!("Execução continuou após stack overflow");
}
#[allow(unconditional_recursion)]
fn stack_overflow() {
stack_overflow(); // para cada recursão, o endereço de retorno é empurrado
volatile::Volatile::new(0).read(); // previne otimizações de tail recursion
}
```
Chamamos nossa função `gdt::init` para inicializar uma nova GDT. Em vez de chamar nossa função `interrupts::init_idt`, chamamos uma função `init_test_idt` que será explicada em um momento. A razão é que queremos registrar um manipulador de double fault customizado que faz um `exit_qemu(QemuExitCode::Success)` em vez de entrar em panic.
A função `stack_overflow` é quase idêntica à função em nosso `main.rs`. A única diferença é que no final da função, realizamos uma leitura [volátil] adicional usando o tipo [`Volatile`] para prevenir uma otimização do compilador chamada [_tail call elimination_]. Entre outras coisas, esta otimização permite ao compilador transformar uma função cuja última instrução é uma chamada de função recursiva em um loop normal. Assim, nenhum stack frame adicional é criado para a chamada de função, então o uso de pilha permanece constante.
[volátil]: https://en.wikipedia.org/wiki/Volatile_(computer_programming)
[`Volatile`]: https://docs.rs/volatile/0.2.6/volatile/struct.Volatile.html
[_tail call elimination_]: https://en.wikipedia.org/wiki/Tail_call
No nosso caso, no entanto, queremos que o stack overflow aconteça, então adicionamos uma instrução de leitura volátil fictícia no final da função, que o compilador não tem permissão para remover. Assim, a função não é mais _tail recursive_, e a transformação em um loop é prevenida. Também adicionamos o atributo `allow(unconditional_recursion)` para silenciar o aviso do compilador de que a função recursa infinitamente.
### A IDT de Teste
Como notado acima, o teste precisa de sua própria IDT com um manipulador de double fault customizado. A implementação se parece com isto:
```rust
// em tests/stack_overflow.rs
use lazy_static::lazy_static;
use x86_64::structures::idt::InterruptDescriptorTable;
lazy_static! {
static ref TEST_IDT: InterruptDescriptorTable = {
let mut idt = InterruptDescriptorTable::new();
unsafe {
idt.double_fault
.set_handler_fn(test_double_fault_handler)
.set_stack_index(blog_os::gdt::DOUBLE_FAULT_IST_INDEX);
}
idt
};
}
pub fn init_test_idt() {
TEST_IDT.load();
}
```
A implementação é muito similar à nossa IDT normal em `interrupts.rs`. Como na IDT normal, definimos um índice de pilha na IST para o manipulador de double fault para trocar para uma pilha separada. A função `init_test_idt` carrega a IDT na CPU através do método `load`.
### O Manipulador de Double Fault
A única peça que falta é nosso manipulador de double fault. Ele se parece com isto:
```rust
// em tests/stack_overflow.rs
use blog_os::{exit_qemu, QemuExitCode, serial_println};
use x86_64::structures::idt::InterruptStackFrame;
extern "x86-interrupt" fn test_double_fault_handler(
_stack_frame: InterruptStackFrame,
_error_code: u64,
) -> ! {
serial_println!("[ok]");
exit_qemu(QemuExitCode::Success);
loop {}
}
```
Quando o manipulador de double fault é chamado, saímos do QEMU com um código de saída de sucesso, o que marca o teste como aprovado. Como testes de integração são executáveis completamente separados, precisamos definir o atributo `#![feature(abi_x86_interrupt)]` novamente no topo do nosso arquivo de teste.
Agora podemos executar nosso teste através de `cargo test --test stack_overflow` (ou `cargo test` para executar todos os testes). Como esperado, vemos a saída `stack_overflow... [ok]` no console. Tente comentar a linha `set_stack_index`; isso deve fazer o teste falhar.
## Resumo
Nesta postagem, aprendemos o que é um double fault e sob quais condições ele ocorre. Adicionamos um manipulador de double fault básico que imprime uma mensagem de erro e adicionamos um teste de integração para ele.
Também habilitamos a troca de pilha suportada por hardware em exceções de double fault para que também funcione em stack overflow. Enquanto implementávamos isso, aprendemos sobre o segmento de estado de tarefa (TSS), a interrupt stack table (IST) contida nele, e a tabela de descritores globais (GDT), que foi usada para segmentação em arquiteturas mais antigas.
## O Que Vem a Seguir?
A próxima postagem explica como manipular interrupções de dispositivos externos como temporizadores, teclados ou controladores de rede. Essas interrupções de hardware são muito similares a exceções, por exemplo, elas também são despachadas através da IDT. No entanto, ao contrário de exceções, elas não surgem diretamente na CPU. Em vez disso, um _controlador de interrupção_ agrega essas interrupções e as encaminha para a CPU dependendo de sua prioridade. Na próxima postagem, exploraremos o controlador de interrupções [Intel 8259] \("PIC") e aprenderemos como implementar suporte a teclado.
[Intel 8259]: https://en.wikipedia.org/wiki/Intel_8259

View File

@@ -0,0 +1,740 @@
+++
title = "Interrupções de Hardware"
weight = 7
path = "pt-BR/hardware-interrupts"
date = 2018-10-22
[extra]
chapter = "Interrupções"
# Please update this when updating the translation
translation_based_on_commit = "9753695744854686a6b80012c89b0d850a44b4b0"
# GitHub usernames of the people that translated this post
translators = ["richarddalves"]
+++
Nesta postagem, configuramos o controlador de interrupção programável para encaminhar corretamente interrupções de hardware para a CPU. Para manipular essas interrupções, adicionamos novas entradas à nossa tabela de descritores de interrupção, assim como fizemos para nossos manipuladores de exceção. Aprenderemos como obter interrupções periódicas de timer e como obter entrada do teclado.
<!-- more -->
Este blog é desenvolvido abertamente no [GitHub]. Se você tiver algum problema ou dúvida, abra um issue lá. Você também pode deixar comentários [na parte inferior]. O código-fonte completo desta publicação pode ser encontrado na branch [`post-07`][post branch].
[GitHub]: https://github.com/phil-opp/blog_os
[na parte inferior]: #comments
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
[post branch]: https://github.com/phil-opp/blog_os/tree/post-07
<!-- toc -->
## Visão Geral
Interrupções fornecem uma forma de notificar a CPU de dispositivos de hardware conectados. Então, em vez de deixar o kernel verificar periodicamente o teclado por novos caracteres (um processo chamado [_polling_]), o teclado pode notificar o kernel de cada pressionamento de tecla. Isso é muito mais eficiente porque o kernel só precisa agir quando algo aconteceu. Também permite tempos de reação mais rápidos, já que o kernel pode reagir imediatamente e não apenas na próxima verificação.
[_polling_]: https://en.wikipedia.org/wiki/Polling_(computer_science)
Conectar todos os dispositivos de hardware diretamente à CPU não é possível. Em vez disso, um _controlador de interrupção_ separado agrega as interrupções de todos os dispositivos e então notifica a CPU:
```
____________ _____
Timer ------------> | | | |
Teclado ----------> | Controlador|---------> | CPU |
Outro Hardware ---> | de | |_____|
Etc. -------------> | Interrupção|
|____________|
```
A maioria dos controladores de interrupção são programáveis, o que significa que suportam diferentes níveis de prioridade para interrupções. Por exemplo, isso permite dar às interrupções de timer uma prioridade mais alta que as interrupções de teclado para garantir cronometragem precisa.
Ao contrário de exceções, interrupções de hardware ocorrem _assincronamente_. Isso significa que são completamente independentes do código executado e podem ocorrer a qualquer momento. Assim, temos repentinamente uma forma de concorrência em nosso kernel com todos os potenciais bugs relacionados à concorrência. O modelo estrito de ownership de Rust nos ajuda aqui porque proíbe estado global mutável. No entanto, deadlocks ainda são possíveis, como veremos mais tarde nesta postagem.
## O 8259 PIC
O [Intel 8259] é um controlador de interrupção programável (PIC) introduzido em 1976. Ele foi há muito tempo substituído pelo mais novo [APIC], mas sua interface ainda é suportada em sistemas atuais por razões de compatibilidade retroativa. O 8259 PIC é significativamente mais fácil de configurar que o APIC, então o usaremos para nos introduzir a interrupções antes de mudarmos para o APIC em uma postagem posterior.
[APIC]: https://en.wikipedia.org/wiki/Intel_APIC_Architecture
O 8259 tem oito linhas de interrupção e várias linhas para se comunicar com a CPU. Os sistemas típicos daquela época eram equipados com duas instâncias do 8259 PIC, um PIC primário e um secundário, conectado a uma das linhas de interrupção do primário:
[Intel 8259]: https://en.wikipedia.org/wiki/Intel_8259
```
____________ ____________
Real Time Clock --> | | Timer -------------> | |
ACPI -------------> | | Teclado-----------> | | _____
Disponível -------> | Controlador|----------------------> | Controlador| | |
Disponível -------> | de | Porta Serial 2 ----> | de |---> | CPU |
Mouse ------------> | Interrupção| Porta Serial 1 ----> | Interrupção| |_____|
Co-Processador ---> | Secundário | Porta Paralela 2/3 > | Primário |
ATA Primário -----> | | Disquete ---------> | |
ATA Secundário ---> |____________| Porta Paralela 1---> |____________|
```
Este gráfico mostra a atribuição típica de linhas de interrupção. Vemos que a maioria das 15 linhas têm um mapeamento fixo, por exemplo, a linha 4 do PIC secundário é atribuída ao mouse.
Cada controlador pode ser configurado através de duas [portas I/O], uma porta "comando" e uma porta "dados". Para o controlador primário, essas portas são `0x20` (comando) e `0x21` (dados). Para o controlador secundário, elas são `0xa0` (comando) e `0xa1` (dados). Para mais informações sobre como os PICs podem ser configurados, veja o [artigo em osdev.org].
[portas I/O]: @/edition-2/posts/04-testing/index.md#i-o-ports
[artigo em osdev.org]: https://wiki.osdev.org/8259_PIC
### Implementação
A configuração padrão dos PICs não é utilizável porque envia números de vetor de interrupção no intervalo de 015 para a CPU. Esses números já estão ocupados por exceções de CPU. Por exemplo, o número 8 corresponde a um double fault. Para corrigir esse problema de sobreposição, precisamos remapear as interrupções PIC para números diferentes. O intervalo real não importa desde que não se sobreponha às exceções, mas tipicamente o intervalo de 3247 é escolhido, porque esses são os primeiros números livres após os 32 slots de exceção.
A configuração acontece escrevendo valores especiais nas portas de comando e dados dos PICs. Felizmente, já existe uma crate chamada [`pic8259`], então não precisamos escrever a sequência de inicialização nós mesmos. No entanto, se você estiver interessado em como funciona, confira [seu código-fonte][pic crate source]. Ele é bastante pequeno e bem documentado.
[pic crate source]: https://docs.rs/crate/pic8259/0.10.1/source/src/lib.rs
Para adicionar a crate como dependência, adicionamos o seguinte ao nosso projeto:
[`pic8259`]: https://docs.rs/pic8259/0.10.1/pic8259/
```toml
# em Cargo.toml
[dependencies]
pic8259 = "0.10.1"
```
A principal abstração fornecida pela crate é a struct [`ChainedPics`] que representa o layout primário/secundário de PIC que vimos acima. Ela é projetada para ser usada da seguinte forma:
[`ChainedPics`]: https://docs.rs/pic8259/0.10.1/pic8259/struct.ChainedPics.html
```rust
// em src/interrupts.rs
use pic8259::ChainedPics;
use spin;
pub const PIC_1_OFFSET: u8 = 32;
pub const PIC_2_OFFSET: u8 = PIC_1_OFFSET + 8;
pub static PICS: spin::Mutex<ChainedPics> =
spin::Mutex::new(unsafe { ChainedPics::new(PIC_1_OFFSET, PIC_2_OFFSET) });
```
Como notado acima, estamos definindo os offsets para os PICs no intervalo 3247. Ao envolver a struct `ChainedPics` em um `Mutex`, obtemos acesso mutável seguro (através do [método `lock`][spin mutex lock]), que precisamos no próximo passo. A função `ChainedPics::new` é unsafe porque offsets errados poderiam causar comportamento indefinido.
[spin mutex lock]: https://docs.rs/spin/0.5.2/spin/struct.Mutex.html#method.lock
Agora podemos inicializar o 8259 PIC em nossa função `init`:
```rust
// em src/lib.rs
pub fn init() {
gdt::init();
interrupts::init_idt();
unsafe { interrupts::PICS.lock().initialize() }; // novo
}
```
Usamos a função [`initialize`] para realizar a inicialização do PIC. Como a função `ChainedPics::new`, esta função também é unsafe porque pode causar comportamento indefinido se o PIC estiver mal configurado.
[`initialize`]: https://docs.rs/pic8259/0.10.1/pic8259/struct.ChainedPics.html#method.initialize
Se tudo correr bem, devemos continuar a ver a mensagem "Não crashou!" ao executar `cargo run`.
## Habilitando Interrupções
Até agora, nada aconteceu porque as interrupções ainda estão desativadas na configuração da CPU. Isso significa que a CPU não escuta o controlador de interrupção de forma alguma, então nenhuma interrupção pode chegar à CPU. Vamos mudar isso:
```rust
// em src/lib.rs
pub fn init() {
gdt::init();
interrupts::init_idt();
unsafe { interrupts::PICS.lock().initialize() };
x86_64::instructions::interrupts::enable(); // novo
}
```
A função `interrupts::enable` da crate `x86_64` executa a instrução especial `sti` ("set interrupts") para habilitar interrupções externas. Quando tentamos `cargo run` agora, vemos que ocorre um double fault:
![QEMU printing `EXCEÇÃO: DOUBLE FAULT` because of hardware timer](qemu-hardware-timer-double-fault.png)
A razão para este double fault é que o timer de hardware (o [Intel 8253], para ser exato) é habilitado por padrão, então começamos a receber interrupções de timer assim que habilitamos interrupções. Como ainda não definimos uma função manipuladora para ele, nosso manipulador de double fault é invocado.
[Intel 8253]: https://en.wikipedia.org/wiki/Intel_8253
## Manipulando Interrupções de Timer
Como vemos do gráfico [acima](#o-8259-pic), o timer usa a linha 0 do PIC primário. Isso significa que ele chega à CPU como interrupção 32 (0 + offset 32). Em vez de codificar rigidamente o índice 32, o armazenamos em um enum `InterruptIndex`:
```rust
// em src/interrupts.rs
#[derive(Debug, Clone, Copy)]
#[repr(u8)]
pub enum InterruptIndex {
Timer = PIC_1_OFFSET,
}
impl InterruptIndex {
fn as_u8(self) -> u8 {
self as u8
}
fn as_usize(self) -> usize {
usize::from(self.as_u8())
}
}
```
O enum é um [enum similar a C] para que possamos especificar diretamente o índice para cada variante. O atributo `repr(u8)` especifica que cada variante é representada como um `u8`. Adicionaremos mais variantes para outras interrupções no futuro.
[enum similar a C]: https://doc.rust-lang.org/reference/items/enumerations.html#custom-discriminant-values-for-fieldless-enumerations
Agora podemos adicionar uma função manipuladora para a interrupção de timer:
```rust
// em src/interrupts.rs
use crate::print;
lazy_static! {
static ref IDT: InterruptDescriptorTable = {
let mut idt = InterruptDescriptorTable::new();
idt.breakpoint.set_handler_fn(breakpoint_handler);
[]
idt[InterruptIndex::Timer.as_usize()]
.set_handler_fn(timer_interrupt_handler); // novo
idt
};
}
extern "x86-interrupt" fn timer_interrupt_handler(
_stack_frame: InterruptStackFrame)
{
print!(".");
}
```
Nosso `timer_interrupt_handler` tem a mesma assinatura que nossos manipuladores de exceção, porque a CPU reage identicamente a exceções e interrupções externas (a única diferença é que algumas exceções empurram um código de erro). A struct [`InterruptDescriptorTable`] implementa a trait [`IndexMut`], então podemos acessar entradas individuais através da sintaxe de indexação de array.
[`InterruptDescriptorTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html
[`IndexMut`]: https://doc.rust-lang.org/core/ops/trait.IndexMut.html
Em nosso manipulador de interrupção de timer, imprimimos um ponto na tela. Como a interrupção de timer acontece periodicamente, esperaríamos ver um ponto aparecendo a cada tick do timer. No entanto, quando o executamos, vemos que apenas um único ponto é impresso:
![QEMU printing only a single dot for hardware timer](qemu-single-dot-printed.png)
### End of Interrupt
A razão é que o PIC espera um sinal explícito de "end of interrupt" (EOI) do nosso manipulador de interrupção. Este sinal diz ao controlador que a interrupção foi processada e que o sistema está pronto para receber a próxima interrupção. Então o PIC pensa que ainda estamos ocupados processando a primeira interrupção de timer e espera pacientemente pelo sinal EOI antes de enviar a próxima.
Para enviar o EOI, usamos nossa struct `PICS` estática novamente:
```rust
// em src/interrupts.rs
extern "x86-interrupt" fn timer_interrupt_handler(
_stack_frame: InterruptStackFrame)
{
print!(".");
unsafe {
PICS.lock()
.notify_end_of_interrupt(InterruptIndex::Timer.as_u8());
}
}
```
O `notify_end_of_interrupt` descobre se o PIC primário ou secundário enviou a interrupção e então usa as portas `command` e `data` para enviar um sinal EOI aos respectivos controladores. Se o PIC secundário enviou a interrupção, ambos os PICs precisam ser notificados porque o PIC secundário está conectado a uma linha de entrada do PIC primário.
Precisamos ter cuidado para usar o número de vetor de interrupção correto, caso contrário poderíamos acidentalmente deletar uma importante interrupção não enviada ou fazer nosso sistema travar. Esta é a razão pela qual a função é unsafe.
Quando agora executamos `cargo run` vemos pontos aparecendo periodicamente na tela:
![QEMU printing consecutive dots showing the hardware timer](qemu-hardware-timer-dots.gif)
### Configurando o Timer
O timer de hardware que usamos é chamado de _Programmable Interval Timer_, ou PIT, resumidamente. Como o nome diz, é possível configurar o intervalo entre duas interrupções. Não entraremos em detalhes aqui porque mudaremos em breve para o [APIC timer], mas a wiki do OSDev tem um artigo extenso sobre [configurando o PIT].
[APIC timer]: https://wiki.osdev.org/APIC_timer
[configurando o PIT]: https://wiki.osdev.org/Programmable_Interval_Timer
## Deadlocks
Agora temos uma forma de concorrência em nosso kernel: As interrupções de timer ocorrem assincronamente, então podem interromper nossa função `_start` a qualquer momento. Felizmente, o sistema de ownership de Rust previne muitos tipos de bugs relacionados à concorrência em tempo de compilação. Uma exceção notável são deadlocks. Deadlocks ocorrem se uma thread tenta adquirir um lock que nunca se tornará livre. Assim, a thread trava indefinidamente.
Já podemos provocar um deadlock em nosso kernel. Lembre-se, nossa macro `println` chama a função `vga_buffer::_print`, que [trava um `WRITER` global][vga spinlock] usando um spinlock:
[vga spinlock]: @/edition-2/posts/03-vga-text-buffer/index.md#spinlocks
```rust
// em src/vga_buffer.rs
[]
#[doc(hidden)]
pub fn _print(args: fmt::Arguments) {
use core::fmt::Write;
WRITER.lock().write_fmt(args).unwrap();
}
```
Ela trava o `WRITER`, chama `write_fmt` nele, e implicitamente o destrava no final da função. Agora imagine que uma interrupção ocorre enquanto o `WRITER` está travado e o manipulador de interrupção tenta imprimir algo também:
Passo de Tempo | _start | interrupt_handler
---------|------|------------------
0 | chama `println!` | &nbsp;
1 | `print` trava `WRITER` | &nbsp;
2 | | **interrupção ocorre**, manipulador começa a executar
3 | | chama `println!` |
4 | | `print` tenta travar `WRITER` (já travado)
5 | | `print` tenta travar `WRITER` (já travado)
… | | …
_nunca_ | _destravar `WRITER`_ |
O `WRITER` está travado, então o manipulador de interrupção espera até que se torne livre. Mas isso nunca acontece, porque a função `_start` só continua a executar após o manipulador de interrupção retornar. Assim, o sistema inteiro trava.
### Provocando um Deadlock
Podemos facilmente provocar tal deadlock em nosso kernel imprimindo algo no loop no final de nossa função `_start`:
```rust
// em src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
[]
loop {
use blog_os::print;
print!("-"); // novo
}
}
```
Quando o executamos no QEMU, obtemos uma saída da forma:
![QEMU output with many rows of hyphens and no dots](./qemu-deadlock.png)
Vemos que apenas um número limitado de hífens são impressos até que a primeira interrupção de timer ocorre. Então o sistema trava porque o manipulador de interrupção de timer entra em deadlock quando tenta imprimir um ponto. Esta é a razão pela qual não vemos pontos na saída acima.
O número real de hífens varia entre execuções porque a interrupção de timer ocorre assincronamente. Este não-determinismo é o que torna bugs relacionados à concorrência tão difíceis de depurar.
### Corrigindo o Deadlock
Para evitar este deadlock, podemos desativar interrupções enquanto o `Mutex` está travado:
```rust
// em src/vga_buffer.rs
/// Imprime a string formatada dada no buffer de texto VGA
/// através da instância global `WRITER`.
#[doc(hidden)]
pub fn _print(args: fmt::Arguments) {
use core::fmt::Write;
use x86_64::instructions::interrupts; // novo
interrupts::without_interrupts(|| { // novo
WRITER.lock().write_fmt(args).unwrap();
});
}
```
A função [`without_interrupts`] recebe um [closure] e o executa em um ambiente livre de interrupções. Usamos isso para garantir que nenhuma interrupção pode ocorrer enquanto o `Mutex` está travado. Quando executamos nosso kernel agora, vemos que ele continua executando sem travar. (Ainda não notamos nenhum ponto, mas isso é porque eles estão rolando rápido demais. Tente diminuir a velocidade da impressão, por exemplo, colocando um `for _ in 0..10000 {}` dentro do loop.)
[`without_interrupts`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/interrupts/fn.without_interrupts.html
[closure]: https://doc.rust-lang.org/book/ch13-01-closures.html
Podemos aplicar a mesma mudança à nossa função de impressão serial para garantir que nenhum deadlock ocorra com ela também:
```rust
// em src/serial.rs
#[doc(hidden)]
pub fn _print(args: ::core::fmt::Arguments) {
use core::fmt::Write;
use x86_64::instructions::interrupts; // novo
interrupts::without_interrupts(|| { // novo
SERIAL1
.lock()
.write_fmt(args)
.expect("Impressão para serial falhou");
});
}
```
Note que desativar interrupções não deve ser uma solução geral. O problema é que isso aumenta a latência de interrupção no pior caso, isto é, o tempo até o sistema reagir a uma interrupção. Portanto, interrupções devem ser desativadas apenas por um tempo muito curto.
## Corrigindo uma Race Condition
Se você executar `cargo test`, pode ver o teste `test_println_output` falhar:
```
> cargo test --lib
[…]
Running 4 tests
test_breakpoint_exception...[ok]
test_println... [ok]
test_println_many... [ok]
test_println_output... [failed]
Error: panicked at 'assertion failed: `(left == right)`
left: `'.'`,
right: `'S'`', src/vga_buffer.rs:205:9
```
A razão é uma _race condition_ entre o teste e nosso manipulador de timer. Lembre-se, o teste se parece com isto:
```rust
// em src/vga_buffer.rs
#[test_case]
fn test_println_output() {
let s = "Uma string de teste que cabe em uma única linha";
println!("{}", s);
for (i, c) in s.chars().enumerate() {
let screen_char = WRITER.lock().buffer.chars[BUFFER_HEIGHT - 2][i].read();
assert_eq!(char::from(screen_char.ascii_character), c);
}
}
```
O teste imprime uma string no buffer VGA e então verifica a saída iterando manualmente pelo array `buffer_chars`. A race condition ocorre porque o manipulador de interrupção de timer pode executar entre o `println` e a leitura dos caracteres de tela. Note que isso não é uma _data race_ perigosa, que Rust previne completamente em tempo de compilação. Veja o [_Rustonomicon_][nomicon-races] para detalhes.
[nomicon-races]: https://doc.rust-lang.org/nomicon/races.html
Para corrigir isso, precisamos manter o `WRITER` travado pela duração completa do teste, para que o manipulador de timer não possa escrever um `.` na tela no meio. O teste corrigido se parece com isto:
```rust
// em src/vga_buffer.rs
#[test_case]
fn test_println_output() {
use core::fmt::Write;
use x86_64::instructions::interrupts;
let s = "Uma string de teste que cabe em uma única linha";
interrupts::without_interrupts(|| {
let mut writer = WRITER.lock();
writeln!(writer, "\n{}", s).expect("writeln falhou");
for (i, c) in s.chars().enumerate() {
let screen_char = writer.buffer.chars[BUFFER_HEIGHT - 2][i].read();
assert_eq!(char::from(screen_char.ascii_character), c);
}
});
}
```
Realizamos as seguintes mudanças:
- Mantemos o writer travado pelo teste completo usando o método `lock()` explicitamente. Em vez de `println`, usamos a macro [`writeln`] que permite imprimir em um writer já travado.
- Para evitar outro deadlock, desativamos interrupções pela duração do teste. Caso contrário, o teste poderia ser interrompido enquanto o writer ainda está travado.
- Como o manipulador de interrupção de timer ainda pode executar antes do teste, imprimimos uma nova linha adicional `\n` antes de imprimir a string `s`. Desta forma, evitamos falha do teste quando o manipulador de timer já imprimiu alguns caracteres `.` na linha atual.
[`writeln`]: https://doc.rust-lang.org/core/macro.writeln.html
Com as mudanças acima, `cargo test` agora tem sucesso deterministicamente novamente.
Esta foi uma race condition muito inofensiva que causou apenas uma falha de teste. Como você pode imaginar, outras race conditions podem ser muito mais difíceis de depurar devido à sua natureza não-determinística. Felizmente, Rust nos previne de data races, que são a classe mais séria de race conditions, já que podem causar todo tipo de comportamento indefinido, incluindo crashes de sistema e corrupções silenciosas de memória.
## A Instrução `hlt`
Até agora, usamos uma simples instrução de loop vazio no final de nossas funções `_start` e `panic`. Isso faz a CPU girar infinitamente, e assim funciona como esperado. Mas também é muito ineficiente, porque a CPU continua executando a velocidade máxima mesmo que não haja trabalho a fazer. Você pode ver este problema em seu gerenciador de tarefas quando executa seu kernel: O processo QEMU precisa de perto de 100% de CPU o tempo todo.
O que realmente queremos fazer é parar a CPU até a próxima interrupção chegar. Isso permite que a CPU entre em um estado de sono no qual consome muito menos energia. A [instrução `hlt`] faz exatamente isso. Vamos usar esta instrução para criar um loop infinito eficiente em energia:
[instrução `hlt`]: https://en.wikipedia.org/wiki/HLT_(x86_instruction)
```rust
// em src/lib.rs
pub fn hlt_loop() -> ! {
loop {
x86_64::instructions::hlt();
}
}
```
A função `instructions::hlt` é apenas um [wrapper fino] em torno da instrução assembly. Ela é segura porque não há forma de comprometer a segurança de memória.
[wrapper fino]: https://github.com/rust-osdev/x86_64/blob/5e8e218381c5205f5777cb50da3ecac5d7e3b1ab/src/instructions/mod.rs#L16-L22
Agora podemos usar este `hlt_loop` em vez dos loops infinitos em nossas funções `_start` e `panic`:
```rust
// em src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
[]
println!("Não crashou!");
blog_os::hlt_loop(); // novo
}
#[cfg(not(test))]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("{}", info);
blog_os::hlt_loop(); // novo
}
```
Vamos atualizar nosso `lib.rs` também:
```rust
// em src/lib.rs
/// Ponto de entrada para `cargo test`
#[cfg(test)]
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
init();
test_main();
hlt_loop(); // novo
}
pub fn test_panic_handler(info: &PanicInfo) -> ! {
serial_println!("[failed]\n");
serial_println!("Error: {}\n", info);
exit_qemu(QemuExitCode::Failed);
hlt_loop(); // novo
}
```
Quando executamos nosso kernel agora no QEMU, vemos um uso de CPU muito menor.
## Entrada de Teclado
Agora que somos capazes de manipular interrupções de dispositivos externos, finalmente podemos adicionar suporte para entrada de teclado. Isso nos permitirá interagir com nosso kernel pela primeira vez.
<aside class="post_aside">
Note que apenas descrevemos como manipular teclados [PS/2] aqui, não teclados USB. No entanto, a placa-mãe emula teclados USB como dispositivos PS/2 para suportar software mais antigo, então podemos seguramente ignorar teclados USB até termos suporte USB em nosso kernel.
</aside>
[PS/2]: https://en.wikipedia.org/wiki/PS/2_port
Como o timer de hardware, o controlador de teclado já está habilitado por padrão. Então quando você pressiona uma tecla, o controlador de teclado envia uma interrupção para o PIC, que a encaminha para a CPU. A CPU procura por uma função manipuladora na IDT, mas a entrada correspondente está vazia. Portanto, ocorre um double fault.
Então vamos adicionar uma função manipuladora para a interrupção de teclado. É bem similar a como definimos o manipulador para a interrupção de timer; apenas usa um número de interrupção diferente:
```rust
// em src/interrupts.rs
#[derive(Debug, Clone, Copy)]
#[repr(u8)]
pub enum InterruptIndex {
Timer = PIC_1_OFFSET,
Keyboard, // novo
}
lazy_static! {
static ref IDT: InterruptDescriptorTable = {
let mut idt = InterruptDescriptorTable::new();
idt.breakpoint.set_handler_fn(breakpoint_handler);
[]
// novo
idt[InterruptIndex::Keyboard.as_usize()]
.set_handler_fn(keyboard_interrupt_handler);
idt
};
}
extern "x86-interrupt" fn keyboard_interrupt_handler(
_stack_frame: InterruptStackFrame)
{
print!("k");
unsafe {
PICS.lock()
.notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8());
}
}
```
Como vemos do gráfico [acima](#o-8259-pic), o teclado usa a linha 1 do PIC primário. Isso significa que ele chega à CPU como interrupção 33 (1 + offset 32). Adicionamos este índice como uma nova variante `Keyboard` ao enum `InterruptIndex`. Não precisamos especificar o valor explicitamente, já que ele assume o valor anterior mais um por padrão, que também é 33. No manipulador de interrupção, imprimimos um `k` e enviamos o sinal end of interrupt para o controlador de interrupção.
Agora vemos que um `k` aparece na tela quando pressionamos uma tecla. No entanto, isso só funciona para a primeira tecla que pressionamos. Mesmo se continuarmos a pressionar teclas, nenhum `k` adicional aparece na tela. Isso ocorre porque o controlador de teclado não enviará outra interrupção até lermos o chamado _scancode_ da tecla pressionada.
### Lendo os Scancodes
Para descobrir _qual_ tecla foi pressionada, precisamos consultar o controlador de teclado. Fazemos isso lendo da porta de dados do controlador PS/2, que é a [porta I/O] com o número `0x60`:
[porta I/O]: @/edition-2/posts/04-testing/index.md#i-o-ports
```rust
// em src/interrupts.rs
extern "x86-interrupt" fn keyboard_interrupt_handler(
_stack_frame: InterruptStackFrame)
{
use x86_64::instructions::port::Port;
let mut port = Port::new(0x60);
let scancode: u8 = unsafe { port.read() };
print!("{}", scancode);
unsafe {
PICS.lock()
.notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8());
}
}
```
Usamos o tipo [`Port`] da crate `x86_64` para ler um byte da porta de dados do teclado. Este byte é chamado de [_scancode_] e representa o pressionamento/liberação de tecla. Ainda não fazemos nada com o scancode, apenas o imprimimos na tela:
[`Port`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/port/struct.Port.html
[_scancode_]: https://en.wikipedia.org/wiki/Scancode
![QEMU printing scancodes to the screen when keys are pressed](qemu-printing-scancodes.gif)
A imagem acima me mostra digitando lentamente "123". Vemos que teclas adjacentes têm scancodes adjacentes e que pressionar uma tecla causa um scancode diferente de liberá-la. Mas como traduzimos exatamente os scancodes para as ações reais de tecla?
### Interpretando os Scancodes
Existem três padrões diferentes para o mapeamento entre scancodes e teclas, os chamados _conjuntos de scancode_. Todos os três remontam aos teclados de computadores IBM antigos: o [IBM XT], o [IBM 3270 PC], e o [IBM AT]. Felizmente, computadores posteriores não continuaram a tendência de definir novos conjuntos de scancode, mas em vez disso emularam os conjuntos existentes e os estenderam. Hoje, a maioria dos teclados pode ser configurada para emular qualquer um dos três conjuntos.
[IBM XT]: https://en.wikipedia.org/wiki/IBM_Personal_Computer_XT
[IBM 3270 PC]: https://en.wikipedia.org/wiki/IBM_3270_PC
[IBM AT]: https://en.wikipedia.org/wiki/IBM_Personal_Computer/AT
Por padrão, teclados PS/2 emulam o conjunto de scancode 1 ("XT"). Neste conjunto, os 7 bits inferiores de um byte de scancode definem a tecla, e o bit mais significativo define se é um pressionamento ("0") ou uma liberação ("1"). Teclas que não estavam presentes no [IBM XT] original, como a tecla enter no teclado numérico, geram dois scancodes em sucessão: um byte de escape `0xe0` e então um byte representando a tecla. Para uma lista de todos os scancodes do conjunto 1 e suas teclas correspondentes, confira a [Wiki OSDev][scancode set 1].
[scancode set 1]: https://wiki.osdev.org/Keyboard#Scan_Code_Set_1
Para traduzir os scancodes para teclas, podemos usar uma instrução `match`:
```rust
// em src/interrupts.rs
extern "x86-interrupt" fn keyboard_interrupt_handler(
_stack_frame: InterruptStackFrame)
{
use x86_64::instructions::port::Port;
let mut port = Port::new(0x60);
let scancode: u8 = unsafe { port.read() };
// novo
let key = match scancode {
0x02 => Some('1'),
0x03 => Some('2'),
0x04 => Some('3'),
0x05 => Some('4'),
0x06 => Some('5'),
0x07 => Some('6'),
0x08 => Some('7'),
0x09 => Some('8'),
0x0a => Some('9'),
0x0b => Some('0'),
_ => None,
};
if let Some(key) = key {
print!("{}", key);
}
unsafe {
PICS.lock()
.notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8());
}
}
```
O código acima traduz pressionamentos das teclas numéricas 0-9 e ignora todas as outras teclas. Ele usa uma instrução [match] para atribuir um caractere ou `None` a cada scancode. Então usa [`if let`] para desestruturar o `key` opcional. Ao usar o mesmo nome de variável `key` no padrão, [sombreamos] a declaração anterior, que é um padrão comum para desestruturar tipos `Option` em Rust.
[match]: https://doc.rust-lang.org/book/ch06-02-match.html
[`if let`]: https://doc.rust-lang.org/book/ch18-01-all-the-places-for-patterns.html#conditional-if-let-expressions
[sombreamos]: https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html#shadowing
Agora podemos escrever números:
![QEMU printing numbers to the screen](qemu-printing-numbers.gif)
Traduzir as outras teclas funciona da mesma forma. Felizmente, existe uma crate chamada [`pc-keyboard`] para traduzir scancodes dos conjuntos de scancode 1 e 2, então não precisamos implementar isso nós mesmos. Para usar a crate, a adicionamos ao nosso `Cargo.toml` e a importamos em nosso `lib.rs`:
[`pc-keyboard`]: https://docs.rs/pc-keyboard/0.7.0/pc_keyboard/
```toml
# em Cargo.toml
[dependencies]
pc-keyboard = "0.7.0"
```
Agora podemos usar esta crate para reescrever nosso `keyboard_interrupt_handler`:
```rust
// em/src/interrupts.rs
extern "x86-interrupt" fn keyboard_interrupt_handler(
_stack_frame: InterruptStackFrame)
{
use pc_keyboard::{layouts, DecodedKey, HandleControl, Keyboard, ScancodeSet1};
use spin::Mutex;
use x86_64::instructions::port::Port;
lazy_static! {
static ref KEYBOARD: Mutex<Keyboard<layouts::Us104Key, ScancodeSet1>> =
Mutex::new(Keyboard::new(ScancodeSet1::new(),
layouts::Us104Key, HandleControl::Ignore)
);
}
let mut keyboard = KEYBOARD.lock();
let mut port = Port::new(0x60);
let scancode: u8 = unsafe { port.read() };
if let Ok(Some(key_event)) = keyboard.add_byte(scancode) {
if let Some(key) = keyboard.process_keyevent(key_event) {
match key {
DecodedKey::Unicode(character) => print!("{}", character),
DecodedKey::RawKey(key) => print!("{:?}", key),
}
}
}
unsafe {
PICS.lock()
.notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8());
}
}
```
Usamos a macro `lazy_static` para criar um objeto [`Keyboard`] estático protegido por um Mutex. Inicializamos o `Keyboard` com um layout de teclado americano e o conjunto de scancode 1. O parâmetro [`HandleControl`] permite mapear `ctrl+[a-z]` aos caracteres Unicode `U+0001` através de `U+001A`. Não queremos fazer isso, então usamos a opção `Ignore` para manipular o `ctrl` como teclas normais.
[`HandleControl`]: https://docs.rs/pc-keyboard/0.7.0/pc_keyboard/enum.HandleControl.html
Em cada interrupção, travamos o Mutex, lemos o scancode do controlador de teclado, e o passamos para o método [`add_byte`], que traduz o scancode em um `Option<KeyEvent>`. O [`KeyEvent`] contém a tecla que causou o evento e se foi um evento de pressionamento ou liberação.
[`Keyboard`]: https://docs.rs/pc-keyboard/0.7.0/pc_keyboard/struct.Keyboard.html
[`add_byte`]: https://docs.rs/pc-keyboard/0.7.0/pc_keyboard/struct.Keyboard.html#method.add_byte
[`KeyEvent`]: https://docs.rs/pc-keyboard/0.7.0/pc_keyboard/struct.KeyEvent.html
Para interpretar este evento de tecla, o passamos para o método [`process_keyevent`], que traduz o evento de tecla em um caractere, se possível. Por exemplo, ele traduz um evento de pressionamento da tecla `A` em um caractere `a` minúsculo ou um caractere `A` maiúsculo, dependendo se a tecla shift foi pressionada.
[`process_keyevent`]: https://docs.rs/pc-keyboard/0.7.0/pc_keyboard/struct.Keyboard.html#method.process_keyevent
Com este manipulador de interrupção modificado, agora podemos escrever texto:
![Typing "Hello World" in QEMU](qemu-typing.gif)
### Configurando o Teclado
É possível configurar alguns aspectos de um teclado PS/2, por exemplo, qual conjunto de scancode ele deve usar. Não cobriremos isso aqui porque esta postagem já está longa o suficiente, mas a Wiki do OSDev tem uma visão geral dos possíveis [comandos de configuração].
[comandos de configuração]: https://wiki.osdev.org/PS/2_Keyboard#Commands
## Resumo
Esta postagem explicou como habilitar e manipular interrupções externas. Aprendemos sobre o 8259 PIC e seu layout primário/secundário, o remapeamento dos números de interrupção, e o sinal "end of interrupt". Implementamos manipuladores para o timer de hardware e o teclado e aprendemos sobre a instrução `hlt`, que para a CPU até a próxima interrupção.
Agora somos capazes de interagir com nosso kernel e temos alguns blocos fundamentais para criar um pequeno shell ou jogos simples.
## O Que Vem a Seguir?
Interrupções de timer são essenciais para um sistema operacional porque fornecem uma forma de interromper periodicamente o processo em execução e deixar o kernel retomar o controle. O kernel pode então mudar para um processo diferente e criar a ilusão de múltiplos processos executando em paralelo.
Mas antes de podermos criar processos ou threads, precisamos de uma forma de alocar memória para eles. As próximas postagens explorarão gerenciamento de memória para fornecer este bloco fundamental.

View File

@@ -0,0 +1,419 @@
+++
title = "Introdução à Paginação"
weight = 8
path = "pt-BR/paging-introduction"
date = 2019-01-14
[extra]
chapter = "Gerenciamento de Memória"
# Please update this when updating the translation
translation_based_on_commit = "9753695744854686a6b80012c89b0d850a44b4b0"
# GitHub usernames of the people that translated this post
translators = ["richarddalves"]
+++
Esta postagem introduz _paginação_, um esquema de gerenciamento de memória muito comum que também usaremos para nosso sistema operacional. Ela explica por que o isolamento de memória é necessário, como _segmentação_ funciona, o que é _memória virtual_, e como paginação resolve problemas de fragmentação de memória. Também explora o layout de tabelas de página multinível na arquitetura x86_64.
<!-- more -->
Este blog é desenvolvido abertamente no [GitHub]. Se você tiver algum problema ou dúvida, abra um issue lá. Você também pode deixar comentários [na parte inferior]. O código-fonte completo desta publicação pode ser encontrado na branch [`post-08`][post branch].
[GitHub]: https://github.com/phil-opp/blog_os
[na parte inferior]: #comments
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
[post branch]: https://github.com/phil-opp/blog_os/tree/post-08
<!-- toc -->
## Proteção de Memória
Uma tarefa principal de um sistema operacional é isolar programas uns dos outros. Seu navegador web não deveria ser capaz de interferir com seu editor de texto, por exemplo. Para alcançar este objetivo, sistemas operacionais utilizam funcionalidade de hardware para garantir que áreas de memória de um processo não sejam acessíveis por outros processos. Existem diferentes abordagens dependendo do hardware e da implementação do SO.
Como exemplo, alguns processadores ARM Cortex-M (usados para sistemas embarcados) têm uma [_Memory Protection Unit_] (MPU), que permite definir um pequeno número (por exemplo, 8) de regiões de memória com diferentes permissões de acesso (por exemplo, sem acesso, somente leitura, leitura-escrita). Em cada acesso à memória, a MPU garante que o endereço está em uma região com permissões de acesso corretas e lança uma exceção caso contrário. Ao mudar as regiões e permissões de acesso em cada troca de processo, o sistema operacional pode garantir que cada processo acesse apenas sua própria memória e assim isole processos uns dos outros.
[_Memory Protection Unit_]: https://developer.arm.com/docs/ddi0337/e/memory-protection-unit/about-the-mpu
No x86, o hardware suporta duas abordagens diferentes para proteção de memória: [segmentação] e [paginação].
[segmentação]: https://en.wikipedia.org/wiki/X86_memory_segmentation
[paginação]: https://en.wikipedia.org/wiki/Virtual_memory#Paged_virtual_memory
## Segmentação
Segmentação já foi introduzida em 1978, originalmente para aumentar a quantidade de memória endereçável. A situação naquela época era que CPUs usavam apenas endereços de 16 bits, o que limitava a quantidade de memória endereçável a 64&nbsp;KiB. Para tornar mais que esses 64&nbsp;KiB acessíveis, registradores de segmento adicionais foram introduzidos, cada um contendo um endereço de deslocamento. A CPU automaticamente adicionava este deslocamento em cada acesso à memória, então até 1&nbsp;MiB de memória era acessível.
O registrador de segmento é escolhido automaticamente pela CPU dependendo do tipo de acesso à memória: Para buscar instruções, o segmento de código `CS` é usado, e para operações de pilha (push/pop), o segmento de pilha `SS` é usado. Outras instruções usam o segmento de dados `DS` ou o segmento extra `ES`. Posteriormente, dois registradores de segmento adicionais, `FS` e `GS`, foram adicionados, que podem ser usados livremente.
Na primeira versão de segmentação, os registradores de segmento continham diretamente o deslocamento e nenhum controle de acesso era realizado. Isso mudou posteriormente com a introdução do [_modo protegido_]. Quando a CPU executa neste modo, os descritores de segmento contêm um índice em uma [_tabela de descritores_] local ou global, que contém além de um endereço de deslocamento o tamanho do segmento e permissões de acesso. Ao carregar tabelas de descritores globais/locais separadas para cada processo, que confinam acessos à memória às próprias áreas de memória do processo, o SO pode isolar processos uns dos outros.
[_modo protegido_]: https://en.wikipedia.org/wiki/X86_memory_segmentation#Protected_mode
[_tabela de descritores_]: https://en.wikipedia.org/wiki/Global_Descriptor_Table
Ao modificar os endereços de memória antes do acesso real, segmentação já empregava uma técnica que agora é usada em quase todo lugar: _memória virtual_.
### Memória Virtual
A ideia por trás da memória virtual é abstrair os endereços de memória do dispositivo de armazenamento físico subjacente. Em vez de acessar diretamente o dispositivo de armazenamento, um passo de tradução é realizado primeiro. Para segmentação, o passo de tradução é adicionar o endereço de deslocamento do segmento ativo. Imagine um programa acessando o endereço de memória `0x1234000` em um segmento com deslocamento de `0x1111000`: O endereço que é realmente acessado é `0x2345000`.
Para diferenciar os dois tipos de endereço, endereços antes da tradução são chamados _virtuais_, e endereços após a tradução são chamados _físicos_. Uma diferença importante entre esses dois tipos de endereços é que endereços físicos são únicos e sempre se referem à mesma localização de memória distinta. Endereços virtuais, por outro lado, dependem da função de tradução. É inteiramente possível que dois endereços virtuais diferentes se refiram ao mesmo endereço físico. Além disso, endereços virtuais idênticos podem se referir a endereços físicos diferentes quando usam funções de tradução diferentes.
Um exemplo onde esta propriedade é útil é executar o mesmo programa duas vezes em paralelo:
![Two virtual address spaces with address 0150, one translated to 100250, the other to 300450](segmentation-same-program-twice.svg)
Aqui o mesmo programa executa duas vezes, mas com funções de tradução diferentes. A primeira instância tem um deslocamento de segmento de 100, então seus endereços virtuais 0150 são traduzidos para os endereços físicos 100250. A segunda instância tem um deslocamento de 300, que traduz seus endereços virtuais 0150 para endereços físicos 300450. Isso permite que ambos os programas executem o mesmo código e usem os mesmos endereços virtuais sem interferir uns com os outros.
Outra vantagem é que programas agora podem ser colocados em localizações arbitrárias de memória física, mesmo se usarem endereços virtuais completamente diferentes. Assim, o SO pode utilizar a quantidade total de memória disponível sem precisar recompilar programas.
### Fragmentação
A diferenciação entre endereços virtuais e físicos torna a segmentação realmente poderosa. No entanto, ela tem o problema de fragmentação. Como exemplo, imagine que queremos executar uma terceira cópia do programa que vimos acima:
![Three virtual address spaces, but there is not enough continuous space for the third](segmentation-fragmentation.svg)
Não há forma de mapear a terceira instância do programa para memória virtual sem sobreposição, mesmo que haja mais que memória livre suficiente disponível. O problema é que precisamos de memória _contínua_ e não podemos usar os pequenos pedaços livres.
Uma forma de combater esta fragmentação é pausar a execução, mover as partes usadas da memória mais próximas, atualizar a tradução, e então retomar a execução:
![Three virtual address spaces after defragmentation](segmentation-fragmentation-compacted.svg)
Agora há espaço contínuo suficiente para iniciar a terceira instância do nosso programa.
A desvantagem deste processo de desfragmentação é que ele precisa copiar grandes quantidades de memória, o que diminui o desempenho. Também precisa ser feito regularmente antes que a memória se torne muito fragmentada. Isso torna o desempenho imprevisível já que programas são pausados em momentos aleatórios e podem se tornar não responsivos.
O problema de fragmentação é uma das razões pelas quais segmentação não é mais usada pela maioria dos sistemas. Na verdade, segmentação nem é mais suportada no modo de 64 bits no x86. Em vez disso, _paginação_ é usada, que evita completamente o problema de fragmentação.
## Paginação
A ideia é dividir tanto o espaço de memória virtual quanto o físico em pequenos blocos de tamanho fixo. Os blocos do espaço de memória virtual são chamados _páginas_, e os blocos do espaço de endereço físico são chamados _frames_. Cada página pode ser individualmente mapeada para um frame, o que torna possível dividir regiões de memória maiores em frames físicos não contínuos.
A vantagem disso se torna visível se recapitularmos o exemplo do espaço de memória fragmentado, mas usando paginação em vez de segmentação desta vez:
![With paging, the third program instance can be split across many smaller physical areas.](paging-fragmentation.svg)
Neste exemplo, temos um tamanho de página de 50 bytes, o que significa que cada uma de nossas regiões de memória é dividida em três páginas. Cada página é mapeada para um frame individualmente, então uma região de memória virtual contínua pode ser mapeada para frames físicos não contínuos. Isso nos permite iniciar a terceira instância do programa sem realizar nenhuma desfragmentação antes.
### Fragmentação Escondida
Comparado à segmentação, paginação usa muitas regiões de memória pequenas de tamanho fixo em vez de algumas regiões grandes de tamanho variável. Como cada frame tem o mesmo tamanho, não há frames que são muito pequenos para serem usados, então nenhuma fragmentação ocorre.
Ou _parece_ que nenhuma fragmentação ocorre. Ainda há algum tipo escondido de fragmentação, a chamada _fragmentação interna_. Fragmentação interna ocorre porque nem toda região de memória é um múltiplo exato do tamanho da página. Imagine um programa de tamanho 101 no exemplo acima: Ele ainda precisaria de três páginas de tamanho 50, então ocuparia 49 bytes a mais que o necessário. Para diferenciar os dois tipos de fragmentação, o tipo de fragmentação que acontece ao usar segmentação é chamado _fragmentação externa_.
Fragmentação interna é infeliz mas frequentemente melhor que a fragmentação externa que ocorre com segmentação. Ela ainda desperdiça memória, mas não requer desfragmentação e torna a quantidade de fragmentação previsível (em média metade de uma página por região de memória).
### Tabelas de Página
Vimos que cada uma das potencialmente milhões de páginas é individualmente mapeada para um frame. Esta informação de mapeamento precisa ser armazenada em algum lugar. Segmentação usa um registrador seletor de segmento individual para cada região de memória ativa, o que não é possível para paginação já que há muito mais páginas que registradores. Em vez disso, paginação usa uma estrutura de tabela chamada _tabela de página_ para armazenar a informação de mapeamento.
Para nosso exemplo acima, as tabelas de página pareceriam com isto:
![Three page tables, one for each program instance. For instance 1, the mapping is 0->100, 50->150, 100->200. For instance 2, it is 0->300, 50->350, 100->400. For instance 3, it is 0->250, 50->450, 100->500.](paging-page-tables.svg)
Vemos que cada instância de programa tem sua própria tabela de página. Um ponteiro para a tabela atualmente ativa é armazenado em um registrador especial da CPU. No `x86`, este registrador é chamado `CR3`. É trabalho do sistema operacional carregar este registrador com o ponteiro para a tabela de página correta antes de executar cada instância de programa.
Em cada acesso à memória, a CPU lê o ponteiro da tabela do registrador e procura o frame mapeado para a página acessada na tabela. Isso é inteiramente feito em hardware e completamente invisível para o programa em execução. Para acelerar o processo de tradução, muitas arquiteturas de CPU têm um cache especial que lembra os resultados das últimas traduções.
Dependendo da arquitetura, entradas da tabela de página também podem armazenar atributos como permissões de acesso em um campo de flags. No exemplo acima, a flag "r/w" torna a página tanto legível quanto gravável.
### Tabelas de Página Multinível
As tabelas de página simples que acabamos de ver têm um problema em espaços de endereço maiores: elas desperdiçam memória. Por exemplo, imagine um programa que usa as quatro páginas virtuais `0`, `1_000_000`, `1_000_050`, e `1_000_100` (usamos `_` como separador de milhares):
![Page 0 mapped to frame 0 and pages `1_000_000``1_000_150` mapped to frames 100250](single-level-page-table.svg)
Ele precisa apenas de 4 frames físicos, mas a tabela de página tem mais de um milhão de entradas. Não podemos omitir as entradas vazias porque então a CPU não seria mais capaz de pular diretamente para a entrada correta no processo de tradução (por exemplo, não é mais garantido que a quarta página use a quarta entrada).
Para reduzir a memória desperdiçada, podemos usar uma **tabela de página de dois níveis**. A ideia é que usamos tabelas de página diferentes para regiões de endereço diferentes. Uma tabela adicional chamada tabela de página de _nível 2_ contém o mapeamento entre regiões de endereço e tabelas de página (nível 1).
Isso é melhor explicado por um exemplo. Vamos definir que cada tabela de página de nível 1 é responsável por uma região de tamanho `10_000`. Então as seguintes tabelas existiriam para o exemplo de mapeamento acima:
![Page 0 points to entry 0 of the level 2 page table, which points to the level 1 page table T1. The first entry of T1 points to frame 0; the other entries are empty. Pages `1_000_000``1_000_150` point to the 100th entry of the level 2 page table, which points to a different level 1 page table T2. The first three entries of T2 point to frames 100250; the other entries are empty.](multilevel-page-table.svg)
A página 0 cai na primeira região de `10_000` bytes, então usa a primeira entrada da tabela de página de nível 2. Esta entrada aponta para a tabela de página de nível 1 T1, que especifica que a página `0` aponta para o frame `0`.
As páginas `1_000_000`, `1_000_050`, e `1_000_100` todas caem na 100ª região de `10_000` bytes, então usam a 100ª entrada da tabela de página de nível 2. Esta entrada aponta para uma tabela de página de nível 1 diferente T2, que mapeia as três páginas para frames `100`, `150`, e `200`. Note que o endereço da página em tabelas de nível 1 não inclui o deslocamento da região. Por exemplo, a entrada para a página `1_000_050` é apenas `50`.
Ainda temos 100 entradas vazias na tabela de nível 2, mas muito menos que o milhão de entradas vazias antes. A razão para essas economias é que não precisamos criar tabelas de página de nível 1 para as regiões de memória não mapeadas entre `10_000` e `1_000_000`.
O princípio de tabelas de página de dois níveis pode ser estendido para três, quatro, ou mais níveis. Então o registrador de tabela de página aponta para a tabela de nível mais alto, que aponta para a tabela de próximo nível mais baixo, que aponta para o próximo nível mais baixo, e assim por diante. A tabela de página de nível 1 então aponta para o frame mapeado. O princípio em geral é chamado de tabela de página _multinível_ ou _hierárquica_.
Agora que sabemos como paginação e tabelas de página multinível funcionam, podemos olhar como paginação é implementada na arquitetura x86_64 (assumimos no seguinte que a CPU executa no modo de 64 bits).
## Paginação no x86_64
A arquitetura x86_64 usa uma tabela de página de 4 níveis e um tamanho de página de 4&nbsp;KiB. Cada tabela de página, independente do nível, tem um tamanho fixo de 512 entradas. Cada entrada tem um tamanho de 8 bytes, então cada tabela tem 512 * 8&nbsp;B = 4&nbsp;KiB de tamanho e assim cabe exatamente em uma página.
O índice da tabela de página para cada nível é derivado diretamente do endereço virtual:
![Bits 012 are the page offset, bits 1221 the level 1 index, bits 2130 the level 2 index, bits 3039 the level 3 index, and bits 3948 the level 4 index](x86_64-table-indices-from-address.svg)
Vemos que cada índice de tabela consiste de 9 bits, o que faz sentido porque cada tabela tem 2^9 = 512 entradas. Os 12 bits mais baixos são o deslocamento na página de 4&nbsp;KiB (2^12 bytes = 4&nbsp;KiB). Os bits 48 a 64 são descartados, o que significa que x86_64 não é realmente 64 bits já que suporta apenas endereços de 48 bits.
Mesmo que os bits 48 a 64 sejam descartados, eles não podem ser definidos para valores arbitrários. Em vez disso, todos os bits nesta faixa devem ser cópias do bit 47 para manter endereços únicos e permitir extensões futuras como a tabela de página de 5 níveis. Isso é chamado _extensão de sinal_ porque é muito similar à [extensão de sinal em complemento de dois]. Quando um endereço não é corretamente estendido com sinal, a CPU lança uma exceção.
[extensão de sinal em complemento de dois]: https://en.wikipedia.org/wiki/Two's_complement#Sign_extension
Vale notar que as CPUs Intel "Ice Lake" recentes opcionalmente suportam [tabelas de página de 5 níveis] para estender endereços virtuais de 48 bits para 57 bits. Dado que otimizar nosso kernel para uma CPU específica não faz sentido neste estágio, trabalharemos apenas com tabelas de página padrão de 4 níveis nesta postagem.
[tabelas de página de 5 níveis]: https://en.wikipedia.org/wiki/Intel_5-level_paging
### Exemplo de Tradução
Vamos passar por um exemplo para entender como o processo de tradução funciona em detalhes:
![An example of a 4-level page hierarchy with each page table shown in physical memory](x86_64-page-table-translation.svg)
O endereço físico da tabela de página de nível 4 atualmente ativa, que é a raiz da tabela de página de 4 níveis, é armazenado no registrador `CR3`. Cada entrada da tabela de página então aponta para o frame físico da tabela de próximo nível. A entrada da tabela de nível 1 então aponta para o frame mapeado. Note que todos os endereços nas tabelas de página são físicos em vez de virtuais, porque caso contrário a CPU precisaria traduzi-los também (o que poderia causar uma recursão sem fim).
A hierarquia de tabela de página acima mapeia duas páginas (em azul). Dos índices da tabela de página, podemos deduzir que os endereços virtuais dessas duas páginas são `0x803FE7F000` e `0x803FE00000`. Vamos ver o que acontece quando o programa tenta ler do endereço `0x803FE7F5CE`. Primeiro, convertemos o endereço para binário e determinamos os índices da tabela de página e o deslocamento de página para o endereço:
![The sign extension bits are all 0, the level 4 index is 1, the level 3 index is 0, the level 2 index is 511, the level 1 index is 127, and the page offset is 0x5ce](x86_64-page-table-translation-addresses.png)
Com esses índices, agora podemos percorrer a hierarquia da tabela de página para determinar o frame mapeado para o endereço:
- Começamos lendo o endereço da tabela de nível 4 do registrador `CR3`.
- O índice de nível 4 é 1, então olhamos para a entrada com índice 1 daquela tabela, que nos diz que a tabela de nível 3 está armazenada no endereço 16&nbsp;KiB.
- Carregamos a tabela de nível 3 daquele endereço e olhamos para a entrada com índice 0, que nos aponta para a tabela de nível 2 em 24&nbsp;KiB.
- O índice de nível 2 é 511, então olhamos para a última entrada daquela página para descobrir o endereço da tabela de nível 1.
- Através da entrada com índice 127 da tabela de nível 1, finalmente descobrimos que a página está mapeada para o frame 12&nbsp;KiB, ou 0x3000 em hexadecimal.
- O passo final é adicionar o deslocamento de página ao endereço do frame para obter o endereço físico 0x3000 + 0x5ce = 0x35ce.
![The same example 4-level page hierarchy with 5 additional arrows: "Step 0" from the CR3 register to the level 4 table, "Step 1" from the level 4 entry to the level 3 table, "Step 2" from the level 3 entry to the level 2 table, "Step 3" from the level 2 entry to the level 1 table, and "Step 4" from the level 1 table to the mapped frames.](x86_64-page-table-translation-steps.svg)
As permissões para a página na tabela de nível 1 são `r`, o que significa somente leitura. O hardware reforça essas permissões e lançaria uma exceção se tentássemos escrever naquela página. Permissões em páginas de nível mais alto restringem as permissões possíveis em níveis mais baixos, então se definirmos a entrada de nível 3 como somente leitura, nenhuma página que use esta entrada pode ser gravável, mesmo se níveis mais baixos especificarem permissões de leitura/escrita.
É importante notar que mesmo que este exemplo usasse apenas uma única instância de cada tabela, tipicamente há múltiplas instâncias de cada nível em cada espaço de endereço. No máximo, há:
- uma tabela de nível 4,
- 512 tabelas de nível 3 (porque a tabela de nível 4 tem 512 entradas),
- 512 * 512 tabelas de nível 2 (porque cada uma das 512 tabelas de nível 3 tem 512 entradas), e
- 512 * 512 * 512 tabelas de nível 1 (512 entradas para cada tabela de nível 2).
### Formato da Tabela de Página
Tabelas de página na arquitetura x86_64 são basicamente um array de 512 entradas. Na sintaxe Rust:
```rust
#[repr(align(4096))]
pub struct PageTable {
entries: [PageTableEntry; 512],
}
```
Como indicado pelo atributo `repr`, tabelas de página precisam ser alinhadas por página, isto é, alinhadas em um limite de 4&nbsp;KiB. Este requisito garante que uma tabela de página sempre preenche uma página completa e permite uma otimização que torna as entradas muito compactas.
Cada entrada tem 8 bytes (64 bits) de tamanho e tem o seguinte formato:
Bit(s) | Nome | Significado
------ | ---- | -------
0 | present | a página está atualmente na memória
1 | writable | é permitido escrever nesta página
2 | user accessible | se não definido, apenas código em modo kernel pode acessar esta página
3 | write-through caching | escritas vão diretamente para a memória
4 | disable cache | nenhum cache é usado para esta página
5 | accessed | a CPU define este bit quando esta página é usada
6 | dirty | a CPU define este bit quando uma escrita nesta página ocorre
7 | huge page/null | deve ser 0 em P1 e P4, cria uma página de 1&nbsp;GiB em P3, cria uma página de 2&nbsp;MiB em P2
8 | global | página não é removida dos caches em troca de espaço de endereço (bit PGE do registrador CR4 deve estar definido)
9-11 | available | pode ser usado livremente pelo SO
12-51 | physical address | o endereço físico de 52 bits alinhado por página do frame ou da próxima tabela de página
52-62 | available | pode ser usado livremente pelo SO
63 | no execute | proíbe executar código nesta página (o bit NXE no registrador EFER deve estar definido)
Vemos que apenas os bits 1251 são usados para armazenar o endereço físico do frame. Os bits restantes são usados como flags ou podem ser usados livremente pelo sistema operacional. Isso é possível porque sempre apontamos para um endereço alinhado em 4096 bytes, seja para uma tabela de página alinhada por página ou para o início de um frame mapeado. Isso significa que os bits 011 são sempre zero, então não há razão para armazenar esses bits porque o hardware pode simplesmente defini-los para zero antes de usar o endereço. O mesmo é verdade para os bits 5263, porque a arquitetura x86_64 suporta apenas endereços físicos de 52 bits (similar a como suporta apenas endereços virtuais de 48 bits).
Vamos olhar mais de perto as flags disponíveis:
- A flag `present` diferencia páginas mapeadas de não mapeadas. Ela pode ser usada para temporariamente trocar páginas para o disco quando a memória principal fica cheia. Quando a página é acessada subsequentemente, uma exceção especial chamada _page fault_ ocorre, à qual o sistema operacional pode reagir recarregando a página faltante do disco e então continuando o programa.
- As flags `writable` e `no execute` controlam se o conteúdo da página é gravável ou contém instruções executáveis, respectivamente.
- As flags `accessed` e `dirty` são automaticamente definidas pela CPU quando uma leitura ou escrita na página ocorre. Esta informação pode ser aproveitada pelo sistema operacional, por exemplo, para decidir quais páginas trocar ou se o conteúdo da página foi modificado desde o último salvamento no disco.
- As flags `write-through caching` e `disable cache` permitem o controle de caches para cada página individualmente.
- A flag `user accessible` torna uma página disponível para código em espaço de usuário, caso contrário, é acessível apenas quando a CPU está em modo kernel. Este recurso pode ser usado para tornar [chamadas de sistema] mais rápidas mantendo o kernel mapeado enquanto um programa em espaço de usuário está executando. No entanto, a vulnerabilidade [Spectre] pode permitir que programas em espaço de usuário leiam essas páginas de qualquer forma.
- A flag `global` sinaliza ao hardware que uma página está disponível em todos os espaços de endereço e assim não precisa ser removida do cache de tradução (veja a seção sobre o TLB abaixo) em trocas de espaço de endereço. Esta flag é comumente usada junto com uma flag `user accessible` desmarcada para mapear o código do kernel para todos os espaços de endereço.
- A flag `huge page` permite a criação de páginas de tamanhos maiores permitindo que as entradas das tabelas de página de nível 2 ou nível 3 apontem diretamente para um frame mapeado. Com este bit definido, o tamanho da página aumenta por fator 512 para 2&nbsp;MiB = 512 * 4&nbsp;KiB para entradas de nível 2 ou até 1&nbsp;GiB = 512 * 2&nbsp;MiB para entradas de nível 3. A vantagem de usar páginas maiores é que menos linhas do cache de tradução e menos tabelas de página são necessárias.
[chamadas de sistema]: https://en.wikipedia.org/wiki/System_call
[Spectre]: https://en.wikipedia.org/wiki/Spectre_(security_vulnerability)
A crate `x86_64` fornece tipos para [tabelas de página] e suas [entradas], então não precisamos criar essas estruturas nós mesmos.
[tabelas de página]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page_table/struct.PageTable.html
[entradas]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page_table/struct.PageTableEntry.html
### O Translation Lookaside Buffer
Uma tabela de página de 4 níveis torna a tradução de endereços virtuais cara porque cada tradução requer quatro acessos à memória. Para melhorar o desempenho, a arquitetura x86_64 armazena em cache as últimas traduções no chamado _translation lookaside buffer_ (TLB). Isso permite pular a tradução quando ela ainda está em cache.
Ao contrário dos outros caches da CPU, o TLB não é totalmente transparente e não atualiza ou remove traduções quando o conteúdo das tabelas de página muda. Isso significa que o kernel deve atualizar manualmente o TLB sempre que modifica uma tabela de página. Para fazer isso, há uma instrução especial da CPU chamada [`invlpg`] ("invalidate page") que remove a tradução para a página especificada do TLB, para que seja carregada novamente da tabela de página no próximo acesso. O TLB também pode ser completamente esvaziado recarregando o registrador `CR3`, que simula uma troca de espaço de endereço. A crate `x86_64` fornece funções Rust para ambas as variantes no [módulo `tlb`].
[`invlpg`]: https://www.felixcloutier.com/x86/INVLPG.html
[módulo `tlb`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/tlb/index.html
É importante lembrar de esvaziar o TLB em cada modificação de tabela de página porque caso contrário a CPU pode continuar usando a tradução antiga, o que pode levar a bugs não-determinísticos que são muito difíceis de depurar.
## Implementação
Uma coisa que ainda não mencionamos: **Nosso kernel já executa em paginação**. O bootloader que adicionamos na postagem ["Um Kernel Rust Mínimo"] já configurou uma hierarquia de paginação de 4 níveis que mapeia cada página do nosso kernel para um frame físico. O bootloader faz isso porque paginação é obrigatória no modo de 64 bits no x86_64.
["Um Kernel Rust Mínimo"]: @/edition-2/posts/02-minimal-rust-kernel/index.md#creating-a-bootimage
Isso significa que cada endereço de memória que usamos em nosso kernel era um endereço virtual. Acessar o buffer VGA no endereço `0xb8000` só funcionou porque o bootloader fez _identity mapping_ daquela página de memória, o que significa que ele mapeou a página virtual `0xb8000` para o frame físico `0xb8000`.
Paginação já torna nosso kernel relativamente seguro, já que cada acesso à memória que está fora dos limites causa uma exceção de page fault em vez de escrever em memória física aleatória. O bootloader até define as permissões de acesso corretas para cada página, o que significa que apenas as páginas contendo código são executáveis e apenas páginas de dados são graváveis.
### Page Faults
Vamos tentar causar um page fault acessando alguma memória fora do nosso kernel. Primeiro, criamos um manipulador de page fault e o registramos em nossa IDT, para que vejamos uma exceção de page fault em vez de um [double fault] genérico:
[double fault]: @/edition-2/posts/06-double-faults/index.md
```rust
// em src/interrupts.rs
lazy_static! {
static ref IDT: InterruptDescriptorTable = {
let mut idt = InterruptDescriptorTable::new();
[]
idt.page_fault.set_handler_fn(page_fault_handler); // novo
idt
};
}
use x86_64::structures::idt::PageFaultErrorCode;
use crate::hlt_loop;
extern "x86-interrupt" fn page_fault_handler(
stack_frame: InterruptStackFrame,
error_code: PageFaultErrorCode,
) {
use x86_64::registers::control::Cr2;
println!("EXCEÇÃO: PAGE FAULT");
println!("Endereço Acessado: {:?}", Cr2::read());
println!("Código de Erro: {:?}", error_code);
println!("{:#?}", stack_frame);
hlt_loop();
}
```
O registrador [`CR2`] é automaticamente definido pela CPU em um page fault e contém o endereço virtual acessado que causou o page fault. Usamos a função [`Cr2::read`] da crate `x86_64` para lê-lo e imprimi-lo. O tipo [`PageFaultErrorCode`] fornece mais informações sobre o tipo de acesso à memória que causou o page fault, por exemplo, se foi causado por uma operação de leitura ou escrita. Por esta razão, também o imprimimos. Não podemos continuar a execução sem resolver o page fault, então entramos em um [`hlt_loop`] no final.
[`CR2`]: https://en.wikipedia.org/wiki/Control_register#CR2
[`Cr2::read`]: https://docs.rs/x86_64/0.14.2/x86_64/registers/control/struct.Cr2.html#method.read
[`PageFaultErrorCode`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.PageFaultErrorCode.html
[LLVM bug]: https://github.com/rust-lang/rust/issues/57270
[`hlt_loop`]: @/edition-2/posts/07-hardware-interrupts/index.md#the-hlt-instruction
Agora podemos tentar acessar alguma memória fora do nosso kernel:
```rust
// em src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
println!("Olá Mundo{}", "!");
blog_os::init();
// novo
let ptr = 0xdeadbeaf as *mut u8;
unsafe { *ptr = 42; }
// como antes
#[cfg(test)]
test_main();
println!("Não crashou!");
blog_os::hlt_loop();
}
```
Quando o executamos, vemos que nosso manipulador de page fault é chamado:
![EXCEPTION: Page Fault, Accessed Address: VirtAddr(0xdeadbeaf), Error Code: CAUSED_BY_WRITE, InterruptStackFrame: {…}](qemu-page-fault.png)
O registrador `CR2` de fato contém `0xdeadbeaf`, o endereço que tentamos acessar. O código de erro nos diz através do [`CAUSED_BY_WRITE`] que a falha ocorreu ao tentar realizar uma operação de escrita. Ele nos diz ainda mais através dos [bits que _não_ estão definidos][`PageFaultErrorCode`]. Por exemplo, o fato de que a flag `PROTECTION_VIOLATION` não está definida significa que o page fault ocorreu porque a página alvo não estava presente.
[`CAUSED_BY_WRITE`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.PageFaultErrorCode.html#associatedconstant.CAUSED_BY_WRITE
Vemos que o ponteiro de instrução atual é `0x2031b2`, então sabemos que este endereço aponta para uma página de código. Páginas de código são mapeadas como somente leitura pelo bootloader, então ler deste endereço funciona mas escrever causa um page fault. Você pode tentar isso mudando o ponteiro `0xdeadbeaf` para `0x2031b2`:
```rust
// Note: O endereço real pode ser diferente para você. Use o endereço que
// seu manipulador de page fault reporta.
let ptr = 0x2031b2 as *mut u8;
// lê de uma página de código
unsafe { let x = *ptr; }
println!("leitura funcionou");
// escreve em uma página de código
unsafe { *ptr = 42; }
println!("escrita funcionou");
```
Ao comentar a última linha, vemos que o acesso de leitura funciona, mas o acesso de escrita causa um page fault:
![QEMU with output: "leitura funcionou, EXCEÇÃO: Page Fault, Accessed Address: VirtAddr(0x2031b2), Error Code: PROTECTION_VIOLATION | CAUSED_BY_WRITE, InterruptStackFrame: {…}"](qemu-page-fault-protection.png)
Vemos que a mensagem _"leitura funcionou"_ é impressa, o que indica que a operação de leitura não causou nenhum erro. No entanto, em vez da mensagem _"escrita funcionou"_, ocorre um page fault. Desta vez a flag [`PROTECTION_VIOLATION`] está definida além da flag [`CAUSED_BY_WRITE`], o que indica que a página estava presente, mas a operação não era permitida nela. Neste caso, escritas na página não são permitidas já que páginas de código são mapeadas como somente leitura.
[`PROTECTION_VIOLATION`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.PageFaultErrorCode.html#associatedconstant.PROTECTION_VIOLATION
### Acessando as Tabelas de Página
Vamos tentar dar uma olhada nas tabelas de página que definem como nosso kernel é mapeado:
```rust
// em src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
println!("Olá Mundo{}", "!");
blog_os::init();
use x86_64::registers::control::Cr3;
let (level_4_page_table, _) = Cr3::read();
println!("Tabela de página de nível 4 em: {:?}", level_4_page_table.start_address());
[] // test_main(), println(…), e hlt_loop()
}
```
A função [`Cr3::read`] da crate `x86_64` retorna a tabela de página de nível 4 atualmente ativa do registrador `CR3`. Ela retorna uma tupla de um tipo [`PhysFrame`] e um tipo [`Cr3Flags`]. Estamos interessados apenas no frame, então ignoramos o segundo elemento da tupla.
[`Cr3::read`]: https://docs.rs/x86_64/0.14.2/x86_64/registers/control/struct.Cr3.html#method.read
[`PhysFrame`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/frame/struct.PhysFrame.html
[`Cr3Flags`]: https://docs.rs/x86_64/0.14.2/x86_64/registers/control/struct.Cr3Flags.html
Quando o executamos, vemos a seguinte saída:
```
Tabela de página de nível 4 em: PhysAddr(0x1000)
```
Então a tabela de página de nível 4 atualmente ativa está armazenada no endereço `0x1000` na memória _física_, como indicado pelo tipo wrapper [`PhysAddr`]. A questão agora é: como podemos acessar esta tabela do nosso kernel?
[`PhysAddr`]: https://docs.rs/x86_64/0.14.2/x86_64/addr/struct.PhysAddr.html
Acessar memória física diretamente não é possível quando paginação está ativa, já que programas poderiam facilmente contornar a proteção de memória e acessar a memória de outros programas caso contrário. Então a única forma de acessar a tabela é através de alguma página virtual que está mapeada para o frame físico no endereço `0x1000`. Este problema de criar mapeamentos para frames de tabela de página é um problema geral, já que o kernel precisa acessar as tabelas de página regularmente, por exemplo, ao alocar uma pilha para uma nova thread.
Soluções para este problema são explicadas em detalhes na próxima postagem.
## Resumo
Esta postagem introduziu duas técnicas de proteção de memória: segmentação e paginação. Enquanto a primeira usa regiões de memória de tamanho variável e sofre de fragmentação externa, a última usa páginas de tamanho fixo e permite controle muito mais refinado sobre permissões de acesso.
Paginação armazena a informação de mapeamento para páginas em tabelas de página com um ou mais níveis. A arquitetura x86_64 usa tabelas de página de 4 níveis e um tamanho de página de 4&nbsp;KiB. O hardware automaticamente percorre as tabelas de página e armazena em cache as traduções resultantes no translation lookaside buffer (TLB). Este buffer não é atualizado transparentemente e precisa ser esvaziado manualmente em mudanças de tabela de página.
Aprendemos que nosso kernel já executa em cima de paginação e que acessos ilegais à memória causam exceções de page fault. Tentamos acessar as tabelas de página atualmente ativas, mas não conseguimos fazê-lo porque o registrador CR3 armazena um endereço físico que não podemos acessar diretamente do nosso kernel.
## O Que Vem a Seguir?
A próxima postagem explica como implementar suporte para paginação em nosso kernel. Ela apresenta diferentes formas de acessar memória física do nosso kernel, o que torna possível acessar as tabelas de página nas quais nosso kernel executa. Neste ponto, seremos capazes de implementar funções para traduzir endereços virtuais para físicos e para criar novos mapeamentos nas tabelas de página.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,769 @@
+++
title = "Alocação no Heap"
weight = 10
path = "pt-BR/heap-allocation"
date = 2019-06-26
[extra]
chapter = "Gerenciamento de Memória"
# Please update this when updating the translation
translation_based_on_commit = "1ba06fe61c39c1379bd768060c21040b62ff3f0b"
# GitHub usernames of the people that translated this post
translators = ["richarddalves"]
+++
Este post adiciona suporte para alocação no heap ao nosso kernel. Primeiro, ele fornece uma introdução à memória dinâmica e mostra como o verificador de empréstimos previne erros comuns de alocação. Em seguida, implementa a interface básica de alocação do Rust, cria uma região de memória heap e configura uma crate de alocador. Ao final deste post, todos os tipos de alocação e coleção da crate embutida `alloc` estarão disponíveis para o nosso kernel.
<!-- more -->
Este blog é desenvolvido abertamente no [GitHub]. Se você tiver algum problema ou pergunta, por favor abra uma issue lá. Você também pode deixar comentários [no final]. O código-fonte completo para este post pode ser encontrado no branch [`post-10`][post branch].
[GitHub]: https://github.com/phil-opp/blog_os
[at the bottom]: #comments
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
[post branch]: https://github.com/phil-opp/blog_os/tree/post-10
<!-- toc -->
## Variáveis Locais e Estáticas
Atualmente usamos dois tipos de variáveis em nosso kernel: variáveis locais e variáveis `static`. Variáveis locais são armazenadas na [pilha de chamadas] e são válidas apenas até que a função circundante retorne. Variáveis estáticas são armazenadas em um local de memória fixo e sempre vivem pela duração completa do programa.
### Variáveis Locais
Variáveis locais são armazenadas na [pilha de chamadas], que é uma [estrutura de dados de pilha] que suporta operações de `push` e `pop`. Em cada entrada de função, os parâmetros, o endereço de retorno e as variáveis locais da função chamada são colocados na pilha pelo compilador:
[pilha de chamadas]: https://en.wikipedia.org/wiki/Call_stack
[estrutura de dados de pilha]: https://en.wikipedia.org/wiki/Stack_(abstract_data_type)
![Uma função `outer()` e uma função `inner(i: usize)`, onde `outer` chama `inner(1)`. Ambas têm algumas variáveis locais. A pilha de chamadas contém os seguintes slots: as variáveis locais de outer, então o argumento `i = 1`, então o endereço de retorno, então as variáveis locais de inner.](call-stack.svg)
O exemplo acima mostra a pilha de chamadas depois que a função `outer` chamou a função `inner`. Vemos que a pilha de chamadas contém as variáveis locais de `outer` primeiro. Na chamada de `inner`, o parâmetro `1` e o endereço de retorno da função foram colocados na pilha. Então o controle foi transferido para `inner`, que colocou suas variáveis locais na pilha.
Depois que a função `inner` retorna, sua parte da pilha de chamadas é removida novamente e apenas as variáveis locais de `outer` permanecem:
![A pilha de chamadas contendo apenas as variáveis locais de `outer`](call-stack-return.svg)
Vemos que as variáveis locais de `inner` vivem apenas até a função retornar. O compilador Rust impõe esses tempos de vida e gera um erro quando usamos um valor por muito tempo, por exemplo, quando tentamos retornar uma referência a uma variável local:
```rust
fn inner(i: usize) -> &'static u32 {
let z = [1, 2, 3];
&z[i]
}
```
([execute o exemplo no playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=6186a0f3a54f468e1de8894996d12819))
Embora retornar uma referência não faça sentido neste exemplo, há casos em que queremos que uma variável viva mais tempo do que a função. Já vimos tal caso em nosso kernel quando tentamos [carregar uma tabela de descritores de interrupção] e tivemos que usar uma variável `static` para estender o tempo de vida.
[carregar uma tabela de descritores de interrupção]: @/edition-2/posts/05-cpu-exceptions/index.md#loading-the-idt
### Variáveis Estáticas
Variáveis estáticas são armazenadas em um local de memória fixo separado da pilha. Este local de memória é atribuído em tempo de compilação pelo linker e codificado no executável. Variáveis estáticas vivem pela duração completa de execução do programa, então têm o tempo de vida `'static` e sempre podem ser referenciadas de variáveis locais:
![O mesmo exemplo outer/inner, exceto que inner tem um `static Z: [u32; 3] = [1,2,3];` e retorna uma referência `&Z[i]`](call-stack-static.svg)
Quando a função `inner` retorna no exemplo acima, sua parte da pilha de chamadas é destruída. As variáveis estáticas vivem em um intervalo de memória separado que nunca é destruído, então a referência `&Z[1]` ainda é válida após o retorno.
Além do tempo de vida `'static`, variáveis estáticas também têm a propriedade útil de que sua localização é conhecida em tempo de compilação, de modo que nenhuma referência é necessária para acessá-las. Utilizamos essa propriedade para nossa macro `println`: Ao usar um [`Writer` estático] internamente, nenhuma referência `&mut Writer` é necessária para invocar a macro, o que é muito útil em [manipuladores de exceção], onde não temos acesso a variáveis adicionais.
[`Writer` estático]: @/edition-2/posts/03-vga-text-buffer/index.md#a-global-interface
[manipuladores de exceção]: @/edition-2/posts/05-cpu-exceptions/index.md#implementation
No entanto, essa propriedade de variáveis estáticas traz uma desvantagem crucial: elas são somente leitura por padrão. Rust impõe isso porque uma [corrida de dados] ocorreria se, por exemplo, duas threads modificassem uma variável estática ao mesmo tempo. A única maneira de modificar uma variável estática é encapsulá-la em um tipo [`Mutex`], que garante que apenas uma referência `&mut` exista em qualquer momento. Já usamos um `Mutex` para nosso [`Writer` estático do buffer VGA][vga mutex].
[corrida de dados]: https://doc.rust-lang.org/nomicon/races.html
[`Mutex`]: https://docs.rs/spin/0.5.2/spin/struct.Mutex.html
[vga mutex]: @/edition-2/posts/03-vga-text-buffer/index.md#spinlocks
## Memória Dinâmica
Variáveis locais e estáticas já são muito poderosas juntas e permitem a maioria dos casos de uso. No entanto, vimos que ambas têm suas limitações:
- Variáveis locais vivem apenas até o final da função ou bloco circundante. Isso ocorre porque elas vivem na pilha de chamadas e são destruídas depois que a função circundante retorna.
- Variáveis estáticas sempre vivem pela duração completa de execução do programa, então não há maneira de recuperar e reutilizar sua memória quando não são mais necessárias. Além disso, elas têm semântica de propriedade pouco clara e são acessíveis de todas as funções, então precisam ser protegidas por um [`Mutex`] quando queremos modificá-las.
Outra limitação de variáveis locais e estáticas é que elas têm um tamanho fixo. Então elas não podem armazenar uma coleção que cresce dinamicamente quando mais elementos são adicionados. (Existem propostas para [rvalues não dimensionados] em Rust que permitiriam variáveis locais de tamanho dinâmico, mas eles só funcionam em alguns casos específicos.)
[rvalues não dimensionados]: https://github.com/rust-lang/rust/issues/48055
Para contornar essas desvantagens, linguagens de programação frequentemente suportam uma terceira região de memória para armazenar variáveis chamada **heap**. O heap suporta _alocação de memória dinâmica_ em tempo de execução através de duas funções chamadas `allocate` e `deallocate`. Funciona da seguinte maneira: A função `allocate` retorna um pedaço livre de memória do tamanho especificado que pode ser usado para armazenar uma variável. Esta variável então vive até ser liberada chamando a função `deallocate` com uma referência à variável.
Vamos passar por um exemplo:
![A função inner chama `allocate(size_of([u32; 3]))`, escreve `z.write([1,2,3]);`, e retorna `(z as *mut u32).offset(i)`. No valor retornado `y`, a função outer realiza `deallocate(y, size_of(u32))`.](call-stack-heap.svg)
Aqui a função `inner` usa memória heap em vez de variáveis estáticas para armazenar `z`. Primeiro ela aloca um bloco de memória do tamanho necessário, que retorna um [ponteiro bruto] `*mut u32`. Em seguida, usa o método [`ptr::write`] para escrever o array `[1,2,3]` nele. No último passo, usa a função [`offset`] para calcular um ponteiro para o `i`-ésimo elemento e então o retorna. (Note que omitimos alguns casts e blocos unsafe necessários nesta função de exemplo por brevidade.)
[ponteiro bruto]: https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html#dereferencing-a-raw-pointer
[`ptr::write`]: https://doc.rust-lang.org/core/ptr/fn.write.html
[`offset`]: https://doc.rust-lang.org/std/primitive.pointer.html#method.offset
A memória alocada vive até ser explicitamente liberada através de uma chamada para `deallocate`. Assim, o ponteiro retornado ainda é válido mesmo depois que `inner` retornou e sua parte da pilha de chamadas foi destruída. A vantagem de usar memória heap comparada à memória estática é que a memória pode ser reutilizada depois de ser liberada, o que fazemos através da chamada `deallocate` em `outer`. Depois dessa chamada, a situação se parece com isso:
![A pilha de chamadas contém as variáveis locais de `outer`, o heap contém `z[0]` e `z[2]`, mas não mais `z[1]`.](call-stack-heap-freed.svg)
Vemos que o slot `z[1]` está livre novamente e pode ser reutilizado para a próxima chamada `allocate`. No entanto, também vemos que `z[0]` e `z[2]` nunca são liberados porque nunca os desalocamos. Tal bug é chamado de _vazamento de memória_ e é frequentemente a causa do consumo excessivo de memória de programas (imagine apenas o que acontece quando chamamos `inner` repetidamente em um loop). Isso pode parecer ruim, mas existem tipos muito mais perigosos de bugs que podem acontecer com alocação dinâmica.
### Erros Comuns
Além de vazamentos de memória, que são lamentáveis mas não tornam o programa vulnerável a atacantes, existem dois tipos comuns de bugs com consequências mais graves:
- Quando acidentalmente continuamos a usar uma variável depois de chamar `deallocate` nela, temos uma chamada vulnerabilidade **use-after-free**. Tal bug causa comportamento indefinido e pode frequentemente ser explorado por atacantes para executar código arbitrário.
- Quando acidentalmente liberamos uma variável duas vezes, temos uma vulnerabilidade **double-free**. Isso é problemático porque pode liberar uma alocação diferente que foi alocada no mesmo local após a primeira chamada `deallocate`. Assim, pode levar a uma vulnerabilidade use-after-free novamente.
Esses tipos de vulnerabilidades são comumente conhecidos, então pode-se esperar que as pessoas tenham aprendido como evitá-los até agora. Mas não, tais vulnerabilidades ainda são encontradas regularmente, por exemplo esta [vulnerabilidade use-after-free no Linux][linux vulnerability] (2019), que permitiu execução de código arbitrário. Uma busca na web como `use-after-free linux {ano atual}` provavelmente sempre produzirá resultados. Isso mostra que mesmo os melhores programadores nem sempre são capazes de lidar corretamente com memória dinâmica em projetos complexos.
[linux vulnerability]: https://securityboulevard.com/2019/02/linux-use-after-free-vulnerability-found-in-linux-2-6-through-4-20-11/
Para evitar esses problemas, muitas linguagens, como Java ou Python, gerenciam memória dinâmica automaticamente usando uma técnica chamada [_coleta de lixo_]. A ideia é que o programador nunca invoca `deallocate` manualmente. Em vez disso, o programa é regularmente pausado e escaneado em busca de variáveis heap não utilizadas, que são então automaticamente desalocadas. Assim, as vulnerabilidades acima nunca podem ocorrer. As desvantagens são a sobrecarga de desempenho do escaneamento regular e os tempos de pausa provavelmente longos.
[_coleta de lixo_]: https://en.wikipedia.org/wiki/Garbage_collection_(computer_science)
Rust adota uma abordagem diferente para o problema: Ele usa um conceito chamado [_propriedade_] que é capaz de verificar a correção das operações de memória dinâmica em tempo de compilação. Assim, nenhuma coleta de lixo é necessária para evitar as vulnerabilidades mencionadas, o que significa que não há sobrecarga de desempenho. Outra vantagem dessa abordagem é que o programador ainda tem controle refinado sobre o uso de memória dinâmica, assim como com C ou C++.
[_propriedade_]: https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html
### Alocações em Rust
Em vez de deixar o programador chamar `allocate` e `deallocate` manualmente, a biblioteca padrão do Rust fornece tipos de abstração que chamam essas funções implicitamente. O tipo mais importante é [**`Box`**], que é uma abstração para um valor alocado no heap. Ele fornece uma função construtora [`Box::new`] que recebe um valor, chama `allocate` com o tamanho do valor e então move o valor para o slot recém-alocado no heap. Para liberar a memória heap novamente, o tipo `Box` implementa a [trait `Drop`] para chamar `deallocate` quando sai do escopo:
[**`Box`**]: https://doc.rust-lang.org/std/boxed/index.html
[`Box::new`]: https://doc.rust-lang.org/alloc/boxed/struct.Box.html#method.new
[trait `Drop`]: https://doc.rust-lang.org/book/ch15-03-drop.html
```rust
{
let z = Box::new([1,2,3]);
[]
} // z sai do escopo e `deallocate` é chamado
```
Esse padrão tem o nome estranho [_aquisição de recurso é inicialização_] (ou _RAII_ abreviado). Ele se originou em C++, onde é usado para implementar um tipo de abstração similar chamado [`std::unique_ptr`].
[_aquisição de recurso é inicialização_]: https://en.wikipedia.org/wiki/Resource_acquisition_is_initialization
[`std::unique_ptr`]: https://en.cppreference.com/w/cpp/memory/unique_ptr
Tal tipo sozinho não é suficiente para prevenir todos os bugs use-after-free, já que programadores ainda podem manter referências depois que o `Box` sai do escopo e o slot de memória heap correspondente é desalocado:
```rust
let x = {
let z = Box::new([1,2,3]);
&z[1]
}; // z sai do escopo e `deallocate` é chamado
println!("{}", x);
```
É aqui que a propriedade do Rust entra. Ela atribui um [tempo de vida] abstrato a cada referência, que é o escopo no qual a referência é válida. No exemplo acima, a referência `x` é retirada do array `z`, então ela se torna inválida depois que `z` sai do escopo. Quando você [executa o exemplo acima no playground][playground-2], você vê que o compilador Rust de fato gera um erro:
[tempo de vida]: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html
[playground-2]: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=28180d8de7b62c6b4a681a7b1f745a48
```
error[E0597]: `z[_]` does not live long enough
--> src/main.rs:4:9
|
2 | let x = {
| - borrow later stored here
3 | let z = Box::new([1,2,3]);
| - binding `z` declared here
4 | &z[1]
| ^^^^^ borrowed value does not live long enough
5 | }; // z sai do escopo e `deallocate` é chamado
| - `z[_]` dropped here while still borrowed
```
A terminologia pode ser um pouco confusa no início. Pegar uma referência a um valor é chamado de _emprestar_ o valor, já que é similar a um empréstimo na vida real: Você tem acesso temporário a um objeto mas precisa devolvê-lo em algum momento, e você não deve destruí-lo. Ao verificar que todos os empréstimos terminam antes que um objeto seja destruído, o compilador Rust pode garantir que nenhuma situação use-after-free pode ocorrer.
O sistema de propriedade do Rust vai ainda mais longe, prevenindo não apenas bugs use-after-free mas também fornecendo [_segurança de memória_] completa, como linguagens com coleta de lixo como Java ou Python fazem. Adicionalmente, ele garante [_segurança de thread_] e assim é ainda mais seguro que essas linguagens em código multi-thread. E mais importante, todas essas verificações acontecem em tempo de compilação, então não há sobrecarga em tempo de execução comparado ao gerenciamento de memória manual em C.
[_segurança de memória_]: https://en.wikipedia.org/wiki/Memory_safety
[_segurança de thread_]: https://en.wikipedia.org/wiki/Thread_safety
### Casos de Uso
Agora sabemos o básico de alocação de memória dinâmica em Rust, mas quando devemos usá-la? Chegamos muito longe com nosso kernel sem alocação de memória dinâmica, então por que precisamos dela agora?
Primeiro, alocação de memória dinâmica sempre vem com um pouco de sobrecarga de desempenho, já que precisamos encontrar um slot livre no heap para cada alocação. Por essa razão, variáveis locais geralmente são preferíveis, especialmente em código kernel sensível ao desempenho. No entanto, existem casos em que alocação de memória dinâmica é a melhor escolha.
Como regra básica, memória dinâmica é necessária para variáveis que têm um tempo de vida dinâmico ou um tamanho variável. O tipo mais importante com tempo de vida dinâmico é [**`Rc`**], que conta as referências ao seu valor encapsulado e o desaloca depois que todas as referências saíram do escopo. Exemplos de tipos com tamanho variável são [**`Vec`**], [**`String`**] e outros [tipos de coleção] que crescem dinamicamente quando mais elementos são adicionados. Esses tipos funcionam alocando uma quantidade maior de memória quando ficam cheios, copiando todos os elementos e então desalocando a alocação antiga.
[**`Rc`**]: https://doc.rust-lang.org/alloc/rc/index.html
[**`Vec`**]: https://doc.rust-lang.org/alloc/vec/index.html
[**`String`**]: https://doc.rust-lang.org/alloc/string/index.html
[tipos de coleção]: https://doc.rust-lang.org/alloc/collections/index.html
Para o nosso kernel, precisaremos principalmente dos tipos de coleção, por exemplo, para armazenar uma lista de tarefas ativas ao implementar multitarefa em posts futuros.
## A Interface do Alocador
O primeiro passo na implementação de um alocador heap é adicionar uma dependência na crate embutida [`alloc`]. Como a crate [`core`], ela é um subconjunto da biblioteca padrão que adicionalmente contém os tipos de alocação e coleção. Para adicionar a dependência em `alloc`, adicionamos o seguinte ao nosso `lib.rs`:
[`alloc`]: https://doc.rust-lang.org/alloc/
[`core`]: https://doc.rust-lang.org/core/
```rust
// em src/lib.rs
extern crate alloc;
```
Ao contrário de dependências normais, não precisamos modificar o `Cargo.toml`. A razão é que a crate `alloc` vem com o compilador Rust como parte da biblioteca padrão, então o compilador já conhece a crate. Ao adicionar esta declaração `extern crate`, especificamos que o compilador deve tentar incluí-la. (Historicamente, todas as dependências precisavam de uma declaração `extern crate`, que agora é opcional).
Como estamos compilando para um alvo personalizado, não podemos usar a versão pré-compilada de `alloc` que vem com a instalação do Rust. Em vez disso, temos que dizer ao cargo para recompilar a crate a partir do código-fonte. Podemos fazer isso adicionando-a ao array `unstable.build-std` em nosso arquivo `.cargo/config.toml`:
```toml
# em .cargo/config.toml
[unstable]
build-std = ["core", "compiler_builtins", "alloc"]
```
Agora o compilador irá recompilar e incluir a crate `alloc` em nosso kernel.
A razão pela qual a crate `alloc` é desabilitada por padrão em crates `#[no_std]` é que ela tem requisitos adicionais. Quando tentamos compilar nosso projeto agora, veremos esses requisitos como erros:
```
error: no global memory allocator found but one is required; link to std or add
#[global_allocator] to a static item that implements the GlobalAlloc trait.
```
O erro ocorre porque a crate `alloc` requer um alocador heap, que é um objeto que fornece as funções `allocate` e `deallocate`. Em Rust, alocadores heap são descritos pela trait [`GlobalAlloc`], que é mencionada na mensagem de erro. Para definir o alocador heap para a crate, o atributo `#[global_allocator]` deve ser aplicado a uma variável `static` que implementa a trait `GlobalAlloc`.
[`GlobalAlloc`]: https://doc.rust-lang.org/alloc/alloc/trait.GlobalAlloc.html
### A Trait `GlobalAlloc`
A trait [`GlobalAlloc`] define as funções que um alocador heap deve fornecer. A trait é especial porque quase nunca é usada diretamente pelo programador. Em vez disso, o compilador irá automaticamente inserir as chamadas apropriadas aos métodos da trait ao usar os tipos de alocação e coleção de `alloc`.
Como precisaremos implementar a trait para todos os nossos tipos de alocador, vale a pena dar uma olhada mais de perto em sua declaração:
```rust
pub unsafe trait GlobalAlloc {
unsafe fn alloc(&self, layout: Layout) -> *mut u8;
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout);
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { ... }
unsafe fn realloc(
&self,
ptr: *mut u8,
layout: Layout,
new_size: usize
) -> *mut u8 { ... }
}
```
Ela define os dois métodos obrigatórios [`alloc`] e [`dealloc`], que correspondem às funções `allocate` e `deallocate` que usamos em nossos exemplos:
- O método [`alloc`] recebe uma instância [`Layout`] como argumento, que descreve o tamanho e alinhamento desejados que a memória alocada deve ter. Ele retorna um [ponteiro bruto] para o primeiro byte do bloco de memória alocado. Em vez de um valor de erro explícito, o método `alloc` retorna um ponteiro nulo para sinalizar um erro de alocação. Isso é um pouco não idiomático, mas tem a vantagem de que envolver alocadores de sistema existentes é fácil, já que eles usam a mesma convenção.
- O método [`dealloc`] é a contraparte e é responsável por liberar um bloco de memória novamente. Ele recebe dois argumentos: o ponteiro retornado por `alloc` e o `Layout` que foi usado para a alocação.
[`alloc`]: https://doc.rust-lang.org/alloc/alloc/trait.GlobalAlloc.html#tymethod.alloc
[`dealloc`]: https://doc.rust-lang.org/alloc/alloc/trait.GlobalAlloc.html#tymethod.dealloc
[`Layout`]: https://doc.rust-lang.org/alloc/alloc/struct.Layout.html
A trait adicionalmente define os dois métodos [`alloc_zeroed`] e [`realloc`] com implementações padrão:
- O método [`alloc_zeroed`] é equivalente a chamar `alloc` e então definir o bloco de memória alocado para zero, que é exatamente o que a implementação padrão fornecida faz. Uma implementação de alocador pode substituir as implementações padrão com uma implementação personalizada mais eficiente se possível.
- O método [`realloc`] permite aumentar ou diminuir uma alocação. A implementação padrão aloca um novo bloco de memória com o tamanho desejado e copia todo o conteúdo da alocação anterior. Novamente, uma implementação de alocador pode provavelmente fornecer uma implementação mais eficiente deste método, por exemplo, aumentando/diminuindo a alocação no lugar, se possível.
[`alloc_zeroed`]: https://doc.rust-lang.org/alloc/alloc/trait.GlobalAlloc.html#method.alloc_zeroed
[`realloc`]: https://doc.rust-lang.org/alloc/alloc/trait.GlobalAlloc.html#method.realloc
#### Insegurança
Uma coisa a notar é que tanto a trait em si quanto todos os métodos da trait são declarados como `unsafe`:
- A razão para declarar a trait como `unsafe` é que o programador deve garantir que a implementação da trait para um tipo de alocador esteja correta. Por exemplo, o método `alloc` nunca deve retornar um bloco de memória que já está sendo usado em outro lugar porque isso causaria comportamento indefinido.
- Similarmente, a razão pela qual os métodos são `unsafe` é que o chamador deve garantir várias invariantes ao chamar os métodos, por exemplo, que o `Layout` passado para `alloc` especifica um tamanho diferente de zero. Isso não é realmente relevante na prática, já que os métodos normalmente são chamados diretamente pelo compilador, que garante que os requisitos sejam atendidos.
### Um `DummyAllocator`
Agora que sabemos o que um tipo de alocador deve fornecer, podemos criar um alocador dummy simples. Para isso, criamos um novo módulo `allocator`:
```rust
// em src/lib.rs
pub mod allocator;
```
Nosso alocador dummy faz o mínimo absoluto para implementar a trait e sempre retorna um erro quando `alloc` é chamado. Ele se parece com isso:
```rust
// em src/allocator.rs
use alloc::alloc::{GlobalAlloc, Layout};
use core::ptr::null_mut;
pub struct Dummy;
unsafe impl GlobalAlloc for Dummy {
unsafe fn alloc(&self, _layout: Layout) -> *mut u8 {
null_mut()
}
unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {
panic!("dealloc should be never called")
}
}
```
A struct não precisa de nenhum campo, então a criamos como um [tipo de tamanho zero]. Como mencionado acima, sempre retornamos o ponteiro nulo de `alloc`, que corresponde a um erro de alocação. Como o alocador nunca retorna nenhuma memória, uma chamada para `dealloc` nunca deve ocorrer. Por essa razão, simplesmente entramos em pânico no método `dealloc`. Os métodos `alloc_zeroed` e `realloc` têm implementações padrão, então não precisamos fornecer implementações para eles.
[tipo de tamanho zero]: https://doc.rust-lang.org/nomicon/exotic-sizes.html#zero-sized-types-zsts
Agora temos um alocador simples, mas ainda temos que dizer ao compilador Rust que ele deve usar este alocador. É aqui que o atributo `#[global_allocator]` entra.
### O Atributo `#[global_allocator]`
O atributo `#[global_allocator]` diz ao compilador Rust qual instância de alocador ele deve usar como alocador heap global. O atributo só é aplicável a um `static` que implementa a trait `GlobalAlloc`. Vamos registrar uma instância de nosso alocador `Dummy` como o alocador global:
```rust
// em src/allocator.rs
#[global_allocator]
static ALLOCATOR: Dummy = Dummy;
```
Como o alocador `Dummy` é um [tipo de tamanho zero], não precisamos especificar nenhum campo na expressão de inicialização.
Com este static, os erros de compilação devem ser corrigidos. Agora podemos usar os tipos de alocação e coleção de `alloc`. Por exemplo, podemos usar um [`Box`] para alocar um valor no heap:
[`Box`]: https://doc.rust-lang.org/alloc/boxed/struct.Box.html
```rust
// em src/main.rs
extern crate alloc;
use alloc::boxed::Box;
fn kernel_main(boot_info: &'static BootInfo) -> ! {
// […] imprimir "Hello World!", chamar `init`, criar `mapper` e `frame_allocator`
let x = Box::new(41);
// […] chamar `test_main` no modo de teste
println!("It did not crash!");
blog_os::hlt_loop();
}
```
Note que precisamos especificar a declaração `extern crate alloc` em nosso `main.rs` também. Isso é necessário porque as partes `lib.rs` e `main.rs` são tratadas como crates separadas. No entanto, não precisamos criar outro `#[global_allocator]` static porque o alocador global se aplica a todas as crates do projeto. Na verdade, especificar um alocador adicional em outra crate seria um erro.
Quando executamos o código acima, vemos que um pânico ocorre:
![QEMU imprimindo "panicked at `allocation error: Layout { size_: 4, align_: 4 }, src/lib.rs:89:5"](qemu-dummy-output.png)
O pânico ocorre porque a função `Box::new` chama implicitamente a função `alloc` do alocador global. Nosso alocador dummy sempre retorna um ponteiro nulo, então toda alocação falha. Para corrigir isso, precisamos criar um alocador que realmente retorna memória utilizável.
## Criando um Heap do Kernel
Antes de podermos criar um alocador apropriado, primeiro precisamos criar uma região de memória heap da qual o alocador pode alocar memória. Para fazer isso, precisamos definir um intervalo de memória virtual para a região heap e então mapear esta região para frames físicos. Veja o post [_"Introdução ao Paging"_] para uma visão geral de memória virtual e tabelas de página.
[_"Introdução ao Paging"_]: @/edition-2/posts/08-paging-introduction/index.md
O primeiro passo é definir uma região de memória virtual para o heap. Podemos escolher qualquer intervalo de endereço virtual que quisermos, desde que não esteja já sendo usado para uma região de memória diferente. Vamos defini-la como a memória começando no endereço `0x_4444_4444_0000` para que possamos facilmente reconhecer um ponteiro heap mais tarde:
```rust
// em src/allocator.rs
pub const HEAP_START: usize = 0x_4444_4444_0000;
pub const HEAP_SIZE: usize = 100 * 1024; // 100 KiB
```
Definimos o tamanho do heap para 100&nbsp;KiB por enquanto. Se precisarmos de mais espaço no futuro, podemos simplesmente aumentá-lo.
Se tentássemos usar esta região heap agora, uma falha de página ocorreria, já que a região de memória virtual ainda não está mapeada para memória física. Para resolver isso, criamos uma função `init_heap` que mapeia as páginas heap usando a [API `Mapper`] que introduzimos no post [_"Implementação de Paging"_]:
[API `Mapper`]: @/edition-2/posts/09-paging-implementation/index.md#using-offsetpagetable
[_"Implementação de Paging"_]: @/edition-2/posts/09-paging-implementation/index.md
```rust
// em src/allocator.rs
use x86_64::{
structures::paging::{
mapper::MapToError, FrameAllocator, Mapper, Page, PageTableFlags, Size4KiB,
},
VirtAddr,
};
pub fn init_heap(
mapper: &mut impl Mapper<Size4KiB>,
frame_allocator: &mut impl FrameAllocator<Size4KiB>,
) -> Result<(), MapToError<Size4KiB>> {
let page_range = {
let heap_start = VirtAddr::new(HEAP_START as u64);
let heap_end = heap_start + HEAP_SIZE - 1u64;
let heap_start_page = Page::containing_address(heap_start);
let heap_end_page = Page::containing_address(heap_end);
Page::range_inclusive(heap_start_page, heap_end_page)
};
for page in page_range {
let frame = frame_allocator
.allocate_frame()
.ok_or(MapToError::FrameAllocationFailed)?;
let flags = PageTableFlags::PRESENT | PageTableFlags::WRITABLE;
unsafe {
mapper.map_to(page, frame, flags, frame_allocator)?.flush()
};
}
Ok(())
}
```
A função recebe referências mutáveis para uma instância [`Mapper`] e uma instância [`FrameAllocator`], ambas limitadas a páginas de 4&nbsp;KiB usando [`Size4KiB`] como o parâmetro genérico. O valor de retorno da função é um [`Result`] com o tipo unitário `()` como a variante de sucesso e um [`MapToError`] como a variante de erro, que é o tipo de erro retornado pelo método [`Mapper::map_to`]. Reutilizar o tipo de erro faz sentido aqui porque o método `map_to` é a principal fonte de erros nesta função.
[`Mapper`]:https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Mapper.html
[`FrameAllocator`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/trait.FrameAllocator.html
[`Size4KiB`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page/enum.Size4KiB.html
[`Result`]: https://doc.rust-lang.org/core/result/enum.Result.html
[`MapToError`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/enum.MapToError.html
[`Mapper::map_to`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Mapper.html#method.map_to
A implementação pode ser dividida em duas partes:
- **Criando o intervalo de páginas:**: Para criar um intervalo das páginas que queremos mapear, convertemos o ponteiro `HEAP_START` para um tipo [`VirtAddr`]. Então calculamos o endereço final do heap a partir dele adicionando o `HEAP_SIZE`. Queremos um limite inclusivo (o endereço do último byte do heap), então subtraímos 1. Em seguida, convertemos os endereços em tipos [`Page`] usando a função [`containing_address`]. Finalmente, criamos um intervalo de páginas das páginas inicial e final usando a função [`Page::range_inclusive`].
- **Mapeando as páginas:** O segundo passo é mapear todas as páginas do intervalo de páginas que acabamos de criar. Para isso, iteramos sobre essas páginas usando um loop `for`. Para cada página, fazemos o seguinte:
- Alocamos um frame físico para o qual a página deve ser mapeada usando o método [`FrameAllocator::allocate_frame`]. Este método retorna [`None`] quando não há mais frames disponíveis. Lidamos com esse caso mapeando-o para um erro [`MapToError::FrameAllocationFailed`] através do método [`Option::ok_or`] e então aplicando o [operador de ponto de interrogação] para retornar cedo em caso de erro.
- Definimos a flag `PRESENT` obrigatória e a flag `WRITABLE` para a página. Com essas flags, tanto acessos de leitura quanto de escrita são permitidos, o que faz sentido para memória heap.
- Usamos o método [`Mapper::map_to`] para criar o mapeamento na tabela de páginas ativa. O método pode falhar, então usamos o [operador de ponto de interrogação] novamente para encaminhar o erro ao chamador. Em caso de sucesso, o método retorna uma instância [`MapperFlush`] que podemos usar para atualizar o [_buffer de tradução lookaside_] usando o método [`flush`].
[`VirtAddr`]: https://docs.rs/x86_64/0.14.2/x86_64/addr/struct.VirtAddr.html
[`Page`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page/struct.Page.html
[`containing_address`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page/struct.Page.html#method.containing_address
[`Page::range_inclusive`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page/struct.Page.html#method.range_inclusive
[`FrameAllocator::allocate_frame`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/trait.FrameAllocator.html#tymethod.allocate_frame
[`None`]: https://doc.rust-lang.org/core/option/enum.Option.html#variant.None
[`MapToError::FrameAllocationFailed`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/enum.MapToError.html#variant.FrameAllocationFailed
[`Option::ok_or`]: https://doc.rust-lang.org/core/option/enum.Option.html#method.ok_or
[operador de ponto de interrogação]: https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html
[`MapperFlush`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.MapperFlush.html
[_buffer de tradução lookaside_]: @/edition-2/posts/08-paging-introduction/index.md#the-translation-lookaside-buffer
[`flush`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.MapperFlush.html#method.flush
O passo final é chamar esta função de nossa `kernel_main`:
```rust
// em src/main.rs
fn kernel_main(boot_info: &'static BootInfo) -> ! {
use blog_os::allocator; // nova importação
use blog_os::memory::{self, BootInfoFrameAllocator};
println!("Hello World{}", "!");
blog_os::init();
let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset);
let mut mapper = unsafe { memory::init(phys_mem_offset) };
let mut frame_allocator = unsafe {
BootInfoFrameAllocator::init(&boot_info.memory_map)
};
// novo
allocator::init_heap(&mut mapper, &mut frame_allocator)
.expect("heap initialization failed");
let x = Box::new(41);
// […] chamar `test_main` no modo de teste
println!("It did not crash!");
blog_os::hlt_loop();
}
```
Mostramos a função completa para contexto aqui. As únicas linhas novas são a importação `blog_os::allocator` e a chamada para a função `allocator::init_heap`. No caso de a função `init_heap` retornar um erro, entramos em pânico usando o método [`Result::expect`], já que atualmente não há maneira sensata de lidarmos com este erro.
[`Result::expect`]: https://doc.rust-lang.org/core/result/enum.Result.html#method.expect
Agora temos uma região de memória heap mapeada que está pronta para ser usada. A chamada `Box::new` ainda usa nosso alocador `Dummy` antigo, então você ainda verá o erro "out of memory" quando executá-lo. Vamos corrigir isso usando um alocador apropriado.
## Usando uma Crate de Alocador
Como implementar um alocador é um tanto complexo, começamos usando uma crate de alocador externa. Aprenderemos como implementar nosso próprio alocador no próximo post.
Uma crate de alocador simples para aplicações `no_std` é a crate [`linked_list_allocator`]. Seu nome vem do fato de que ela usa uma estrutura de dados de lista encadeada para acompanhar as regiões de memória desalocadas. Veja o próximo post para uma explicação mais detalhada dessa abordagem.
Para usar a crate, primeiro precisamos adicionar uma dependência nela em nosso `Cargo.toml`:
[`linked_list_allocator`]: https://github.com/phil-opp/linked-list-allocator/
```toml
# em Cargo.toml
[dependencies]
linked_list_allocator = "0.9.0"
```
Então podemos substituir nosso alocador dummy pelo alocador fornecido pela crate:
```rust
// em src/allocator.rs
use linked_list_allocator::LockedHeap;
#[global_allocator]
static ALLOCATOR: LockedHeap = LockedHeap::empty();
```
A struct é chamada `LockedHeap` porque usa o tipo [`spinning_top::Spinlock`] para sincronização. Isso é necessário porque múltiplas threads podem acessar o static `ALLOCATOR` ao mesmo tempo. Como sempre, ao usar um spinlock ou um mutex, precisamos ter cuidado para não causar acidentalmente um deadlock. Isso significa que não devemos realizar nenhuma alocação em manipuladores de interrupção, já que eles podem executar em um momento arbitrário e podem interromper uma alocação em andamento.
[`spinning_top::Spinlock`]: https://docs.rs/spinning_top/0.1.0/spinning_top/type.Spinlock.html
Definir o `LockedHeap` como alocador global não é suficiente. A razão é que usamos a função construtora [`empty`], que cria um alocador sem nenhuma memória de suporte. Como nosso alocador dummy, ele sempre retorna um erro em `alloc`. Para corrigir isso, precisamos inicializar o alocador após criar o heap:
[`empty`]: https://docs.rs/linked_list_allocator/0.9.0/linked_list_allocator/struct.LockedHeap.html#method.empty
```rust
// em src/allocator.rs
pub fn init_heap(
mapper: &mut impl Mapper<Size4KiB>,
frame_allocator: &mut impl FrameAllocator<Size4KiB>,
) -> Result<(), MapToError<Size4KiB>> {
// […] mapear todas as páginas heap para frames físicos
// novo
unsafe {
ALLOCATOR.lock().init(HEAP_START, HEAP_SIZE);
}
Ok(())
}
```
Usamos o método [`lock`] no spinlock interno do tipo `LockedHeap` para obter uma referência exclusiva à instância [`Heap`] encapsulada, na qual então chamamos o método [`init`] com os limites do heap como argumentos. Como a função [`init`] já tenta escrever na memória heap, devemos inicializar o heap somente _depois_ de mapear as páginas heap.
[`lock`]: https://docs.rs/lock_api/0.3.3/lock_api/struct.Mutex.html#method.lock
[`Heap`]: https://docs.rs/linked_list_allocator/0.9.0/linked_list_allocator/struct.Heap.html
[`init`]: https://docs.rs/linked_list_allocator/0.9.0/linked_list_allocator/struct.Heap.html#method.init
Depois de inicializar o heap, agora podemos usar todos os tipos de alocação e coleção da crate embutida [`alloc`] sem erro:
```rust
// em src/main.rs
use alloc::{boxed::Box, vec, vec::Vec, rc::Rc};
fn kernel_main(boot_info: &'static BootInfo) -> ! {
// […] inicializar interrupções, mapper, frame_allocator, heap
// alocar um número no heap
let heap_value = Box::new(41);
println!("heap_value at {:p}", heap_value);
// criar um vetor de tamanho dinâmico
let mut vec = Vec::new();
for i in 0..500 {
vec.push(i);
}
println!("vec at {:p}", vec.as_slice());
// criar um vetor com contagem de referências -> será liberado quando a contagem chegar a 0
let reference_counted = Rc::new(vec![1, 2, 3]);
let cloned_reference = reference_counted.clone();
println!("current reference count is {}", Rc::strong_count(&cloned_reference));
core::mem::drop(reference_counted);
println!("reference count is {} now", Rc::strong_count(&cloned_reference));
// […] chamar `test_main` no contexto de teste
println!("It did not crash!");
blog_os::hlt_loop();
}
```
Este exemplo de código mostra alguns usos dos tipos [`Box`], [`Vec`] e [`Rc`]. Para os tipos `Box` e `Vec`, imprimimos os ponteiros heap subjacentes usando o [especificador de formatação `{:p}`]. Para mostrar `Rc`, criamos um valor heap com contagem de referências e usamos a função [`Rc::strong_count`] para imprimir a contagem de referências atual antes e depois de descartar uma instância (usando [`core::mem::drop`]).
[`Vec`]: https://doc.rust-lang.org/alloc/vec/
[`Rc`]: https://doc.rust-lang.org/alloc/rc/
[especificador de formatação `{:p}`]: https://doc.rust-lang.org/core/fmt/trait.Pointer.html
[`Rc::strong_count`]: https://doc.rust-lang.org/alloc/rc/struct.Rc.html#method.strong_count
[`core::mem::drop`]: https://doc.rust-lang.org/core/mem/fn.drop.html
Quando o executamos, vemos o seguinte:
![QEMU imprimindo `
heap_value at 0x444444440000
vec at 0x4444444408000
current reference count is 2
reference count is 1 now
](qemu-alloc-showcase.png)
Como esperado, vemos que os valores `Box` e `Vec` vivem no heap, como indicado pelo ponteiro começando com o prefixo `0x_4444_4444_*`. O valor com contagem de referências também se comporta como esperado, com a contagem de referências sendo 2 após a chamada `clone`, e 1 novamente depois que uma das instâncias foi descartada.
A razão pela qual o vetor começa no offset `0x800` não é que o valor encaixotado seja `0x800` bytes grande, mas as [realocações] que ocorrem quando o vetor precisa aumentar sua capacidade. Por exemplo, quando a capacidade do vetor é 32 e tentamos adicionar o próximo elemento, o vetor aloca um novo array de suporte com capacidade de 64 nos bastidores e copia todos os elementos. Então ele libera a alocação antiga.
[realocações]: https://doc.rust-lang.org/alloc/vec/struct.Vec.html#capacity-and-reallocation
É claro que existem muitos mais tipos de alocação e coleção na crate `alloc` que agora podemos usar todos em nosso kernel, incluindo:
- o ponteiro com contagem de referências thread-safe [`Arc`]
- o tipo de string proprietária [`String`] e a macro [`format!`]
- [`LinkedList`]
- o buffer circular crescente [`VecDeque`]
- a fila de prioridade [`BinaryHeap`]
- [`BTreeMap`] e [`BTreeSet`]
[`Arc`]: https://doc.rust-lang.org/alloc/sync/struct.Arc.html
[`String`]: https://doc.rust-lang.org/alloc/string/struct.String.html
[`format!`]: https://doc.rust-lang.org/alloc/macro.format.html
[`LinkedList`]: https://doc.rust-lang.org/alloc/collections/linked_list/struct.LinkedList.html
[`VecDeque`]: https://doc.rust-lang.org/alloc/collections/vec_deque/struct.VecDeque.html
[`BinaryHeap`]: https://doc.rust-lang.org/alloc/collections/binary_heap/struct.BinaryHeap.html
[`BTreeMap`]: https://doc.rust-lang.org/alloc/collections/btree_map/struct.BTreeMap.html
[`BTreeSet`]: https://doc.rust-lang.org/alloc/collections/btree_set/struct.BTreeSet.html
Esses tipos se tornarão muito úteis quando quisermos implementar listas de threads, filas de escalonamento ou suporte para async/await.
## Adicionando um Teste
Para garantir que não quebremos acidentalmente nosso novo código de alocação, devemos adicionar um teste de integração para ele. Começamos criando um novo arquivo `tests/heap_allocation.rs` com o seguinte conteúdo:
```rust
// em tests/heap_allocation.rs
#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(blog_os::test_runner)]
#![reexport_test_harness_main = "test_main"]
extern crate alloc;
use bootloader::{entry_point, BootInfo};
use core::panic::PanicInfo;
entry_point!(main);
fn main(boot_info: &'static BootInfo) -> ! {
unimplemented!();
}
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
blog_os::test_panic_handler(info)
}
```
Reutilizamos as funções `test_runner` e `test_panic_handler` de nosso `lib.rs`. Como queremos testar alocações, habilitamos a crate `alloc` através da declaração `extern crate alloc`. Para mais informações sobre o boilerplate de teste, confira o post [_Testing_].
[_Testing_]: @/edition-2/posts/04-testing/index.md
A implementação da função `main` se parece com isso:
```rust
// em tests/heap_allocation.rs
fn main(boot_info: &'static BootInfo) -> ! {
use blog_os::allocator;
use blog_os::memory::{self, BootInfoFrameAllocator};
use x86_64::VirtAddr;
blog_os::init();
let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset);
let mut mapper = unsafe { memory::init(phys_mem_offset) };
let mut frame_allocator = unsafe {
BootInfoFrameAllocator::init(&boot_info.memory_map)
};
allocator::init_heap(&mut mapper, &mut frame_allocator)
.expect("heap initialization failed");
test_main();
loop {}
}
```
Ela é muito similar à função `kernel_main` em nosso `main.rs`, com as diferenças de que não invocamos `println`, não incluímos nenhuma alocação de exemplo, e chamamos `test_main` incondicionalmente.
Agora estamos prontos para adicionar alguns casos de teste. Primeiro, adicionamos um teste que realiza algumas alocações simples usando [`Box`] e verifica os valores alocados para garantir que as alocações básicas funcionam:
```rust
// em tests/heap_allocation.rs
use alloc::boxed::Box;
#[test_case]
fn simple_allocation() {
let heap_value_1 = Box::new(41);
let heap_value_2 = Box::new(13);
assert_eq!(*heap_value_1, 41);
assert_eq!(*heap_value_2, 13);
}
```
Mais importante, este teste verifica que nenhum erro de alocação ocorre.
Em seguida, construímos iterativamente um vetor grande, para testar tanto alocações grandes quanto múltiplas alocações (devido a realocações):
```rust
// em tests/heap_allocation.rs
use alloc::vec::Vec;
#[test_case]
fn large_vec() {
let n = 1000;
let mut vec = Vec::new();
for i in 0..n {
vec.push(i);
}
assert_eq!(vec.iter().sum::<u64>(), (n - 1) * n / 2);
}
```
Verificamos a soma comparando-a com a fórmula para a [soma parcial n-ésima]. Isso nos dá alguma confiança de que os valores alocados estão todos corretos.
[soma parcial n-ésima]: https://en.wikipedia.org/wiki/1_%2B_2_%2B_3_%2B_4_%2B_%E2%8B%AF#Partial_sums
Como terceiro teste, criamos dez mil alocações uma após a outra:
```rust
// em tests/heap_allocation.rs
use blog_os::allocator::HEAP_SIZE;
#[test_case]
fn many_boxes() {
for i in 0..HEAP_SIZE {
let x = Box::new(i);
assert_eq!(*x, i);
}
}
```
Este teste garante que o alocador reutiliza memória liberada para alocações subsequentes, já que ficaria sem memória caso contrário. Isso pode parecer um requisito óbvio para um alocador, mas existem designs de alocador que não fazem isso. Um exemplo é o design de alocador bump que será explicado no próximo post.
Vamos executar nosso novo teste de integração:
```
> cargo test --test heap_allocation
[…]
Running 3 tests
simple_allocation... [ok]
large_vec... [ok]
many_boxes... [ok]
```
Todos os três testes foram bem-sucedidos! Você também pode invocar `cargo test` (sem o argumento `--test`) para executar todos os testes unitários e de integração.
## Resumo
Este post deu uma introdução à memória dinâmica e explicou por que e onde ela é necessária. Vimos como o verificador de empréstimos do Rust previne vulnerabilidades comuns e aprendemos como a API de alocação do Rust funciona.
Depois de criar uma implementação mínima da interface de alocador do Rust usando um alocador dummy, criamos uma região de memória heap apropriada para o nosso kernel. Para isso, definimos um intervalo de endereço virtual para o heap e então mapeamos todas as páginas desse intervalo para frames físicos usando o `Mapper` e `FrameAllocator` do post anterior.
Finalmente, adicionamos uma dependência na crate `linked_list_allocator` para adicionar um alocador apropriado ao nosso kernel. Com este alocador, pudemos usar `Box`, `Vec` e outros tipos de alocação e coleção da crate `alloc`.
## O que vem a seguir?
Embora já tenhamos adicionado suporte para alocação heap neste post, deixamos a maior parte do trabalho para a crate `linked_list_allocator`. O próximo post mostrará em detalhes como um alocador pode ser implementado do zero. Ele apresentará múltiplos designs de alocador possíveis, mostrará como implementar versões simples deles e explicará suas vantagens e desvantagens.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
+++
title = "Posts"
sort_by = "weight"
insert_anchor_links = "left"
render = false
page_template = "edition-2/page.html"
+++

View File

@@ -3,6 +3,7 @@ extend-exclude = [
"*.svg",
"*.fr.md",
"*.es.md",
"*.pt-BR.md",
"blog/config.toml",
]