SnakeOS - Rustban írt, bootlható kígyójáték minimális kernellel

Címkék
  • Play snake on any x86_64 CPU
    • Let's have fun!
  • Dynamic memory management
    • The snake can grow!
  • Interrupt handling
    • We can read the keyboard!
  • Async/Await support
    • We can update the world and read user input at the same time!
  • Only 212kB kernel size
    • You can even put this on a 8-inch floppy disk!

Az szerző az alapötletet a Writing an OS in Rust blogból merítette. A projekt megtalálható a GitHub-on.

Hozzászólások

'Only 212kB kernel size' nem nagy az egy text mode snakehez ?

Valaki emlegszik meg mekkora volt DOS 5.0/3.3 'sys a:' utani foglalas, ill menyi volt quickbasic demo jatek merete ?
https://en.wikipedia.org/wiki/Nibbles_(video_game)

Amit nem lehet megirni assemblyben, azt nem lehet megirni.

(Minden mondatom elé képzeljétek oda, hogy "úgy rémlik", mert már nagyon régen volt.)

Mintha az alap DOS 5.0 többet evett volna, mint 212kB. 3.1 -> 3.3 közöttt is elég nagy ugrás volt. A 3.1-es kód (talán) nem címzett ki 64kB-ból, szóval az bufferekkel, meg gwbasic-kel együtt is bele kellet férjen 192kB-ba.

Dolgoztam egy IBM PC-vel, amiben összesen csak 256kB volt. Nem 512, vagy 640. Azon biztosan futott a 3.1-es DOS és a gwbasic.

Hát igen, a Rust-ból elkezdünk C-t csinálni:

Cargo.toml állományba berakjuk:

[dependencies]
libc = { version = "0.2", default-features = false }

[profile.release]
lto = true
panic = "abort"

Az általad hivatkozott példafájlon túl, ha write!()/writeln!() makrókat akarjuk low level kiterjeszteni, no_std módon csak core dolgokat használva:

#![no_std]
#![no_main]

#[panic_handler]
fn my_panic(_info: &core::panic::PanicInfo) -> ! {
    loop {}
}

use core::fmt::{self, Write};

struct Stdout;
// write!() és writeln!() kiterjesztése
impl Write for Stdout {
    fn write_str(&mut self, s: &str) -> fmt::Result {
        let ret = unsafe { libc::write(libc::STDOUT_FILENO, s.as_ptr() as *const _, s.len()) };
        if ret == s.len() as isize {
            Ok(())
        } else {
            Err(fmt::Error)
        }
    }
}

#[no_mangle]
extern "C" fn main(_argc: isize, _argv: *const *const u8) -> isize {
    let _ = write!(&mut Stdout, "Hello world!  ");
    let _ = writeln!(&mut Stdout, "Hello world!");
    0
}

Eredmény x86_64 architektúrára való lefordítás és strip után: 14392 byte.
Összehasonlításként C-ben egy

#include <stdio.h>

int main() {
    printf("Hello world!");
}

gcc -O2 -s után 14464 byte,
clang -O2 -s után 14472 byte.

Ritka eset lehet, amikor Rust esetén a méret miatt ennyire bűvészkednénk. Mikrovezérlőn eleve no_std van használatban, oké.
Talán OpenWRT-s kicsi memóriás routerekben futó Linuxoknál jöhet az iménti trükk jól.

Megjegyzem, akkor is kicsi lesz maga a Rust bináris, ha az alap dolgokat ki lehet rakni majd shared lib-be, ahogy a libc esetén is kint van.

Ekkor egy sima println!("Hello World"); is kicsi lesz (14392 byte), a többi a /usr/lib/ mappában mind dinamikus LIB lesz.
Dinamikus fordításhoz a projektnél a .cargo/config.toml -be ennyit kell beírni:

[build]
rustflags = ["-C", "prefer-dynamic",]

Viszont addig nem futtatható az ilyen bináris, amíg nem lesznek benne a /usr/lib/ -ben az alap Rust LIB-ek.

:)
Egyik hátránya a Rustnak, hogy nehéz a tanulási görbéje. Főleg az ownership környékének készségszintű elsajátítása a vízválasztó.
Na meg sokkal összetettebb nyelvi elemei vannak. A fenti példa egyébként nem a kezdő szint, ezek már a haladó szint.

A normál szint kb. a

fn main() {
    println!("Hello, world!");
}

És hasonlók. Szépen felhasználva a Rust std libjét, benne sok-sok finomsággal és tényleg csak használni és továbbépíteni a lib-eket.
A fenti bonyolítás főleg a mikrovezérlős low-level lib-ek esetén jön elő. Jó hír viszont, hogy napjainkban azt is csak be kell rántani a crates.io-ról és a te programod innentől kulturált kinézetű marad, a low level részek a berántott crate-ban vannak.

Lásd még:
   https://crates.io/crates/stm32-hal
   https://crates.io/crates/stm32-hal2
   https://crates.io/crates/stm32-eth
   ...

Tehát a nem kezdőknek szánt részek a crate-kban már le vannak programozva, azokat csak használod.
Nézd meg például a mikrovezérlős ethernet kezelő rész felhasználását az utolsó linken.

Hat, eleg sokat foglalkozom az SMT32Fxxx-esekkel, mondhatni. Ugy altalaban eleg jol ismerem a lelkivilagukat... de hogy ez mit csinalhat vajon:

    fn pin_config(&self, pin: usize) -> &stm32f1xx::GPIOBitbandConfigBlock {
        let registers: &stm32f1xx::GPIOBitbandRegisterBlock = ::bitband::to_bitband_address(self);
        &registers.config(pin)
    }

