mirror of
https://github.com/phil-opp/blog_os.git
synced 2025-12-16 14:27:49 +00:00
Merge d9de86f6df into 4de9527ef8
This commit is contained in:
13
blog/content/_index.es.md
Normal file
13
blog/content/_index.es.md
Normal file
@@ -0,0 +1,13 @@
|
||||
+++
|
||||
template = "edition-2/index.html"
|
||||
+++
|
||||
|
||||
<h1 style="visibility: hidden; height: 0px; margin: 0px; padding: 0px;">Escribiendo un sistema operativo en Rust</h1>
|
||||
|
||||
<div class="front-page-introduction">
|
||||
|
||||
Esta serie de blogs crea un pequeño sistema operativo en el [lenguaje de programación Rust](https://www.rust-lang.org/). Cada publicación es un pequeño tutorial e incluye todo el código necesario, para que puedas seguir los pasos si lo deseas. El código fuente también está disponible en el [repositorio correspondiente de Github](https://github.com/phil-opp/blog_os).
|
||||
|
||||
Última publicación: <!-- latest-post -->
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,519 @@
|
||||
+++
|
||||
title = "Un Binario Rust Autónomo"
|
||||
weight = 1
|
||||
path = "freestanding-rust-binary"
|
||||
date = 2018-02-10
|
||||
|
||||
[extra]
|
||||
chapter = "Bare Bones"
|
||||
+++
|
||||
|
||||
El primer paso para crear nuestro propio kernel de sistema operativo es crear un ejecutable en Rust que no enlace con la biblioteca estándar. Esto hace posible ejecutar código Rust directamente en el [bare metal] sin un sistema operativo subyacente.
|
||||
|
||||
[bare metal]: https://en.wikipedia.org/wiki/Bare_machine
|
||||
|
||||
<!-- more -->
|
||||
|
||||
Este blog se desarrolla abiertamente en [GitHub]. Si tienes algún problema o pregunta, por favor abre un issue allí. También puedes dejar comentarios [al final]. El código fuente completo para esta publicación se encuentra en la rama [`post-01`][post branch].
|
||||
|
||||
[GitHub]: https://github.com/phil-opp/blog_os
|
||||
[al final]: #comments
|
||||
<!-- solución para el verificador de anclajes de zola (el objetivo está en la plantilla): <a id="comments"> -->
|
||||
[post branch]: https://github.com/phil-opp/blog_os/tree/post-01
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## Introducción
|
||||
Para escribir un kernel de sistema operativo, necesitamos código que no dependa de características del sistema operativo. Esto significa que no podemos usar hilos, archivos, memoria dinámica, redes, números aleatorios, salida estándar ni ninguna otra característica que requiera abstracciones de sistema operativo o hardware específico. Esto tiene sentido, ya que estamos intentando escribir nuestro propio sistema operativo y nuestros propios controladores.
|
||||
|
||||
Esto implica que no podemos usar la mayor parte de la [biblioteca estándar de Rust], pero hay muchas características de Rust que sí _podemos_ usar. Por ejemplo, podemos utilizar [iteradores], [closures], [pattern matching], [option] y [result], [formateo de cadenas] y, por supuesto, el [sistema de ownership]. Estas características hacen posible escribir un kernel de una manera muy expresiva y de alto nivel, sin preocuparnos por el [comportamiento indefinido] o la [seguridad de la memoria].
|
||||
|
||||
[option]: https://doc.rust-lang.org/core/option/
|
||||
[result]: https://doc.rust-lang.org/core/result/
|
||||
[biblioteca estándar de Rust]: 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
|
||||
[formateo de cadenas]: https://doc.rust-lang.org/core/macro.write.html
|
||||
[sistema de ownership]: https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html
|
||||
[comportamiento indefinido]: https://www.nayuki.io/page/undefined-behavior-in-c-and-cplusplus-programs
|
||||
[seguridad de la memoria]: https://tonyarcieri.com/it-s-time-for-a-memory-safety-intervention
|
||||
|
||||
Para crear un kernel de sistema operativo en Rust, necesitamos crear un ejecutable que pueda ejecutarse sin un sistema operativo subyacente. Dicho ejecutable se llama frecuentemente un ejecutable “autónomo” o de “bare metal”.
|
||||
|
||||
Esta publicación describe los pasos necesarios para crear un binario autónomo en Rust y explica por qué son necesarios. Si solo te interesa un ejemplo mínimo, puedes **[saltar al resumen](#summary)**.
|
||||
|
||||
## Deshabilitando la Biblioteca Estándar
|
||||
Por defecto, todos los crates de Rust enlazan con la [biblioteca estándar], que depende del sistema operativo para características como hilos, archivos o redes. También depende de la biblioteca estándar de C, `libc`, que interactúa estrechamente con los servicios del sistema operativo. Como nuestro plan es escribir un sistema operativo, no podemos usar ninguna biblioteca que dependa del sistema operativo. Por lo tanto, tenemos que deshabilitar la inclusión automática de la biblioteca estándar mediante el atributo [`no_std`].
|
||||
|
||||
[biblioteca estándar]: https://doc.rust-lang.org/std/
|
||||
[`no_std`]: https://doc.rust-lang.org/1.30.0/book/first-edition/using-rust-without-the-standard-library.html
|
||||
|
||||
Comenzamos creando un nuevo proyecto de aplicación en Cargo. La forma más fácil de hacerlo es a través de la línea de comandos:
|
||||
|
||||
|
||||
```
|
||||
cargo new blog_os --bin --edition 2018
|
||||
```
|
||||
|
||||
Nombré el proyecto `blog_os`, pero, por supuesto, puedes elegir tu propio nombre. La bandera `--bin` especifica que queremos crear un binario ejecutable (en contraste con una biblioteca), y la bandera `--edition 2018` indica que queremos usar la [edición 2018] de Rust para nuestro crate. Al ejecutar el comando, Cargo crea la siguiente estructura de directorios para nosotros:
|
||||
|
||||
[2018 edition]: https://doc.rust-lang.org/nightly/edition-guide/rust-2018/index.html
|
||||
|
||||
```
|
||||
blog_os
|
||||
├── Cargo.toml
|
||||
└── src
|
||||
└── main.rs
|
||||
```
|
||||
|
||||
El archivo `Cargo.toml` contiene la configuración del crate, como el nombre del crate, el autor, el número de [versión semántica] y las dependencias. El archivo `src/main.rs` contiene el módulo raíz de nuestro crate y nuestra función `main`. Puedes compilar tu crate utilizando `cargo build` y luego ejecutar el binario compilado `blog_os` ubicado en la subcarpeta `target/debug`.
|
||||
|
||||
[semantic version]: https://semver.org/
|
||||
|
||||
### El Atributo `no_std`
|
||||
|
||||
Actualmente, nuestro crate enlaza implícitamente con la biblioteca estándar. Intentemos deshabilitar esto añadiendo el [`atributo no_std`]:
|
||||
|
||||
```rust
|
||||
// main.rs
|
||||
|
||||
#![no_std]
|
||||
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
```
|
||||
|
||||
Cuando intentamos compilarlo ahora (ejecutando `cargo build`), ocurre el siguiente error:
|
||||
|
||||
```
|
||||
error: cannot find macro `println!` in this scope
|
||||
--> src/main.rs:4:5
|
||||
|
|
||||
4 | println!("Hello, world!");
|
||||
| ^^^^^^^
|
||||
```
|
||||
|
||||
La razón de este error es que la [macro `println`] forma parte de la biblioteca estándar, la cual ya no estamos incluyendo. Por lo tanto, ya no podemos imprimir cosas. Esto tiene sentido, ya que `println` escribe en la [salida estándar], que es un descriptor de archivo especial proporcionado por el sistema operativo.
|
||||
|
||||
[macro `println`]: https://doc.rust-lang.org/std/macro.println.html
|
||||
[salida estándar]: https://en.wikipedia.org/wiki/Standard_streams#Standard_output_.28stdout.29
|
||||
|
||||
Así que eliminemos la impresión e intentemos de nuevo con una función `main` vacía:
|
||||
|
||||
```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`
|
||||
```
|
||||
|
||||
Ahora el compilador indica que falta una función `#[panic_handler]` y un _elemento de lenguaje_ (_language item_).
|
||||
|
||||
## Implementación de Panic
|
||||
|
||||
El atributo `panic_handler` define la función que el compilador invoca cuando ocurre un [panic]. La biblioteca estándar proporciona su propia función de panico, pero en un entorno `no_std` debemos definirla nosotros mismos:
|
||||
|
||||
[panic]: https://doc.rust-lang.org/stable/book/ch09-01-unrecoverable-errors-with-panic.html
|
||||
|
||||
```rust
|
||||
// in main.rs
|
||||
|
||||
use core::panic::PanicInfo;
|
||||
|
||||
/// This function is called on panic.
|
||||
#[panic_handler]
|
||||
fn panic(_info: &PanicInfo) -> ! {
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
El [parámetro `PanicInfo`][PanicInfo] contiene el archivo y la línea donde ocurrió el panic, así como el mensaje opcional del panic. La función no debería retornar nunca, por lo que se marca como una [función divergente][diverging function] devolviendo el [tipo “never”][“never” type] `!`. Por ahora, no hay mucho que podamos hacer en esta función, así que simplemente entramos en un bucle infinito.
|
||||
|
||||
[PanicInfo]: https://doc.rust-lang.org/nightly/core/panic/struct.PanicInfo.html
|
||||
[diverging function]: https://doc.rust-lang.org/1.30.0/book/first-edition/functions.html#diverging-functions
|
||||
[“never” type]: https://doc.rust-lang.org/nightly/std/primitive.never.html
|
||||
|
||||
## El Elemento de Lenguaje `eh_personality`
|
||||
|
||||
Los elementos de lenguaje son funciones y tipos especiales que el compilador requiere internamente. Por ejemplo, el trait [`Copy`] es un elemento de lenguaje que indica al compilador qué tipos tienen [_semántica de copia_][`Copy`]. Si observamos su [implementación][copy code], veremos que tiene el atributo especial `#[lang = "copy"]`, que lo define como un elemento de lenguaje.
|
||||
|
||||
[`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
|
||||
|
||||
Aunque es posible proporcionar implementaciones personalizadas de elementos de lenguaje, esto debería hacerse solo como último recurso. La razón es que los elementos de lenguaje son detalles de implementación altamente inestables y ni siquiera están verificados por tipos (el compilador no comprueba si una función tiene los tipos de argumento correctos). Afortunadamente, hay una forma más estable de solucionar el error relacionado con el elemento de lenguaje mencionado.
|
||||
|
||||
El [elemento de lenguaje `eh_personality`][`eh_personality` language item] marca una función utilizada para implementar el [desenrollado de pila][stack unwinding]. Por defecto, Rust utiliza unwinding para ejecutar los destructores de todas las variables de pila activas en caso de un [pánico][panic]. Esto asegura que toda la memoria utilizada sea liberada y permite que el hilo principal capture el pánico y continúe ejecutándose. Sin embargo, el unwinding es un proceso complicado y requiere algunas bibliotecas específicas del sistema operativo (por ejemplo, [libunwind] en Linux o [manejadores estructurados de excepciones][structured exception handling] en Windows), por lo que no queremos usarlo en nuestro sistema operativo.
|
||||
|
||||
[`eh_personality` language item]: 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/
|
||||
[structured exception handling]: https://docs.microsoft.com/en-us/windows/win32/debug/structured-exception-handling
|
||||
[panic]: https://doc.rust-lang.org/book/ch09-01-unrecoverable-errors-with-panic.html
|
||||
|
||||
### Deshabilitando el Unwinding
|
||||
|
||||
Existen otros casos de uso en los que el no es deseable, por lo que Rust proporciona una opción para [abortar en caso de pánico][abort on panic]. Esto desactiva la generación de información de símbolos de unwinding y, por lo tanto, reduce considerablemente el tamaño del binario. Hay múltiples lugares donde podemos deshabilitar el unwinding. La forma más sencilla es agregar las siguientes líneas a nuestro archivo `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[profile.dev]
|
||||
panic = "abort"
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
```
|
||||
|
||||
Esto establece la estrategia de pánico en `abort` tanto para el perfil `dev` (utilizado en `cargo build`) como para el perfil `release` (utilizado en `cargo build --release`). Ahora, el elemento de lenguaje `eh_personality` ya no debería ser necesario.
|
||||
|
||||
[abort on panic]: https://github.com/rust-lang/rust/pull/32900
|
||||
|
||||
Ahora hemos solucionado ambos errores anteriores. Sin embargo, si intentamos compilarlo ahora, ocurre otro error:
|
||||
|
||||
```
|
||||
> cargo build
|
||||
error: requires `start` lang_item
|
||||
```
|
||||
|
||||
Nuestro programa carece del elemento de lenguaje `start`, que define el punto de entrada.
|
||||
|
||||
## El Atributo `start`
|
||||
|
||||
Podría pensarse que la función `main` es la primera que se ejecuta al correr un programa. Sin embargo, la mayoría de los lenguajes tienen un [sistema de tiempo de ejecución][runtime system], encargado de tareas como la recolección de basura (por ejemplo, en Java) o los hilos de software (por ejemplo, goroutines en Go). Este sistema de tiempo de ejecución necesita ejecutarse antes de `main`, ya que debe inicializarse.
|
||||
|
||||
[runtime system]: https://en.wikipedia.org/wiki/Runtime_system
|
||||
|
||||
En un binario típico de Rust que enlaza con la biblioteca estándar, la ejecución comienza en una biblioteca de tiempo de ejecución de C llamada `crt0` ("C runtime zero"), que configura el entorno para una aplicación en C. Esto incluye la creación de una pila y la colocación de los argumentos en los registros adecuados. Luego, el tiempo de ejecución de C invoca el [punto de entrada del tiempo de ejecución de Rust][rt::lang_start], que está marcado por el elemento de lenguaje `start`. Rust tiene un tiempo de ejecución muy minimalista, que se encarga de tareas menores como configurar los guardias de desbordamiento de pila o imprimir un backtrace en caso de pánico. Finalmente, el tiempo de ejecución llama a la función `main`.
|
||||
|
||||
[rt::lang_start]: https://github.com/rust-lang/rust/blob/bb4d1491466d8239a7a5fd68bd605e3276e97afb/src/libstd/rt.rs#L32-L73
|
||||
|
||||
Nuestro ejecutable autónomo no tiene acceso al tiempo de ejecución de Rust ni a `crt0`, por lo que necesitamos definir nuestro propio punto de entrada. Implementar el elemento de lenguaje `start` no ayudaría, ya que aún requeriría `crt0`. En su lugar, debemos sobrescribir directamente el punto de entrada de `crt0`.
|
||||
|
||||
### Sobrescribiendo el Punto de Entrada
|
||||
|
||||
Para indicar al compilador de Rust que no queremos usar la cadena normal de puntos de entrada, agregamos el atributo `#![no_main]`:
|
||||
|
||||
```rust
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
|
||||
use core::panic::PanicInfo;
|
||||
|
||||
/// This function is called on panic.
|
||||
#[panic_handler]
|
||||
fn panic(_info: &PanicInfo) -> ! {
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
Podrás notar que eliminamos la función `main`. La razón es que una función `main` no tiene sentido sin un sistema de tiempo de ejecución subyacente que la invoque. En su lugar, estamos sobrescribiendo el punto de entrada del sistema operativo con nuestra propia función `_start`:
|
||||
|
||||
```rust
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() -> ! {
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
Al usar el atributo `#[no_mangle]`, deshabilitamos el [name mangling] para asegurarnos de que el compilador de Rust realmente genere una función con el nombre `_start`. Sin este atributo, el compilador generaría un símbolo críptico como `_ZN3blog_os4_start7hb173fedf945531caE` para dar un nombre único a cada función. Este atributo es necesario porque necesitamos informar al enlazador el nombre de la función de punto de entrada en el siguiente paso.
|
||||
|
||||
También debemos marcar la función como `extern "C"` para indicar al compilador que debe usar la [convención de llamadas en C][C calling convention] para esta función (en lugar de la convención de llamadas de Rust, que no está especificada). El motivo para nombrar la función `_start` es que este es el nombre predeterminado del punto de entrada en la mayoría de los sistemas.
|
||||
|
||||
[name mangling]: https://en.wikipedia.org/wiki/Name_mangling
|
||||
[C calling convention]: https://en.wikipedia.org/wiki/Calling_convention
|
||||
|
||||
El tipo de retorno `!` significa que la función es divergente, es decir, no está permitido que retorne nunca. Esto es necesario porque el punto de entrada no es llamado por ninguna función, sino que es invocado directamente por el sistema operativo o el bootloader. En lugar de retornar, el punto de entrada debería, por ejemplo, invocar la [llamada al sistema `exit`][`exit` system call] del sistema operativo. En nuestro caso, apagar la máquina podría ser una acción razonable, ya que no queda nada por hacer si un binario autónomo regresa. Por ahora, cumplimos con este requisito entrando en un bucle infinito.
|
||||
|
||||
[`exit` system call]: https://en.wikipedia.org/wiki/Exit_(system_call)
|
||||
|
||||
Cuando ejecutamos `cargo build` ahora, obtenemos un feo error del _linker_ (enlazador).
|
||||
|
||||
## Errores del Enlazador
|
||||
|
||||
El enlazador es un programa que combina el código generado en un ejecutable. Dado que el formato del ejecutable varía entre Linux, Windows y macOS, cada sistema tiene su propio enlazador que lanza errores diferentes. Sin embargo, la causa fundamental de los errores es la misma: la configuración predeterminada del enlazador asume que nuestro programa depende del tiempo de ejecución de C, lo cual no es cierto.
|
||||
|
||||
Para solucionar los errores, necesitamos informar al enlazador que no debe incluir el tiempo de ejecución de C. Esto puede hacerse pasando un conjunto específico de argumentos al enlazador o construyendo para un destino de bare metal.
|
||||
|
||||
### Construyendo para un Destino de Bare Metal
|
||||
|
||||
Por defecto, Rust intenta construir un ejecutable que pueda ejecutarse en el entorno actual de tu sistema. Por ejemplo, si estás usando Windows en `x86_64`, Rust intenta construir un ejecutable `.exe` para Windows que utilice instrucciones `x86_64`. Este entorno se llama tu sistema "host".
|
||||
|
||||
Para describir diferentes entornos, Rust utiliza una cadena llamada [_target triple_]. Puedes ver el _target triple_ de tu sistema host ejecutando:
|
||||
|
||||
```
|
||||
rustc 1.35.0-nightly (474e7a648 2019-04-07)
|
||||
binary: rustc
|
||||
commit-hash: 474e7a6486758ea6fc761893b1a49cd9076fb0ab
|
||||
commit-date: 2019-04-07
|
||||
host: x86_64-unknown-linux-gnu
|
||||
release: 1.35.0-nightly
|
||||
LLVM version: 8.0
|
||||
```
|
||||
|
||||
El resultado anterior es de un sistema Linux `x86_64`. Vemos que la tripleta del `host` es `x86_64-unknown-linux-gnu`, lo que incluye la arquitectura de la CPU (`x86_64`), el proveedor (`unknown`), el sistema operativo (`linux`) y el [ABI] (`gnu`).
|
||||
|
||||
[ABI]: https://en.wikipedia.org/wiki/Application_binary_interface
|
||||
|
||||
Al compilar para la tripleta del host, el compilador de Rust y el enlazador asumen que hay un sistema operativo subyacente como Linux o Windows que utiliza el tiempo de ejecución de C de forma predeterminada, lo que provoca los errores del enlazador. Para evitar estos errores, podemos compilar para un entorno diferente que no tenga un sistema operativo subyacente.
|
||||
|
||||
Un ejemplo de este tipo de entorno bare metal es la tripleta de destino `thumbv7em-none-eabihf`, que describe un sistema [embebido][embedded] basado en [ARM]. Los detalles no son importantes, lo que importa es que la tripleta de destino no tiene un sistema operativo subyacente, lo cual se indica por el `none` en la tripleta de destino. Para poder compilar para este destino, necesitamos agregarlo usando `rustup`:
|
||||
|
||||
```
|
||||
rustup target add thumbv7em-none-eabihf
|
||||
```
|
||||
|
||||
Esto descarga una copia de las bibliotecas estándar (y core) para el sistema. Ahora podemos compilar nuestro ejecutable autónomo para este destino:
|
||||
|
||||
```
|
||||
cargo build --target thumbv7em-none-eabihf
|
||||
```
|
||||
|
||||
Al pasar un argumento `--target`, realizamos un [compilado cruzado][cross compile] de nuestro ejecutable para un sistema bare metal. Dado que el sistema de destino no tiene un sistema operativo, el enlazador no intenta enlazar con el tiempo de ejecución de C, y nuestra compilación se completa sin errores del enlazador.
|
||||
|
||||
[cross compile]: https://en.wikipedia.org/wiki/Cross_compiler
|
||||
|
||||
Este es el enfoque que utilizaremos para construir nuestro kernel de sistema operativo. En lugar de `thumbv7em-none-eabihf`, utilizaremos un [destino personalizado][custom target] que describa un entorno bare metal `x86_64`. Los detalles se explicarán en la siguiente publicación.
|
||||
|
||||
[custom target]: https://doc.rust-lang.org/rustc/targets/custom.html
|
||||
|
||||
### Argumentos del Enlazador
|
||||
|
||||
En lugar de compilar para un sistema bare metal, también es posible resolver los errores del enlazador pasando un conjunto específico de argumentos al enlazador. Este no es el enfoque que usaremos para nuestro kernel, por lo tanto, esta sección es opcional y se proporciona solo para completar. Haz clic en _"Argumentos del Enlazador"_ a continuación para mostrar el contenido opcional.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Argumentos del Enlazador</summary>
|
||||
|
||||
En esta sección discutimos los errores del enlazador que ocurren en Linux, Windows y macOS, y explicamos cómo resolverlos pasando argumentos adicionales al enlazador. Ten en cuenta que el formato del ejecutable y el enlazador varían entre sistemas operativos, por lo que se requiere un conjunto diferente de argumentos para cada sistema.
|
||||
|
||||
#### Linux
|
||||
|
||||
En Linux ocurre el siguiente error del enlazador (resumido):
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
El problema es que el enlazador incluye por defecto la rutina de inicio del tiempo de ejecución de C, que también se llama `_start`. Esta rutina requiere algunos símbolos de la biblioteca estándar de C `libc` que no incluimos debido al atributo `no_std`, por lo que el enlazador no puede resolver estas referencias. Para solucionar esto, podemos indicar al enlazador que no enlace la rutina de inicio de C pasando la bandera `-nostartfiles`.
|
||||
|
||||
Una forma de pasar atributos al enlazador a través de Cargo es usar el comando `cargo rustc`. Este comando se comporta exactamente como `cargo build`, pero permite pasar opciones a `rustc`, el compilador subyacente de Rust. `rustc` tiene la bandera `-C link-arg`, que pasa un argumento al enlazador. Combinados, nuestro nuevo comando de compilación se ve así:
|
||||
|
||||
```
|
||||
cargo rustc -- -C link-arg=-nostartfiles
|
||||
```
|
||||
|
||||
¡Ahora nuestro crate se compila como un ejecutable autónomo en Linux!
|
||||
|
||||
No fue necesario especificar explícitamente el nombre de nuestra función de punto de entrada, ya que el enlazador busca una función con el nombre `_start` por defecto.
|
||||
|
||||
#### Windows
|
||||
|
||||
En Windows, ocurre un error del enlazador diferente (resumido):
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
El error "entry point must be defined" significa que el enlazador no puede encontrar el punto de entrada. En Windows, el nombre predeterminado del punto de entrada [depende del subsistema utilizado][windows-subsystems]. Para el subsistema `CONSOLE`, el enlazador busca una función llamada `mainCRTStartup`, y para el subsistema `WINDOWS`, busca una función llamada `WinMainCRTStartup`. Para anular este comportamiento predeterminado y decirle al enlazador que busque nuestra función `_start`, podemos pasar un argumento `/ENTRY` al enlazador:
|
||||
|
||||
[windows-subsystems]: https://docs.microsoft.com/en-us/cpp/build/reference/entry-entry-point-symbol
|
||||
|
||||
```
|
||||
cargo rustc -- -C link-arg=/ENTRY:_start
|
||||
```
|
||||
|
||||
Por el formato diferente del argumento, podemos ver claramente que el enlazador de Windows es un programa completamente distinto al enlazador de Linux.
|
||||
|
||||
Ahora ocurre un error diferente del enlazador:
|
||||
|
||||
```
|
||||
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 error ocurre porque los ejecutables de Windows pueden usar diferentes [subsistemas][windows-subsystems]. En programas normales, se infieren dependiendo del nombre del punto de entrada: si el punto de entrada se llama `main`, se usa el subsistema `CONSOLE`, y si el punto de entrada se llama `WinMain`, se usa el subsistema `WINDOWS`. Dado que nuestra función `_start` tiene un nombre diferente, necesitamos especificar el subsistema explícitamente:
|
||||
|
||||
```
|
||||
cargo rustc -- -C link-args="/ENTRY:_start /SUBSYSTEM:console"
|
||||
```
|
||||
|
||||
Aquí usamos el subsistema `CONSOLE`, pero el subsistema `WINDOWS` también funcionaría. En lugar de pasar `-C link-arg` varias veces, podemos usar `-C link-args`, que acepta una lista de argumentos separados por espacios.
|
||||
|
||||
Con este comando, nuestro ejecutable debería compilarse exitosamente en Windows.
|
||||
|
||||
#### macOS
|
||||
|
||||
En macOS, ocurre el siguiente error del enlazador (resumido):
|
||||
|
||||
```
|
||||
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 […]
|
||||
```
|
||||
|
||||
Este mensaje de error nos indica que el enlazador no puede encontrar una función de punto de entrada con el nombre predeterminado `main` (por alguna razón, en macOS todas las funciones tienen un prefijo `_`). Para establecer el punto de entrada en nuestra función `_start`, pasamos el argumento del enlazador `-e`:
|
||||
|
||||
```
|
||||
cargo rustc -- -C link-args="-e __start"
|
||||
```
|
||||
|
||||
La bandera `-e` especifica el nombre de la función de punto de entrada. Dado que en macOS todas las funciones tienen un prefijo adicional `_`, necesitamos establecer el punto de entrada en `__start` en lugar de `_start`.
|
||||
|
||||
Ahora ocurre el siguiente error del enlazador:
|
||||
|
||||
```
|
||||
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 [no admite oficialmente binarios enlazados estáticamente] y requiere que los programas enlacen la biblioteca `libSystem` por defecto. Para anular esto y enlazar un binario estático, se pasa la bandera `-static` al enlazador:
|
||||
|
||||
[no admite oficialmente binarios enlazados estáticamente]: https://developer.apple.com/library/archive/qa/qa1118/_index.html
|
||||
|
||||
|
||||
```
|
||||
cargo rustc -- -C link-args="-e __start -static"
|
||||
```
|
||||
|
||||
Esto aún no es suficiente, ya que ocurre un tercer error del enlazador:
|
||||
|
||||
```
|
||||
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 error ocurre porque los programas en macOS enlazan con `crt0` (“C runtime zero”) por defecto. Esto es similar al error que tuvimos en Linux y también se puede resolver añadiendo el argumento del enlazador `-nostartfiles`:
|
||||
|
||||
```
|
||||
cargo rustc -- -C link-args="-e __start -static -nostartfiles"
|
||||
```
|
||||
|
||||
Ahora nuestro programa debería compilarse exitosamente en macOS.
|
||||
|
||||
#### Unificando los Comandos de Construcción
|
||||
|
||||
Actualmente, tenemos diferentes comandos de construcción dependiendo de la plataforma host, lo cual no es ideal. Para evitar esto, podemos crear un archivo llamado `.cargo/config.toml` que contenga los argumentos específicos de cada 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"]
|
||||
```
|
||||
|
||||
La clave `rustflags` contiene argumentos que se añaden automáticamente a cada invocación de `rustc`. Para más información sobre el archivo `.cargo/config.toml`, consulta la [documentación oficial](https://doc.rust-lang.org/cargo/reference/config.html).
|
||||
|
||||
Ahora nuestro programa debería poder construirse en las tres plataformas con un simple `cargo build`.
|
||||
|
||||
#### ¿Deberías Hacer Esto?
|
||||
|
||||
Aunque es posible construir un ejecutable autónomo para Linux, Windows y macOS, probablemente no sea una buena idea. La razón es que nuestro ejecutable aún espera varias cosas, por ejemplo, que una pila esté inicializada cuando se llama a la función `_start`. Sin el tiempo de ejecución de C, algunos de estos requisitos podrían no cumplirse, lo que podría hacer que nuestro programa falle, por ejemplo, con un error de segmentación.
|
||||
|
||||
Si deseas crear un binario mínimo que se ejecute sobre un sistema operativo existente, incluir `libc` y configurar el atributo `#[start]` como se describe [aquí](https://doc.rust-lang.org/1.16.0/book/no-stdlib.html) probablemente sea una mejor idea.
|
||||
|
||||
</details>
|
||||
|
||||
## Resumen
|
||||
|
||||
Un binario mínimo autónomo en Rust se ve así:
|
||||
|
||||
`src/main.rs`:
|
||||
|
||||
```rust
|
||||
#![no_std] // don't link the Rust standard library
|
||||
#![no_main] // disable all Rust-level entry points
|
||||
|
||||
use core::panic::PanicInfo;
|
||||
|
||||
#[no_mangle] // don't mangle the name of this function
|
||||
pub extern "C" fn _start() -> ! {
|
||||
// this function is the entry point, since the linker looks for a function
|
||||
// named `_start` by default
|
||||
loop {}
|
||||
}
|
||||
|
||||
/// This function is called on panic.
|
||||
#[panic_handler]
|
||||
fn panic(_info: &PanicInfo) -> ! {
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
`Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[package]
|
||||
name = "crate_name"
|
||||
version = "0.1.0"
|
||||
authors = ["Author Name <author@example.com>"]
|
||||
|
||||
# the profile used for `cargo build`
|
||||
[profile.dev]
|
||||
panic = "abort" # disable stack unwinding on panic
|
||||
|
||||
# the profile used for `cargo build --release`
|
||||
[profile.release]
|
||||
panic = "abort" # disable stack unwinding on panic
|
||||
```
|
||||
|
||||
Para construir este binario, necesitamos compilar para un destino bare metal, como `thumbv7em-none-eabihf`:
|
||||
|
||||
```
|
||||
cargo build --target thumbv7em-none-eabihf
|
||||
```
|
||||
|
||||
Alternativamente, podemos compilarlo para el sistema host pasando argumentos adicionales al enlazador:
|
||||
|
||||
```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"
|
||||
```
|
||||
|
||||
Ten en cuenta que este es solo un ejemplo mínimo de un binario autónomo en Rust. Este binario espera varias cosas, por ejemplo, que una pila esté inicializada cuando se llama a la función `_start`. **Por lo tanto, para cualquier uso real de un binario como este, se requieren más pasos**.
|
||||
|
||||
## ¿Qué sigue?
|
||||
|
||||
La [próxima publicación][next post] explica los pasos necesarios para convertir nuestro binario autónomo en un kernel de sistema operativo mínimo. Esto incluye crear un destino personalizado, combinar nuestro ejecutable con un bootloader y aprender cómo imprimir algo en la pantalla.
|
||||
|
||||
[next post]: @/edition-2/posts/02-minimal-rust-kernel/index.md
|
||||
497
blog/content/edition-2/posts/02-minimal-rust-kernel/index.es.md
Normal file
497
blog/content/edition-2/posts/02-minimal-rust-kernel/index.es.md
Normal file
@@ -0,0 +1,497 @@
|
||||
+++
|
||||
title = "Un Kernel Mínimo en Rust"
|
||||
weight = 2
|
||||
path = "minimal-rust-kernel"
|
||||
date = 2018-02-10
|
||||
|
||||
[extra]
|
||||
chapter = "Bare Bones"
|
||||
+++
|
||||
|
||||
En este artículo, crearemos un kernel mínimo de 64 bits en Rust para la arquitectura x86. Partiremos del [un binario Rust autónomo] del artículo anterior para crear una imagen de disco arrancable que imprima algo en la pantalla.
|
||||
|
||||
[un binario Rust autónomo]: @/edition-2/posts/01-freestanding-rust-binary/index.md
|
||||
|
||||
<!-- more -->
|
||||
|
||||
Este blog se desarrolla abiertamente en [GitHub]. Si tienes problemas o preguntas, por favor abre un issue ahí. También puedes dejar comentarios [al final]. El código fuente completo para este artículo se encuentra en la rama [`post-02`][post branch].
|
||||
|
||||
[GitHub]: https://github.com/phil-opp/blog_os
|
||||
[al final]: #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 -->
|
||||
|
||||
## El Proceso de Arranque
|
||||
Cuando enciendes una computadora, comienza a ejecutar código de firmware almacenado en la [ROM] de la placa madre. Este código realiza una [prueba automática de encendido], detecta la memoria RAM disponible y preinicializa la CPU y el hardware. Después, busca un disco arrancable y comienza a cargar el kernel del sistema operativo.
|
||||
|
||||
[ROM]: https://en.wikipedia.org/wiki/Read-only_memory
|
||||
[prueba automática de encendido]: https://en.wikipedia.org/wiki/Power-on_self-test
|
||||
|
||||
En x86, existen dos estándares de firmware: el “Sistema Básico de Entrada/Salida” (**[BIOS]**) y la más reciente “Interfaz de Firmware Extensible Unificada” (**[UEFI]**). El estándar BIOS es antiguo y está desactualizado, pero es simple y está bien soportado en cualquier máquina x86 desde los años 80. UEFI, en contraste, es más moderno y tiene muchas más funciones, pero es más complejo de configurar (al menos en mi opinión).
|
||||
|
||||
[BIOS]: https://en.wikipedia.org/wiki/BIOS
|
||||
[UEFI]: https://en.wikipedia.org/wiki/Unified_Extensible_Firmware_Interface
|
||||
|
||||
Actualmente, solo proporcionamos soporte para BIOS, pero también planeamos agregar soporte para UEFI. Si te gustaría ayudarnos con esto, revisa el [issue en Github](https://github.com/phil-opp/blog_os/issues/349).
|
||||
|
||||
### Arranque con BIOS
|
||||
Casi todos los sistemas x86 tienen soporte para arranque con BIOS, incluyendo máquinas más recientes basadas en UEFI que usan un BIOS emulado. Esto es excelente, porque puedes usar la misma lógica de arranque en todas las máquinas del último siglo. Sin embargo, esta amplia compatibilidad también es la mayor desventaja del arranque con BIOS, ya que significa que la CPU se coloca en un modo de compatibilidad de 16 bits llamado [modo real] antes de arrancar, para que los bootloaders arcaicos de los años 80 sigan funcionando.
|
||||
|
||||
Pero comencemos desde el principio:
|
||||
|
||||
Cuando enciendes una computadora, carga el BIOS desde una memoria flash especial ubicada en la placa madre. El BIOS ejecuta rutinas de autoprueba e inicialización del hardware, y luego busca discos arrancables. Si encuentra uno, transfiere el control a su _bootloader_ (_cargador de arranque_), que es una porción de código ejecutable de 512 bytes almacenada al inicio del disco. La mayoría de los bootloaders son más grandes que 512 bytes, por lo que suelen dividirse en una pequeña primera etapa, que cabe en esos 512 bytes, y una segunda etapa que se carga posteriormente.
|
||||
|
||||
El bootloader debe determinar la ubicación de la imagen del kernel en el disco y cargarla en la memoria. Tambien necesita cambiar la CPU del [modo real] de 16 bits primero al [modo protegido] de 32 bits, y luego al [modo largo] de 64 bits, donde están disponibles los registros de 64 bits y toda la memoria principal. Su tercera tarea es consultar cierta información (como un mapa de memoria) desde el BIOS y pasársela al kernel del sistema operativo.
|
||||
|
||||
[modo real]: https://en.wikipedia.org/wiki/Real_mode
|
||||
[modo protegido]: https://en.wikipedia.org/wiki/Protected_mode
|
||||
[modo largo]: https://en.wikipedia.org/wiki/Long_mode
|
||||
[segmentación de memoria]: https://en.wikipedia.org/wiki/X86_memory_segmentation
|
||||
|
||||
Escribir un bootloader es un poco tedioso, ya que requiere lenguaje ensamblador y muchos pasos poco claros como “escribir este valor mágico en este registro del procesador”. Por ello, no cubrimos la creación de bootloaders en este artículo y en su lugar proporcionamos una herramienta llamada [bootimage] que automatiza el proceso de creación de un bootloader.
|
||||
|
||||
[bootimage]: https://github.com/rust-osdev/bootimage
|
||||
|
||||
Si te interesa construir tu propio bootloader: ¡Estén atentos! Un conjunto de artículos sobre este tema está en camino.
|
||||
|
||||
#### El Estándar Multiboot
|
||||
Para evitar que cada sistema operativo implemente su propio bootloader, que sea compatible solo con un único sistema, la [Free Software Foundation] creó en 1995 un estándar abierto de bootloaders llamado [Multiboot]. El estándar define una interfaz entre el bootloader y el sistema operativo, de modo que cualquier bootloader compatible con Multiboot pueda cargar cualquier sistema operativo compatible con Multiboot. La implementación de referencia es [GNU GRUB], que es el bootloader más 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 hacer un kernel compatible con Multiboot, solo necesitas insertar un llamado [encabezado Multiboot] al inicio del archivo del kernel. Esto hace que arrancar un sistema operativo desde GRUB sea muy sencillo. Sin embargo, GRUB y el estándar Multiboot también tienen algunos problemas:
|
||||
|
||||
[encabezado Multiboot]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#OS-image-format
|
||||
|
||||
- Solo soportan el modo protegido de 32 bits. Esto significa que aún tienes que configurar la CPU para cambiar al modo largo de 64 bits.
|
||||
- Están diseñados para simplificar el cargador de arranque en lugar del kernel. Por ejemplo, el kernel necesita vincularse con un [tamaño de página predeterminado ajustado], porque GRUB no puede encontrar el encabezado Multiboot de otro modo. Otro ejemplo es que la [información de arranque], que se pasa al kernel, contiene muchas estructuras dependientes de la arquitectura en lugar de proporcionar abstracciones limpias.
|
||||
- Tanto GRUB como el estándar Multiboot están escasamente documentados.
|
||||
- GRUB necesita instalarse en el sistema host para crear una imagen de disco arrancable a partir del archivo del kernel. Esto dificulta el desarrollo en Windows o Mac.
|
||||
|
||||
[tamaño de página predeterminado ajustado]: https://wiki.osdev.org/Multiboot#Multiboot_2
|
||||
[información de arranque]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#Boot-information-format
|
||||
|
||||
Debido a estas desventajas, decidimos no usar GRUB ni el estándar Multiboot. Sin embargo, planeamos agregar soporte para Multiboot a nuestra herramienta [bootimage], para que sea posible cargar tu kernel en un sistema GRUB también. Si te interesa escribir un kernel compatible con Multiboot, revisa la [primera edición] de esta serie de blogs.
|
||||
|
||||
[primera edición]: @/edition-1/_index.md
|
||||
|
||||
### UEFI
|
||||
|
||||
(Por el momento no proporcionamos soporte para UEFI, ¡pero nos encantaría hacerlo! Si deseas ayudar, por favor háznoslo saber en el [issue de Github](https://github.com/phil-opp/blog_os/issues/349).)
|
||||
|
||||
## Un Kernel Mínimo
|
||||
Ahora que tenemos una idea general de cómo arranca una computadora, es momento de crear nuestro propio kernel mínimo. Nuestro objetivo es crear una imagen de disco que, al arrancar, imprima “Hello World!” en la pantalla. Para esto, extendemos el [un binario Rust autónomo] del artículo anterior.
|
||||
|
||||
Como recordarás, construimos el binario independiente mediante `cargo`, pero dependiendo del sistema operativo, necesitábamos diferentes nombres de punto de entrada y banderas de compilación. Esto se debe a que `cargo` construye por defecto para el _sistema anfitrión_, es decir, el sistema en el que estás ejecutando el comando. Esto no es lo que queremos para nuestro kernel, ya que un kernel que funcione encima, por ejemplo, de Windows, no tiene mucho sentido. En su lugar, queremos compilar para un _sistema destino_ claramente definido.
|
||||
|
||||
### Instalación de Rust Nightly
|
||||
Rust tiene tres canales de lanzamiento: _stable_, _beta_ y _nightly_. El libro de Rust explica muy bien la diferencia entre estos canales, así que tómate un momento para [revisarlo](https://doc.rust-lang.org/book/appendix-07-nightly-rust.html#choo-choo-release-channels-and-riding-the-trains). Para construir un sistema operativo, necesitaremos algunas características experimentales que solo están disponibles en el canal nightly, por lo que debemos instalar una versión nightly de Rust.
|
||||
|
||||
Para administrar instalaciones de Rust, recomiendo ampliamente [rustup]. Este permite instalar compiladores nightly, beta y estable lado a lado, y facilita mantenerlos actualizados. Con rustup, puedes usar un compilador nightly en el directorio actual ejecutando `rustup override set nightly`. Alternativamente, puedes agregar un archivo llamado `rust-toolchain` con el contenido `nightly` en el directorio raíz del proyecto. Puedes verificar que tienes una versión nightly instalada ejecutando `rustc --version`: el número de versión debería contener `-nightly` al final.
|
||||
|
||||
[rustup]: https://www.rustup.rs/
|
||||
|
||||
El compilador nightly nos permite activar varias características experimentales utilizando las llamadas _banderas de características_ al inicio de nuestro archivo. Por ejemplo, podríamos habilitar el macro experimental [`asm!`] para ensamblador en línea agregando `#![feature(asm)]` en la parte superior de nuestro archivo `main.rs`. Ten en cuenta que estas características experimentales son completamente inestables, lo que significa que futuras versiones de Rust podrían cambiarlas o eliminarlas sin previo aviso. Por esta razón, solo las utilizaremos si son absolutamente necesarias.
|
||||
|
||||
[`asm!`]: https://doc.rust-lang.org/stable/reference/inline-assembly.html
|
||||
|
||||
### Especificación del Objetivo
|
||||
Cargo soporta diferentes sistemas destino mediante el parámetro `--target`. El destino se describe mediante un _[tripleta de destino]_, que especifica la arquitectura de la CPU, el proveedor, el sistema operativo y el [ABI]. Por ejemplo, el tripleta de destino `x86_64-unknown-linux-gnu` describe un sistema con una CPU `x86_64`, sin un proveedor claro, y un sistema operativo Linux con el ABI GNU. Rust soporta [muchas tripleta de destino diferentes][platform-support], incluyendo `arm-linux-androideabi` para Android o [`wasm32-unknown-unknown` para WebAssembly](https://www.hellorust.com/setup/wasm-target/).
|
||||
|
||||
[tripleta de destino]: 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 nuestro sistema destino, sin embargo, requerimos algunos parámetros de configuración especiales (por ejemplo, sin un sistema operativo subyacente), por lo que ninguno de los [tripletas de destino existentes][platform-support] encaja. Afortunadamente, Rust nos permite definir [nuestros propios objetivos][custom-targets] mediante un archivo JSON. Por ejemplo, un archivo JSON que describe el objetivo `x86_64-unknown-linux-gnu` se ve así:
|
||||
|
||||
```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
|
||||
}
|
||||
```
|
||||
|
||||
La mayoría de los campos son requeridos por LLVM para generar código para esa plataforma. Por ejemplo, el campo [`data-layout`] define el tamaño de varios tipos de enteros, números de punto flotante y punteros. Luego, hay campos que Rust utiliza para la compilación condicional, como `target-pointer-width`. El tercer tipo de campo define cómo debe construirse el crate. Por ejemplo, el campo `pre-link-args` especifica argumentos que se pasan al [linker].
|
||||
|
||||
[`data-layout`]: https://llvm.org/docs/LangRef.html#data-layout
|
||||
[linker]: https://en.wikipedia.org/wiki/Linker_(computing)
|
||||
|
||||
Nuestro kernel también tiene como objetivo los sistemas `x86_64`, por lo que nuestra especificación de objetivo será muy similar a la anterior. Comencemos creando un archivo llamado `x86_64-blog_os.json` (puedes elegir el nombre que prefieras) con el siguiente contenido común:
|
||||
|
||||
```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
|
||||
}
|
||||
```
|
||||
|
||||
Ten en cuenta que cambiamos el sistema operativo en el campo `llvm-target` y en el campo `os` a `none`, porque nuestro kernel se ejecutará directamente sobre hardware sin un sistema operativo subyacente.
|
||||
|
||||
Agregamos las siguientes entradas relacionadas con la construcción:
|
||||
|
||||
|
||||
```json
|
||||
"linker-flavor": "ld.lld",
|
||||
"linker": "rust-lld",
|
||||
```
|
||||
|
||||
En lugar de usar el enlazador predeterminado de la plataforma (que podría no soportar objetivos de Linux), utilizamos el enlazador multiplataforma [LLD] que se incluye con Rust para enlazar nuestro kernel.
|
||||
|
||||
[LLD]: https://lld.llvm.org/
|
||||
|
||||
```json
|
||||
"panic-strategy": "abort",
|
||||
```
|
||||
|
||||
Esta configuración especifica que el objetivo no soporta [stack unwinding] en caso de un pánico, por lo que el programa debería abortar directamente. Esto tiene el mismo efecto que la opción `panic = "abort"` en nuestro archivo Cargo.toml, por lo que podemos eliminarla de ahí. (Ten en cuenta que, a diferencia de la opción en Cargo.toml, esta opción del destino también se aplica cuando recompilamos la biblioteca `core` más adelante en este artículo. Por lo tanto, incluso si prefieres mantener la opción en Cargo.toml, asegúrate de incluir esta opción.)
|
||||
|
||||
[stack unwinding]: https://www.bogotobogo.com/cplusplus/stackunwinding.php
|
||||
|
||||
```json
|
||||
"disable-redzone": true,
|
||||
```
|
||||
|
||||
Estamos escribiendo un kernel, por lo que en algún momento necesitaremos manejar interrupciones. Para hacerlo de manera segura, debemos deshabilitar una optimización del puntero de pila llamada _“red zone”_, ya que de lo contrario podría causar corrupción en la pila. Para más información, consulta nuestro artículo sobre [cómo deshabilitar la red zone].
|
||||
|
||||
[deshabilitar la red zone]: @/edition-2/posts/02-minimal-rust-kernel/disable-red-zone/index.md
|
||||
|
||||
```json
|
||||
"features": "-mmx,-sse,+soft-float",
|
||||
```
|
||||
|
||||
El campo `features` habilita o deshabilita características del destinos. Deshabilitamos las características `mmx` y `sse` anteponiéndoles un signo menos y habilitamos la característica `soft-float` anteponiéndole un signo más. Ten en cuenta que no debe haber espacios entre las diferentes banderas, ya que de lo contrario LLVM no podrá interpretar correctamente la cadena de características.
|
||||
|
||||
Las características `mmx` y `sse` determinan el soporte para instrucciones [Single Instruction Multiple Data (SIMD)], que a menudo pueden acelerar significativamente los programas. Sin embargo, el uso de los registros SIMD en kernels de sistemas operativos genera problemas de rendimiento. Esto se debe a que el kernel necesita restaurar todos los registros a su estado original antes de continuar un programa interrumpido. Esto implica que el kernel debe guardar el estado completo de SIMD en la memoria principal en cada llamada al sistema o interrupción de hardware. Dado que el estado SIMD es muy grande (512–1600 bytes) y las interrupciones pueden ocurrir con mucha frecuencia, estas operaciones adicionales de guardar/restaurar afectan considerablemente el rendimiento. Para evitar esto, deshabilitamos SIMD para nuestro kernel (pero no para las aplicaciones que se ejecutan encima).
|
||||
|
||||
[Single Instruction Multiple Data (SIMD)]: https://en.wikipedia.org/wiki/SIMD
|
||||
|
||||
Un problema al deshabilitar SIMD es que las operaciones de punto flotante en `x86_64` requieren registros SIMD por defecto. Para resolver este problema, agregamos la característica `soft-float`, que emula todas las operaciones de punto flotante mediante funciones de software basadas en enteros normales.
|
||||
|
||||
Para más información, consulta nuestro artículo sobre [cómo deshabilitar SIMD](@/edition-2/posts/02-minimal-rust-kernel/disable-simd/index.md).
|
||||
|
||||
#### Juntándolo Todo
|
||||
Nuestro archivo de especificación de objetivo ahora se ve así:
|
||||
|
||||
|
||||
```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"
|
||||
}
|
||||
```
|
||||
|
||||
### Construyendo nuestro Kernel
|
||||
Compilar para nuestro nuevo objetivo usará convenciones de Linux, ya que la opción de enlazador `ld.lld` instruye a LLVM a compilar con la bandera `-flavor gnu` (para más opciones del enlazador, consulta [la documentación de rustc](https://doc.rust-lang.org/rustc/codegen-options/index.html#linker-flavor)). Esto significa que necesitamos un punto de entrada llamado `_start`, como se describió en el [artículo anterior]:
|
||||
|
||||
[artículo anterior]: @/edition-2/posts/01-freestanding-rust-binary/index.md
|
||||
|
||||
```rust
|
||||
// src/main.rs
|
||||
|
||||
#![no_std] // don't link the Rust standard library
|
||||
#![no_main] // disable all Rust-level entry points
|
||||
|
||||
use core::panic::PanicInfo;
|
||||
|
||||
/// This function is called on panic.
|
||||
#[panic_handler]
|
||||
fn panic(_info: &PanicInfo) -> ! {
|
||||
loop {}
|
||||
}
|
||||
|
||||
#[no_mangle] // don't mangle the name of this function
|
||||
pub extern "C" fn _start() -> ! {
|
||||
// this function is the entry point, since the linker looks for a function
|
||||
// named `_start` by default
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
Ten en cuenta que el punto de entrada debe llamarse `_start` sin importar el sistema operativo anfitrión.
|
||||
|
||||
Ahora podemos construir el kernel para nuestro nuevo objetivo pasando el nombre del archivo JSON como `--target`:
|
||||
|
||||
```
|
||||
> cargo build --target x86_64-blog_os.json
|
||||
|
||||
error[E0463]: can't find crate for `core`
|
||||
```
|
||||
|
||||
¡Falla! El error nos indica que el compilador de Rust ya no encuentra la [biblioteca `core`]. Esta biblioteca contiene tipos básicos de Rust como `Result`, `Option` e iteradores, y se vincula implícitamente a todos los crates con `no_std`.
|
||||
|
||||
[biblioteca `core`]: https://doc.rust-lang.org/nightly/core/index.html
|
||||
|
||||
El problema es que la biblioteca `core` se distribuye junto con el compilador de Rust como una biblioteca _precompilada_. Por lo tanto, solo es válida para tripletas de anfitrión soportados (por ejemplo, `x86_64-unknown-linux-gnu`), pero no para nuestro objetivo personalizado. Si queremos compilar código para otros objetivos, necesitamos recompilar `core` para esos objetivos primero.
|
||||
|
||||
#### La Opción `build-std`
|
||||
|
||||
Aquí es donde entra en juego la característica [`build-std`] de cargo. Esta permite recompilar `core` y otras bibliotecas estándar bajo demanda, en lugar de usar las versiones precompiladas que vienen con la instalación de Rust. Esta característica es muy nueva y aún no está terminada, por lo que está marcada como "inestable" y solo está disponible en los [compiladores de Rust nightly].
|
||||
|
||||
[`build-std`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std
|
||||
[compiladores de Rust nightly]: #installing-rust-nightly
|
||||
|
||||
Para usar esta característica, necesitamos crear un archivo de configuración local de [cargo] en `.cargo/config.toml` (la carpeta `.cargo` debería estar junto a tu carpeta `src`) con el siguiente contenido:
|
||||
|
||||
[cargo]: https://doc.rust-lang.org/cargo/reference/config.html
|
||||
|
||||
```toml
|
||||
# in .cargo/config.toml
|
||||
|
||||
[unstable]
|
||||
build-std = ["core", "compiler_builtins"]
|
||||
```
|
||||
|
||||
Esto le indica a cargo que debe recompilar las bibliotecas `core` y `compiler_builtins`. Esta última es necesaria porque es una dependencia de `core`. Para poder recompilar estas bibliotecas, cargo necesita acceso al código fuente de Rust, el cual podemos instalar ejecutando `rustup component add rust-src`.
|
||||
|
||||
<div class="note">
|
||||
|
||||
**Nota:** La clave de configuración `unstable.build-std` requiere al menos la versión de Rust nightly del 15 de julio de 2020.
|
||||
|
||||
</div>
|
||||
|
||||
Después de configurar la clave `unstable.build-std` e instalar el componente `rust-src`, podemos ejecutar nuevamente nuestro comando de construcción:
|
||||
|
||||
```
|
||||
> 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` ahora recompila las bibliotecas `core`, `rustc-std-workspace-core` (una dependencia de `compiler_builtins`) y `compiler_builtins` para nuestro objetivo personalizado.
|
||||
|
||||
#### Intrínsecos Relacionados con la Memoria
|
||||
|
||||
El compilador de Rust asume que un cierto conjunto de funciones integradas está disponible para todos los sistemas. La mayoría de estas funciones son proporcionadas por el crate `compiler_builtins`, que acabamos de recompilar. Sin embargo, hay algunas funciones relacionadas con la memoria en ese crate que no están habilitadas por defecto, ya que normalmente son proporcionadas por la biblioteca C del sistema. Estas funciones incluyen `memset`, que establece todos los bytes de un bloque de memoria a un valor dado, `memcpy`, que copia un bloque de memoria a otro, y `memcmp`, que compara dos bloques de memoria. Aunque no necesitamos estas funciones para compilar nuestro kernel en este momento, serán necesarias tan pronto como agreguemos más código (por ejemplo, al copiar estructuras).
|
||||
|
||||
Dado que no podemos vincularnos a la biblioteca C del sistema operativo, necesitamos una forma alternativa de proporcionar estas funciones al compilador. Una posible solución podría ser implementar nuestras propias funciones `memset`, `memcpy`, etc., y aplicarles el atributo `#[no_mangle]` (para evitar el renombramiento automático durante la compilación). Sin embargo, esto es peligroso, ya que el más mínimo error en la implementación de estas funciones podría conducir a un comportamiento indefinido. Por ejemplo, implementar `memcpy` con un bucle `for` podría resultar en una recursión infinita, ya que los bucles `for` llaman implícitamente al método del trait [`IntoIterator::into_iter`], que podría invocar nuevamente a `memcpy`. Por lo tanto, es una buena idea reutilizar implementaciones existentes y bien probadas.
|
||||
|
||||
[`IntoIterator::into_iter`]: https://doc.rust-lang.org/stable/core/iter/trait.IntoIterator.html#tymethod.into_iter
|
||||
|
||||
Afortunadamente, el crate `compiler_builtins` ya contiene implementaciones para todas las funciones necesarias, pero están deshabilitadas por defecto para evitar conflictos con las implementaciones de la biblioteca C. Podemos habilitarlas configurando la bandera [`build-std-features`] de cargo como `["compiler-builtins-mem"]`. Al igual que la bandera `build-std`, esta bandera puede pasarse como un flag `-Z` en la línea de comandos o configurarse en la tabla `unstable` en el archivo `.cargo/config.toml`. Dado que siempre queremos compilar con esta bandera, la opción de archivo de configuración tiene más sentido para nosotros:
|
||||
|
||||
[`build-std-features`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std-features
|
||||
|
||||
```toml
|
||||
# in .cargo/config.toml
|
||||
|
||||
[unstable]
|
||||
build-std-features = ["compiler-builtins-mem"]
|
||||
build-std = ["core", "compiler_builtins"]
|
||||
```
|
||||
|
||||
(El soporte para la característica `compiler-builtins-mem` fue [añadido muy recientemente](https://github.com/rust-lang/rust/pull/77284), por lo que necesitas al menos Rust nightly `2020-09-30` para usarla).
|
||||
|
||||
Detrás de escena, esta bandera habilita la [característica `mem`] del crate `compiler_builtins`. El efecto de esto es que el atributo `#[no_mangle]` se aplica a las [implementaciones de `memcpy`, etc.] del crate, lo que las hace disponibles para el enlazador.
|
||||
|
||||
[característica `mem`]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/Cargo.toml#L54-L55
|
||||
[implementaciones de `memcpy`, etc.]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/src/mem.rs#L12-L69
|
||||
|
||||
Con este cambio, nuestro kernel tiene implementaciones válidas para todas las funciones requeridas por el compilador, por lo que continuará compilándose incluso si nuestro código se vuelve más complejo.
|
||||
|
||||
#### Configurar un Objetivo Predeterminado
|
||||
|
||||
Para evitar pasar el parámetro `--target` en cada invocación de `cargo build`, podemos sobrescribir el objetivo predeterminado. Para hacer esto, añadimos lo siguiente a nuestro archivo de [configuración de cargo] en `.cargo/config.toml`:
|
||||
|
||||
[configuración de cargo]: https://doc.rust-lang.org/cargo/reference/config.html
|
||||
|
||||
```toml
|
||||
# in .cargo/config.toml
|
||||
|
||||
[build]
|
||||
target = "x86_64-blog_os.json"
|
||||
```
|
||||
|
||||
Esto le indica a `cargo` que use nuestro objetivo `x86_64-blog_os.json` cuando no se pase explícitamente el argumento `--target`. Esto significa que ahora podemos construir nuestro kernel con un simple `cargo build`. Para más información sobre las opciones de configuración de cargo, consulta la [documentación oficial][configuración de cargo].
|
||||
|
||||
Ahora podemos construir nuestro kernel para un objetivo bare metal con un simple `cargo build`. Sin embargo, nuestro punto de entrada `_start`, que será llamado por el cargador de arranque, aún está vacío. Es momento de mostrar algo en la pantalla desde ese punto.
|
||||
|
||||
### Imprimiendo en Pantalla
|
||||
La forma más sencilla de imprimir texto en la pantalla en esta etapa es usando el [búfer de texto VGA]. Es un área de memoria especial mapeada al hardware VGA que contiene el contenido mostrado en pantalla. Normalmente consta de 25 líneas, cada una con 80 celdas de caracteres. Cada celda de carácter muestra un carácter ASCII con algunos colores de primer plano y fondo. La salida en pantalla se ve así:
|
||||
|
||||
[búfer de texto VGA]: https://en.wikipedia.org/wiki/VGA-compatible_text_mode
|
||||
|
||||

|
||||
|
||||
Discutiremos el diseño exacto del búfer VGA en el próximo artículo, donde escribiremos un primer controlador pequeño para él. Para imprimir “Hello World!”, solo necesitamos saber que el búfer está ubicado en la dirección `0xb8000` y que cada celda de carácter consta de un byte ASCII y un byte de color.
|
||||
|
||||
La implementación se ve así:
|
||||
|
||||
```rust
|
||||
static HELLO: &[u8] = b"Hello World!";
|
||||
|
||||
#[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 {}
|
||||
}
|
||||
```
|
||||
|
||||
Primero, convertimos el entero `0xb8000` en un [raw pointer]. Luego, [iteramos] sobre los bytes de la [cadena de bytes estática] `HELLO`. Usamos el método [`enumerate`] para obtener adicionalmente una variable de conteo `i`. En el cuerpo del bucle `for`, utilizamos el método [`offset`] para escribir el byte de la cadena y el byte de color correspondiente (`0xb` representa un cian claro).
|
||||
|
||||
[iteramos]: https://doc.rust-lang.org/stable/book/ch13-02-iterators.html
|
||||
[raw pointer]: https://doc.rust-lang.org/stable/book/ch19-01-unsafe-rust.html#dereferencing-a-raw-pointer
|
||||
[estática]: 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
|
||||
[cadena de bytes]: https://doc.rust-lang.org/reference/tokens.html#byte-string-literals
|
||||
[`offset`]: https://doc.rust-lang.org/std/primitive.pointer.html#method.offset
|
||||
|
||||
Ten en cuenta que hay un bloque [`unsafe`] alrededor de todas las escrituras de memoria. Esto se debe a que el compilador de Rust no puede probar que los punteros crudos que creamos son válidos. Podrían apuntar a cualquier lugar y causar corrupción de datos. Al poner estas operaciones en un bloque `unsafe`, básicamente le decimos al compilador que estamos absolutamente seguros de que las operaciones son válidas. Sin embargo, un bloque `unsafe` no desactiva las verificaciones de seguridad de Rust; simplemente permite realizar [cinco operaciones adicionales].
|
||||
|
||||
[`unsafe`]: https://doc.rust-lang.org/stable/book/ch19-01-unsafe-rust.html
|
||||
[cinco operaciones adicionales]: https://doc.rust-lang.org/stable/book/ch19-01-unsafe-rust.html#unsafe-superpowers
|
||||
|
||||
Quiero enfatizar que **esta no es la forma en que queremos hacer las cosas en Rust**. Es muy fácil cometer errores al trabajar con punteros crudos dentro de bloques `unsafe`. Por ejemplo, podríamos escribir más allá del final del búfer si no somos cuidadosos.
|
||||
|
||||
Por lo tanto, queremos minimizar el uso de `unsafe` tanto como sea posible. Rust nos permite lograr esto creando abstracciones seguras. Por ejemplo, podríamos crear un tipo de búfer VGA que encapsule toda la inseguridad y garantice que sea _imposible_ hacer algo incorrecto desde el exterior. De esta manera, solo necesitaríamos cantidades mínimas de código `unsafe` y podríamos estar seguros de no violar la [seguridad de la memoria]. Crearemos una abstracción segura para el búfer VGA en el próximo artículo.
|
||||
|
||||
[seguridad de la memoria]: https://en.wikipedia.org/wiki/Memory_safety
|
||||
|
||||
## Ejecutando Nuestro Kernel
|
||||
|
||||
Ahora que tenemos un ejecutable que realiza algo perceptible, es momento de ejecutarlo. Primero, necesitamos convertir nuestro kernel compilado en una imagen de disco arrancable vinculándolo con un cargador de arranque. Luego, podemos ejecutar la imagen de disco en la máquina virtual [QEMU] o iniciarla en hardware real usando una memoria USB.
|
||||
|
||||
### Creando una Bootimage
|
||||
|
||||
Para convertir nuestro kernel compilado en una imagen de disco arrancable, debemos vincularlo con un cargador de arranque. Como aprendimos en la [sección sobre el proceso de arranque], el cargador de arranque es responsable de inicializar la CPU y cargar nuestro kernel.
|
||||
|
||||
[sección sobre el proceso de arranque]: #the-boot-process
|
||||
|
||||
En lugar de escribir nuestro propio cargador de arranque, lo cual es un proyecto en sí mismo, usamos el crate [`bootloader`]. Este crate implementa un cargador de arranque básico para BIOS sin dependencias en C, solo Rust y ensamblador en línea. Para usarlo y arrancar nuestro kernel, necesitamos agregarlo como dependencia:
|
||||
|
||||
[`bootloader`]: https://crates.io/crates/bootloader
|
||||
|
||||
```toml
|
||||
# in Cargo.toml
|
||||
|
||||
[dependencies]
|
||||
bootloader = "0.9"
|
||||
```
|
||||
|
||||
**Nota:** Este artículo solo es compatible con `bootloader v0.9`. Las versiones más recientes usan un sistema de construcción diferente y generarán errores de compilación al seguir este artículo.
|
||||
|
||||
Agregar el bootloader como dependencia no es suficiente para crear una imagen de disco arrancable. El problema es que necesitamos vincular nuestro kernel con el bootloader después de la compilación, pero cargo no tiene soporte para [scripts post-compilación].
|
||||
|
||||
[scripts post-compilación]: https://github.com/rust-lang/cargo/issues/545
|
||||
|
||||
Para resolver este problema, creamos una herramienta llamada `bootimage` que primero compila el kernel y el bootloader, y luego los vincula para crear una imagen de disco arrancable. Para instalar esta herramienta, dirígete a tu directorio de inicio (o cualquier directorio fuera de tu proyecto de cargo) y ejecuta el siguiente comando en tu terminal:
|
||||
|
||||
```
|
||||
cargo install bootimage
|
||||
```
|
||||
|
||||
Para ejecutar `bootimage` y compilar el bootloader, necesitas tener instalado el componente `llvm-tools-preview` de rustup. Puedes hacerlo ejecutando el comando correspondiente.
|
||||
|
||||
Después de instalar `bootimage` y agregar el componente `llvm-tools-preview`, puedes crear una imagen de disco arrancable regresando al directorio de tu proyecto de cargo y ejecutando:
|
||||
|
||||
```
|
||||
> cargo bootimage
|
||||
```
|
||||
|
||||
Vemos que la herramienta recompila nuestro kernel usando `cargo build`, por lo que automáticamente aplicará cualquier cambio que realices. Después, compila el bootloader, lo cual puede tardar un poco. Como ocurre con todas las dependencias de los crates, solo se compila una vez y luego se almacena en caché, por lo que las compilaciones posteriores serán mucho más rápidas. Finalmente, `bootimage` combina el bootloader y tu kernel en una imagen de disco arrancable.
|
||||
|
||||
Después de ejecutar el comando, deberías ver una imagen de disco arrancable llamada `bootimage-blog_os.bin` en tu directorio `target/x86_64-blog_os/debug`. Puedes arrancarla en una máquina virtual o copiarla a una unidad USB para arrancarla en hardware real. (Ten en cuenta que esta no es una imagen de CD, que tiene un formato diferente, por lo que grabarla en un CD no funcionará).
|
||||
|
||||
#### ¿Cómo funciona?
|
||||
La herramienta `bootimage` realiza los siguientes pasos detrás de escena:
|
||||
|
||||
- Compila nuestro kernel en un archivo [ELF].
|
||||
- Compila la dependencia del bootloader como un ejecutable independiente.
|
||||
- Vincula los bytes del archivo ELF del kernel con el bootloader.
|
||||
|
||||
[ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
|
||||
[rust-osdev/bootloader]: https://github.com/rust-osdev/bootloader
|
||||
|
||||
Al arrancar, el bootloader lee y analiza el archivo ELF anexado. Luego, mapea los segmentos del programa a direcciones virtuales en las tablas de páginas, inicializa a cero la sección `.bss` y configura una pila. Finalmente, lee la dirección del punto de entrada (nuestra función `_start`) y salta a ella.
|
||||
|
||||
### Arrancando en QEMU
|
||||
|
||||
Ahora podemos arrancar la imagen de disco en una máquina virtual. Para arrancarla en [QEMU], ejecuta el comando correspondiente.
|
||||
|
||||
[QEMU]: https://www.qemu.org/
|
||||
|
||||
```
|
||||
> qemu-system-x86_64 -drive format=raw,file=target/x86_64-blog_os/debug/bootimage-blog_os.bin
|
||||
```
|
||||
|
||||
Esto abre una ventana separada que debería verse similar a esto:
|
||||
|
||||

|
||||
|
||||
Vemos que nuestro "Hello World!" es visible en la pantalla.
|
||||
|
||||
### Máquina Real
|
||||
|
||||
También es posible escribir la imagen a una memoria USB y arrancarla en una máquina real, **pero ten mucho cuidado** al elegir el nombre correcto del dispositivo, porque **todo en ese dispositivo será sobrescrito**:
|
||||
|
||||
```
|
||||
> dd if=target/x86_64-blog_os/debug/bootimage-blog_os.bin of=/dev/sdX && sync
|
||||
```
|
||||
|
||||
Donde `sdX` es el nombre del dispositivo de tu memoria USB.
|
||||
|
||||
Después de escribir la imagen en la memoria USB, puedes ejecutarla en hardware real iniciando desde ella. Probablemente necesitarás usar un menú de arranque especial o cambiar el orden de arranque en la configuración del BIOS para iniciar desde la memoria USB. Ten en cuenta que actualmente no funciona para máquinas UEFI, ya que el crate `bootloader` aún no tiene soporte para UEFI.
|
||||
|
||||
### Usando `cargo run`
|
||||
|
||||
Para facilitar la ejecución de nuestro kernel en QEMU, podemos configurar la clave de configuración `runner` para cargo:
|
||||
|
||||
```toml
|
||||
# in .cargo/config.toml
|
||||
|
||||
[target.'cfg(target_os = "none")']
|
||||
runner = "bootimage runner"
|
||||
```
|
||||
|
||||
La tabla `target.'cfg(target_os = "none")'` se aplica a todos los objetivos cuyo campo `"os"` en el archivo de configuración del objetivo esté configurado como `"none"`. Esto incluye nuestro objetivo `x86_64-blog_os.json`. La clave `runner` especifica el comando que debe ejecutarse para `cargo run`. El comando se ejecuta después de una compilación exitosa, con la ruta del ejecutable pasada como el primer argumento. Consulta la [documentación de cargo][configuración de cargo] para más detalles.
|
||||
|
||||
El comando `bootimage runner` está específicamente diseñado para ser utilizado como un ejecutable `runner`. Vincula el ejecutable dado con la dependencia del bootloader del proyecto y luego lanza QEMU. Consulta el [README de `bootimage`] para más detalles y posibles opciones de configuración.
|
||||
|
||||
[README de `bootimage`]: https://github.com/rust-osdev/bootimage
|
||||
|
||||
Ahora podemos usar `cargo run` para compilar nuestro kernel e iniciarlo en QEMU.
|
||||
|
||||
## ¿Qué sigue?
|
||||
|
||||
En el próximo artículo, exploraremos el búfer de texto VGA con más detalle y escribiremos una interfaz segura para él. También añadiremos soporte para el macro `println`.
|
||||
7
blog/content/edition-2/posts/_index.es.md
Normal file
7
blog/content/edition-2/posts/_index.es.md
Normal file
@@ -0,0 +1,7 @@
|
||||
+++
|
||||
title = "Posts"
|
||||
sort_by = "weight"
|
||||
insert_anchor_links = "left"
|
||||
render = false
|
||||
page_template = "edition-2/page.html"
|
||||
+++
|
||||
Reference in New Issue
Block a user