Arquitectura del sistema operativo
Arriba de las presentes líneas puede usted ver la arquitectura del sistema en forma de un diagrama simple. Idealmente la arquitectura debe mantenerse tan simple como sea posible.
El kernel es minimalista; tiene el sistema de archivos, las librerías criptográficas y los drivers necesarios para que la CPU, RAM y el ethernet arranquen. Todo el resto va en forma de módulos.
El programa “dev” interactua con el kernel para disponibilizar tal dispositivo en forma de un archivo con la información en forma de; [key] { [value] }
Huginn en su instancia de sistema inicializa todos los servicios considerados de sistema y el administrador de red; Futhark.
Luego se inicia la instancia de usuario de huginn, inicializando solamente los servicios de usuario (sin permisos de administrador) que permitirán iniciar sesión y luego poder usar la shell (Rune) como las utilidades.
Kernel
Empecemos por algo básico; programar un kernel es extremadamente complejo.
Cuando uno hace un kernel es programar para un sistema bare-metal, por lo que uno debe asegurarse que el binario (el kernel en si) no tenga un formato ELF si no que sea binario puro, pero a su vez debemos asegurarnos que el firmware del dispositivo es capaz de detectar e iniciar nuetro programa por lo que el mismo debe tener cabeceras y detalles específicos. A su vez, el kernel debe controlar desde el inicio de la CPU hasta el manejo de la memoria RAM (disponibilizando y/o liberando según corresponda), y eso solamente por mencionar dos componentes.
- ¿Querés implementar soporte para enviar texto por UART? Toca programar la interfaz.
- ¿Querés implementar soporte para enviar texto por HDMI? Toca programar la interfaz y mucha paciencia por que ese protocolo no es sencillo.
- ¿Querés soporte de teclado? Ya sabes.
Lo normal no es que pase como en los sistemas que utlizan el kernel Linux en donde apenas sale un dispositivo nuevo en poco tiempo ya suele estar el driver.
Por lo que hay que saber varias cosas primero;
Aclaración e importante |
---|
Mucho de todo esto lo aprendí de; https://github.com/rust-embedded/rust-raspberrypi-OS-tutorials/tree/master por lo que verán similitudes y a veces el mismo código |
- No se pueden utilizar las librerías estándar del lenguaje (stdlib) debido a que vamos a hacer todo bare-metal.
#![allow(unused)] #![no_std] fn main() { }
- No se puede utilizar el clásico “main” como función de entrada, ya que el firmware tiende a usar/seguir el estándar basado en C puro
#![allow(unused)] #![no_main] fn main() { }
- Debemos implementar un sistema de manejo de errores.
#![allow(unused)] fn main() { use core::panic::PanicInfo; #[panic_handler] fn panic (_info: &PanicInfo) -> ! { loop {} } }
- Debemos asegurarnos que al momento de compilar no destruya el nombre de ciertas funciones para que el procesador, y su firmware, las puedan cargar luego. En concreto la función “_start” que debe utilizar el estándar C. Esto significa que el código que utilizariamos, en un no embebido, dentro de “main” ahora tiene que ir acá;
#![allow(unused)] fn main() { #[no_mangle] pub extern "C" fn _start() -> ! { [código] } }
- Debemos asegurarnos que el firmware del dispositivo (en este caso la Raspberry Pi) va a cargar el kernel en una dirección concreta de la memoria con determinados FLAGS en el CPU. Este seria el archivo linker.ld;
/* Copyright; https://github.com/rust-embedded/rust-raspberrypi-OS-tutorials/tree/master */
/* The physical address at which the the kernel binary will be loaded by the Raspberry's firmware */
__rpi_phys_binary_load_addr = 0x80000;
/* Set the above variable as the ENTRY point to load the binary in RAM */
ENTRY(__rpi_phys_binary_load_addr)
/* Flags:
* 4 == R
* 5 == RX
* 6 == RW
*/
PHDRS
{
segment_code PT_LOAD FLAGS(5);
}
SECTIONS
{
. = __rpi_phys_binary_load_addr;
/***********************************************************************************************
* Code
***********************************************************************************************/
.text :
{
KEEP(*(.text._start))
} :segment_code
}
-
Debemos poner el procesador en un cierto modo antes de iniciar lo que queremos, esto hace que sí o sí tengamos que usar cierto nivel de assembler para eso.
Esta parte es la que se ejecuta como ensamblador puro y luego da inicio al código en Rust.
Yo llamo a este archivo; “arm64.s”
#![allow(unused)] fn main() { // SPDX-License-Identifier: MIT OR Apache-2.0 // // Copyright (c) 2021-2022 Andre Richter <andre.o.richter@gmail.com> //-------------------------------------------------------------------------------------------------- // Definitions //-------------------------------------------------------------------------------------------------- // Load the address of a symbol into a register, PC-relative. // // The symbol must lie within +/- 4 GiB of the Program Counter. // // # Resources // // - https://sourceware.org/binutils/docs-2.36/as/AArch64_002dRelocations.html .macro ADR_REL register, symbol adrp \register, \symbol add \register, \register, #:lo12:\symbol .endm //-------------------------------------------------------------------------------------------------- // Public Code //-------------------------------------------------------------------------------------------------- .section .text._start //------------------------------------------------------------------------------ // fn _start() //------------------------------------------------------------------------------ _start: // Only proceed on the boot core. Park it otherwise. mrs x0, MPIDR_EL1 and x0, x0, {CONST_CORE_ID_MASK} ldr x1, BOOT_CORE_ID // provided by bsp/__board_name__/cpu.rs cmp x0, x1 b.ne .L_parking_loop // If execution reaches here, it is the boot core. // Initialize DRAM. ADR_REL x0, __bss_start ADR_REL x1, __bss_end_exclusive .L_bss_init_loop: cmp x0, x1 b.eq .L_prepare_rust stp xzr, xzr, [x0], #16 b .L_bss_init_loop // Prepare the jump to Rust code. .L_prepare_rust: // Set the stack pointer. ADR_REL x0, __boot_core_stack_end_exclusive mov sp, x0 // Jump to Rust code. b _start_rust // Infinitely wait for events (aka "park the core"). .L_parking_loop: wfe b .L_parking_loop .size _start, . - _start .type _start, function .global _start }
La línea; “ b _start_rust“ ejecutará la función en Rust llamada “_start_rust” que debe estar en el mismo scope que el código del puto 6 que lo carga.
Un buen approach es poner este booteo en un archivo separado y luego integrar el “main.rs” como un módulo que es llamado por “_start_rust”, o la función que se utilice.
- Nuestro código en Rust debe incluir el ensamblador anterior, para no poner todo el código ensamblador dentro del “main.rs” podemos poner esto para que lo tome desde un archivo;
#![allow(unused)] fn main() { mod boot { use core::arch::global_asm; /*========================================================================*/ /* The global_asm! macro allows the programmer to write arbitrary assembly*/ /* outside the scope of a function body, passing it through rustc and llvm*/ /* to the assembler. We include the assembler file; arm64.s */ /*========================================================================*/ global_asm!(include_str!("arm64.s")); } }
- De todos los primeros drivers que podemos generar, el UART suele ser el más fácil.
- Al momento de generar el binario podemos usar este comando para RPI3;
cargo rustc --release -- -C link-arg=--script=./linker.ld -C target-cpu=cortex-a53 --target aarch64-unknown-none-softfloat
Si es RPI4 el target de cpu es cortex-a72, para RPI5 y posteriores ir buscando cual es el provisto.
- La RPI usa el nombre kernel7.img para ARM32 y kernel8.img para ARM64. Como RavnOS es puro 64bits lleva ese nombre el kernel.
- Una vez compilado en ARM64 el kernel, debemos extraer el binario puro ya que por defecto tiene formato ELF;
aarch64-linux-gnu-objcopy -O binary --strip-all [linux_bin] [binary_dest]