Szoval oke hogy meredeken indul a tanulasi gorbe, de utana cserebe exponencialisan megmeredekebb lesz :))

Igen, a modulok használata az modul dokumentáció + lexika.
Az általad kérdezett például visszatér a

    pub struct GPIOBitbandConfigBlock {
        mode_low: VolatileCell<u32>,
        mode_high: VolatileCell<u32>,
        cnf_low: VolatileCell<u32>,
        cnf_high: VolatileCell<u32>,
    }

struktúra referenciájával. De ez a struktúra ne érdekeljen, csak legyen példányosítva. Helyette nézd meg milyen metódusok vannak alább:
   https://docs.rs/stm32-hal/0.2.0/src/stm32_hal/gpio.rs.html#32
És ezeket a metódusokat hivogasd és "jó lesz neked". Lásd a dokumentációját a példával:
   https://docs.rs/stm32-hal/0.2.0/stm32_hal/gpio/index.html

// Modifies corresponding GPIO configuration bits without reads
gpioc.pin_config(13).output2().open_drain();

Ennyi és nem több vele a feladatod. A low level dolgokkal nem kell foglalkoznod. Ha igen, akkor viszont
  - nincs kész a modul
  - jön a "haladó szint" (lehet a félkész modullal szívni)

Helyette nézd meg milyen metódusok vannak alább:  https://docs.rs/stm32-hal/0.2.0/src/stm32_hal/gpio.rs.html#32

Igenigen, pont innen masoltam be az elrettento peldat ;)

Egy teljes konfiguralast tudnal mutatni itten peldanak? Pl hogy a PB6/PB7-re rameppelem az USART1 TX/RX-et, es el is kezdem hasznalni? Pl ha van adat, akkor beolvasom, ha kisbetu akkor nagybetuve alakitom, majd megy vissza a feladonak? 
 

Remélem lesz kis időm a következő héten. Van valahol egy bluepill lapkám és összedobok egy alap UART példát.
Addig érdekességképpen egy USB-soros: https://github.com/stm32-rs/stm32f4xx-hal/blob/master/examples/usb_seri…
Itt látszik, hogy az egyébként bonyolult USB device kezelő rész annyira szépen meg van alkotva egy crate-ben, hogy a metódusok felhasználása minimális kódolást igényel. Az inicializálás, típus és descriptor beállítás szintén faék egyszerűen alkalmazható (43. sor).

Az már csak ráadás, hogy pont a 0x61 -> 0x41 konverziót láthatod a belsejében, mint ASCII nagybetűsítést. :)
Megjegyzem, normál esetben amikor Rust std-t használunk, akkor UTF8 Stringek vannak és minden nyelv betűit amit lehet nagybetűsíti a to_uppercase metódus, azaz nem csak az ASCII-re fókuszál. Ez mikrovezérlőnél nem játszik, itt no_std a praktikus irány. Rust std-vel UTF8 Stringek nagybetűsítésre lásd ezt a példát [RUN].

Sorost összeraktam, leteszteltem Bluepill lapkán. ASCII nagybetűre alakít, ahogy kérted.

#![no_std]
#![no_main]

extern crate panic_halt;
use cortex_m_rt::entry;
use stm32f1xx_hal::{pac, prelude::*, serial};

#[entry]
fn main() -> ! {
    let p = pac::Peripherals::take().unwrap();
    let mut flash = p.FLASH.constrain();
    let mut rcc = p.RCC.constrain();

    let clocks = rcc.cfgr.freeze(&mut flash.acr);
    let mut afio = p.AFIO.constrain(&mut rcc.apb2);
    let mut gpioa = p.GPIOA.split(&mut rcc.apb2);
    let tx = gpioa.pa9.into_alternate_push_pull(&mut gpioa.crh);
    let rx = gpioa.pa10;
    let mut usart1 = serial::Serial::usart1(
        p.USART1,
        (tx, rx),
        &mut afio.mapr,
        serial::Config::default().baudrate(38400.bps()),
        clocks,
        &mut rcc.apb2,
    );

    loop {
        if let Ok(x) = usart1.read() {
            let _ = usart1.write(x & !0x20);
        }
    }
}

Azt hiszem, hogy láttam erről egy videón a YouTube-on. A fazon kicsit fogalomzavarban van, mert attól, hogy ő egy Rust OS projektből lopkodta össze a dolgokat, az nem azt jelenti, hogy ez egy OS. Ezt a műfajt úgy hívják, hogy bare metal programozás, ilyenek a 80-as, 90-es években is voltak, hogy programozók hobbiként, meg gyakorlásként írtak ASM-ben ilyen 256-512 bájtos bináris kódokat, amik belefértek az MBR-be, így bootloader se kellett, természetesen kernel se. Ez a SnakeOS csak annyiban más, hogy mennie kell modern UEFI-s gépen is, meg védett 64 bites módból kell gazdálkodjon, így már bootloader kell, meg egy nagyon minimális extra kód, de még így se nevezhető OS-nek. Még mindig bare metal.

Windows 95/98: 32 bit extension and a graphical shell for a 16 bit patch to an 8 bit operating system originally coded for a 4 bit microprocessor, written by a 2 bit company that can't stand 1 bit of competition.”

Most is vannak 256 byte intrók, nem csak a '80-as, '90-es években voltak. Itt van pl. az egyik a 2020-as Revision party-ról. 8 effekt és hang, 256 byte-ban!
Memories - Hellmood / Desire
https://youtu.be/Imquk_3oFf4