RavnOS
Imagen creada con Stable Diffusion 2.1 |
RavnOS, (del noruego; raven), es un sistema operativo programado en Rust. Tiene como objetivo ser minimalista, estable, seguro y moderno (esto tal vez lo conozcas como “estado del arte”).
Es mi proyecto personal de crear un sistema operativo desde cero, lo considero un hobby y no algo profesional.
Ejecutar sobre un sistema operativo existente
Para ejecutar el sistema sobre un sistema operativo existente, el kernel debe tener soporte para POSIX.
Los pilares de RavnOS son:
-
Minimalismo
No hay software innecesario en el código o en el sistema. Solo lo necesario para hacer el trabajo (producto mínimo viable).
El minimalismo también afecta el alcance del sistema, verifica ese punto.
-
Estabilidad y seguridad sobre características
La estabilidad y la seguridad del sistema tendrán prioridad absoluta sobre una característica “X”.
-
Aislamiento y portabilidad
Cada parte del sistema debe ser portable en las plataformas admitidas por Rust.
Cada parte del sistema debe ser autoalojada, no se permiten dependencias externas (a través de Internet). Todo debe ser local, ¿cómo puedes estar seguro de que la dependencia remota es segura? ¿cómo puedes estar seguro de que la dependencia remota se desarrollará hasta el final de los tiempos? ¿cómo puedes estar seguro de que la dependencia remota no romperá la compatibilidad?
Cada parte del sistema distribuida en forma binaria debe hacerse como estática, desde 2022, la mejor manera de lograrlo es utilizando la biblioteca estándar C MUSL (en el caso del kernel Linux).
-
Claves - Datos
La información se imprime en la pantalla y se maneja con la siguiente sintaxis:
[clave] { [dato] }
Esto permite trabajar con datos agrupados por claves y evita problemas al extraer datos cuando se utilizan caracteres como “”, ‘’, , , y otros.
-
Imagen del SO - Contenedor
La distribución final debe utilizar el esquema de “imagen del SO”; la parte base del sistema es inmutable y la parte mutable se utiliza a través del esquema de contenedores.
Requisitos
-
Rustc
-
Un terminal
-
Cargo con la cadena de herramientas (‘toolchain’) objetivo que desees/necesites:
Esto no es obligatorio, pero te ayudará a compilar con un solo comando.
-
Objetivo de Musl:
Esto no es obligatorio, pero te recomiendo utilizarlo para compilar de forma estática en el caso de que se ejecute sobre Linux.
Versionado
El esquema de versionado para las versiones sigue la especificación de Versionado Semántico v2 ( https://semver.org/spec/v2.0.0.html ), aquí tienes un resumen;
Dado un número de versión MAJOR.MINOR.PATCH, se incrementa:
- la versión MAJOR cuando se realizan cambios incompatibles en la API/programa,
- la versión MINOR cuando se agregan funcionalidades de manera retrocompatible,
- la versión PATCH cuando se realizan correcciones de errores retrocompatibles.
Etiquetas adicionales para versiones previas y metadatos de compilación están disponibles como extensiones del formato MAJOR.MINOR.PATCH.
Un cambio desde el desarrollo temprano es que cada versión MINOR también indica cada característica nueva/existente dentro de los programas internos. Y las correcciones de rendimiento se incluyen en la sección “PATCH” en lugar de la sección “MINOR”.
Con este esquema de versionado, sabes que dentro de cada versión MAJOR el sistema siempre es compatible con las versiones más antiguas.
Documentación
Esta web está creada con el progrmaa “mdbook” (Markdown Book) y sirve como toda la documentación del sistema operativo.
Compilación
Hay dos formas de compilar RavnOS:
- Cargo
Recuerda reemplazar [x86_64/aarch64] por tu caso.
Recomiendo utilizar esta configuración dentro de “~/.cargo/config”;
[build]
target= "[x86_64/aarch64]-unknown-linux-musl"
jobs=20
rustflags = ["-C","opt-level=2","-C","debuginfo=0","-C", "target-feature=+crt-static","-C","target-cpu=native"]
Agrega el objetivo construido con Musl;
rustup toolchain add --force-non-host stable-x86_64-unknown-linux-musl && rustup target add [x86_64/aarch64]-unknown-linux-musl
Para compilar;
cargo build --release --target [x86_64/aarch64]-unknown-linux-musl
Todos los binarios se encontrarán en “target/[TARGET]/release”.
Si tienes restricciones de espacio, utiliza “strip” en los binarios finales. Esto se debe a que incluso con el objetivo “–release” aún se pueden encontrar símbolos de depuración en el binario final.
- Rustc
Para cada librería de RavnOS, primero debes compilarla como un archivo de objeto y luego puedes usarla en el proceso de compilación;
rustc --crate-type=rlib --crate-name libconfarg [PATH_TO_LIBCONFARG]/src/lib.rs -o libconfarg.rlib
rustc --crate-type=rlib --crate-name libstream [PATH_TO_LIBSTREAM]/src/lib.rs -o libstream.rlib
rustc --crate-type=rlib --crate-name libfile [PATH_TO_LIBFILE]/src/lib.rs -o libfile.rlib
Luego puedes enlazarla en la compilación del binario;
rustc --target=[x86_64/aarm64/etc]-unknown-linux-musl -C opt-level=2 -C target-feature=+crt-static --extern libconfarg=libconfarg.rlib --extern libfile=libfile.rlib --extern libstream=libstream.rlib [COMPONENT]/src/main.rs -o [final_name]
Con el comando anterior, obtendrás el binario final de [COMPONENT] en forma final estática (es decir, enlazada estáticamente) con nivel de optimización 2 y librerías (crates) específicas.
Al igual que con cargo, te recomiendo utilizar “strip” en los binarios finales para eliminar los símbolos de depuración.
Antes - Después de “strip” en el binario
Antes de “strip”;
f: ls [10/4/2023 10:44:0 UTC-0] ["sticky bit", "rwx", "r-x", "r-x"] [uid=1000(shyanjmc) gid=1000(shyanjmc)] 4M
Después de “strip”;
f: ls [10/4/2023 10:49:11 UTC-0] ["sticky bit", "rwx", "r-x", "r-x"] [uid=1000(shyanjmc) gid=1000(shyanjmc)] 542K
El comando “strip” elimina los símbolos de depuración, que ocupan el 86.45% del espacio.
Derechos de autor
Yo, Joaquin Manuel ‘ShyanJMC’ Crespo, soy el principal desarrollador y creador de este proyecto.
-
2023-02-13; Tengo un acuerdo firmado de renuncia de derechos de autor con mi empleador, en el que la empresa renuncia a los derechos de autor de este proyecto.
-
2023-02-13; Estoy en proceso de registrar el software a mi nombre en la República Argentina.
-
2023-03-13; Este proyecto está registrado en la República Argentina a mi nombre.
Licencia
RavnOS y todas sus partes están licenciadas bajo GPLv3.
Contacto
Si quieres contactarme, puedes hacerlo a través de:
Correo electrónico:
- shyanjmc@proton.me
- shyanjmc@protonmail.com
- joaquincrespo96@gmail.com
Linkedin:
- https://www.linkedin.com/in/joaquin-mcrespo/
Contribuciones y soporte
Por ahora soy el principal y único desarrollador de este proyecto, tal vez en el futuro permita colaboraciones.
Si quieres apoyar este proyecto, puedes:
-
Unirte a mi Patreon:
https://patreon.com/shyanjmc
-
Si preferís donarme crypto:
-
DAI (ERC-20) - La crypto estable preferida;
0x27219354cC70dE84e7fae0B71E9e2605026b10B2
-
Bitcoin (BTC);
16n6GP4XkZiyoGCZei6uxdpohCkPm7eQ7L
-
Ethereum (ETH);
0x27219354cC70dE84e7fae0B71E9e2605026b10B2
-
Tu apoyo financiero es muy valioso y me ayuda a seguir trabajando en el desarrollo de RavnOS. ¡Gracias!
También acepto hidromiel como apoyo personal al proyecto, mujer/hombre de honor :)
Arquitectura ARM
ARM (Advance RISC Machine) es una arquitectura de procesador basado en RISC que se caracteriza por ser extremadamente eficiente en el uso de energía. Posee varias familias de procesadores y controladores adaptados a un uso específico cada una.
“RISC” son las siglas de; “Reduced Instruction Set Computing” el cual es una filosofía de diseño de microprocesadores o microcontroladores en donde se utiliza un conjunto de instrucciones de tamaño fijo y reducido (en cantidad). Esto quiere decir que el procesador o controlador posee instruciones simples que se optimizan para trabajar en conjunto y realizar operaciones más complejas.
ARM Ltd Holdings (la empresa propietaria) es la propietaria de esta arquitectura, y licencia los núcleos que crean a los fabricantes que deseen usarlos.
Generalmente las instrucciones se dividen en dos grandes grupos; carga/descarga de datos en/desde RAM y modificación (y posterior almacenamiento) de datos en registros.
Al conjunto de instrucciones que soporta el procesador se le llama; ISA (Instruction Set Available).
Aunque un CPU tenga un determinado conjunto ISA, puede ser que otro CPU de la misma arquitectura y fabricante pero de distinta familia tenga un ISA con diferencias mínimas o más notables (como instrucciones adicionales o la falta de alguna específica). Por eso siempre se recomienda utilizar el set básico para garantizar la mayor compatibilidad posible, igualmente cuando ARM Ltd Holdings licencia a un fabricante un determinado núcleo de CPU, el mismo indica que se debe mantener la compatibilidad de la ISA en su conjunto.
La arquitectura ARM posee varias familias, especialmente adaptadas a determinados requerimientos;
-
Sintaxis de la tabla;
- Lo que se encuentra entre “[]” son variables.
- Lo que se encuentra entre paréntesis de una versión de arquitectura son los núcleos y sus modelos.
Familia | Propósito | Versión de arquitectura | Arquitectura de bits |
---|---|---|---|
Cortex M[0-85] | Microcontrolador | ARMv6-M (M0, M0+, M1), ARMv7-M (M3), ARMv7E-M (M4, M7), ARMv8-M (M23, M33), ARMv8.1-M (M55, M85) | 32 |
Cortex R[4-82] | Real Time | ARMv7-R (R4, R5, R7, R8), ARMv8-R (R52, R82) | 32 (R4, R5, R7, R8, R52) y 64 (R82) |
Cortex A[5-720] | Microprocesador general | ARMv7-A (A5, A7, A8, A9, A12, A15, A17), ARMv8-A (A32, A34, A35, A53, A57, A72, A73), ARMv8.2-A (A55, A75, A76, A77, A78, A65, A65AE), ARMv9.0-A (A510, A710, A715), ARMv9.2-A (A520, A720) | 32 (A5, A7, A8, A9, A12, A15, A17, A32, A34, A35, A53, A57, A72, A73, A55, A75, A76, A77, A78) y 64 (A5, A7, A8, A9, A12, A15, A17, A32, A34, A35, A53, A57, A65, A65AEA72, A73, A55, A75, A76, A77, A78, A510, A710, A715, A520, A720) |
Cortex X[1-4] | Microprocesador de servidor | ARMv8.2-A (X1), ARMv9.0-A (X2, X3), ARMv9.2-A (X4) | 32 (X1) y 64 (X1, X2, X3, X4) |
Cortex Neoverse | Microprocesador de datacenter, supercomputadoras y edge | ARMv8.2-A (N1), ARMv8.2-A (E1), ARMv8.4-A (V1), ARM9.0-A (E2, N2, V2) | 32 (N1) y 64 (N1, N2, E1, E2, V1, V2) |
Algunos fabricantes licenciados como; Apple y NVIDIA, usan núcleos muy personalizados por ellos, por lo que a veces es muy dificil enmarcarlos en alguna familia.
System on a chip (SOC)
Esta arquitctura (ARM y también RISC-V) tiene una característica importante; permite unir diferentes núcleos (aunque sean de diferentes familias, aunque no es lo común) en un mismo chip, según necesidades del fabricante.
Adicionalmente para tener los menores costos operativos se suele integrar otros componentes en conjunto con los núcleos para dar la mejor performance. Es muy común unir al conjunto la memoria RAM para disminuir el consumo eléctrico y la latencia entre ambos (CPU y RAM). Pero esto dependerá del fabricante, ya que algunos deciden unir una GPU, un modem 4G/5G, el almacenamiento, etc mientras que otros solo crean la CPU en si (como los procesadores Ampere).
Por lo tanto los sistemas en un chip (SOC en inglés) pueden integrar una multitud de componentes.
Al blueprint (el plano) de un SOC se le conoce como “die”;
DIE del Apple M1 |
---|
Copyright; https://x.com/Locuza |
Consideraciones importantes sobre el boot
A diferencia de la arquitectura x86 (Intel) y x86_64 (AMD), ARM no tiene estandarizado el sistema de booteo a utilizar.
Esto significa que cada fabricante implementa el firmware como considere. El firmware es el programa más básico, que reside en el chip de forma directa y que se encarga de controlarlo (al chip) lógicamente para que pueda establecer sus funciones más básicas.
Por lo que, el firmware que se utiliza en la Raspberry Pi 4 (núcleos A72) no es el mismo que se utiliza en Pine64 RockPro64 (núcleos A53 y A72), y por ende la forma en que inician el procesador, la memoria RAM y luego el sistema operativo son distintos en cada uno.
Esto hace que cada programa que se quiera cargar en modo bare metal o embebido debe tener soporte específico para ese SOC y su firmware subyacente.
Bibliografia
- Mi experiencia técnica en el campo
- https://wiki.osdev.org/ARM_Overview
Estándar de programación para RavnOS
Todo el desarrollo de RavnOS se guia por las siguientes premisas;
-
El flujo del código debe ser simple, no complejo.
No siempre un código más complejo es de mejor calidad, muchas veces todo lo contrario.
Un código más simple permite entenderlo mejor y detectar más fácilmente; errores y oportunidades de optimización.
-
No se permiten “unwrap” ó “expect”, todo error debe ser manejado manualmente para decidir si se recupera o no.
El uso de los “unwraps” y “expects” no es inútil; sirven a un propósito. Pero en RavnOS todo Result debe ser evaluado para evaluar individualmente si se prosigue o si se realiza otra acción.
Actualmente estoy trabajando en un sustituto para poder lidiar con diferentes situaciones que requieran o no una parada de procesamiento.
-
Uso del stack antes que el del heap
El uso del heap permite la locación dinámica de memoria pero a su vez dificulta la detección de erorres de bajo nivel y consume mayores ciclos de procesamiento.
Por supuesto, no siempre se puede saber el tamaño de la entrada de información y para evitar erorres de seguridad (como los buffer overflow) se debe utilizar el heap en tales casos. Pero de ser posible se usará el stack en vez del heap.
-
Auto contención
No se permiten el uso de librerías externas (los crates), todo el sistema debe estar autocontenido en su propio código.
Para evitar problemas de derechos de autor se crea todo desde cero.
-
Modularidad y máximo producto viable
Todo programa debe estar diseñado con la idea del “máximo producto viable”; el programa final va a tener X características y no más.
Por supuesto, los tiempos cambian y también las necesidades. Para evitar los futuros problemas de código e integraciones, se debe usar una arquitectura modular que permita el rápido y limpio remplazo de sus partes.
-
Compatibilidades y portabilidad
Todo el userland de RavnOS (véase; todo lo que está por encima del kernel) debe ser portable a otros sistemas operativos que soporten la librería estándar de Rust al completo. Esto permite que, al igual que el proyecto GNU, partes del sistema puedan funcionar sobre un kernel diferente.
-
Aislamiento
El sistema base tiene la finalidad de ser inmutable de cara al usuario final.
Todo lo que el usuario instale/modifique se aplica en su propio “home” como entorno aislado del resto.
-
Seguridad
Este es el punto más importante y bajo ningún concepto o escenario es negociable; la seguridad del sistema está siempre primero sobre cualquier otra característica.
Diseño del sistema operativo
El diseño de RavnOS es modular y minimalista.
- El sysinit del sistema, Huginn, se divide en dos instancias; la de sistema (que inicia los servicios escenciales) y el de usuario (que inicia los servicios específicos de cada usuario).
- La shell del sistema, Rune, integra como builtins todos los programas básicos necesarios (show, ls, etc).
- El package manager, Muninn, se encarga de instalar los programas en el home del usuario (~/.local), aislando así a los usuarios entre sí y al mismo del sistema base.
- El gestor de red, Futhark, se encarga de leer las configuraciones de red basándose en el usuario que lo ejecuta, aislando así las redes.
La finalidad de tal diseño es, en conjunto con el estándar de programación de RavnOS, crear un sistema tan estable como sea posible que pueda actualizarse en tiempo real sin necesitar reiniciarse (salvo el kernel o el sysinit de sistema).
Muchas de las ideas utilizadas tanto en el diseño de arquitectura del sistema como del estándar se basan en las cosas buenas que tienen tanto las distribuciones GNU/Linux como los sistemas BSD (OpenBSD y DragonFlyBSD en específico). Por lo que si presta atención técnica, puede detectar cosas de ambos sistemas.
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]
Código fuente
El código fuente de RavnOS puede encontrarse en este repositorio de GitHub;
Así mismo, y por que la resilencia es importante, acá también tenés el código fuente;
Rune - La shell del sistema
Rune es la shell de RavnOS, es un shell auto contenida en donde todos sus built-ins (comandos internos) son los necesarios para un mínimo uso del sistema:
Con todos los builtins contenidos deberías poder hacer todo lo que necesitas para poder administrar de forma básica el sistema operativo.
Actualmente estoy trabajando en que la shell pueda reconocer una cantidad indefinida de redirecciones (<, > y 2>) ya que actualmente solo reconoce una (1) sola por cada comando. Cuando esto esté listo debería entrar en fase “BETA”, ya que actualmente estamos en la “ALPHA”.