rou2exOS Rusted Edition

krusty,

A second iteration of a DOS-like hobby OS in Rust.

Introduction

rou2exOS is a 64-bit DOS-like operating system (OS). The system is mainly written in Rust, but some portion of x86 assembly is used as well (inline + freestanding code for the stage2 kernel loading).

Fig.: Initialization procedure and system startup

Features

  • simple VGA operations,
  • network stack:
    • SLIP for IPv4 (can communicate over a serial line atm),
    • simple ICMP, UDP and TCP implementations,
    • very minimal HTTP (one running instance serves a static HTML page at the moment too),
  • FAT12 + Floppy block device implementation
    • support for reading and writing sectors and files, working with real floppies via QEMU
  • RTC clock
  • TAB autocompletion for files and directories
  • text editor (just a MVP now)
  • simple Snake-like videogame

Down below, the currently implemented command list is shown in the Fig. 2.

Fig.: Initialization procedure and system startup

Architecture and Components

The target CPU architecture is x86_64 at the moment. A support for the ARM architecture (aarch) is coming soon too.

Bootloader

First iteration of the system has got its own custom-written bootloader in x86 assembly to load the kernel to memory from a floppy image, to set proper registers and stack, and to execute the kernel right away. This enables one to have full control of what is being done from the very beginning of the boot process.

However, as this edition involves a 64-bit kernel, it is necessary to enable the Long CPU Mode before the control is given to the kernel. To make things easier a bit, the GRUB2 bootloader has been chosen to take care of the Real to Protected Mode switch.

Fig.: GRUB boot entry menu

After the control is given from the GRUB bootloader, the stage2 bootloader is taking the action henceforth. Its purpose is to set up GDT, IDT and simple paging before the long jump into the 64-bit Long Mode, where the Rust kernel can be called promptly.

The stage2 bootloader also takes care of the Multiboot2 pointer (multiboot_ptr) assignment. This means that the Multiboot2 address provided by GRUB is saved for later usage as the very first command executed. This address for example contains the framebuffer tag, where descriptions like enabled graphics dimensions and more are stored.

asm
 section .bss
 align 4

 global multiboot_ptr
 multiboot_ptr:
     resq 1

 ;
 ;
;

section .text
align 4

global _start

_start:
    mov [multiboot_ptr], ebx

The provided label can be then used in Rust code:

rust
unsafe extern "C" {
    pub unsafe static multiboot_ptr: u64;
}

Kernel

Kernel is the main part of the system. It consists of a shell command processor and executor, and custom component implementations described below.

Emulation in QEMU

Even though the target architecture is x86_64 baremetal environment, all testing and development is carried out using the QEMU emulation system.

All provided figures are screenshots of the system running in the QEMU emulator.

ISO Image

As the OS has to be packed in some form of a bootable image, the xorriso tool (under the hood of the grub2-mkrescue tool) is required for the proper image generation.

shell
grub2-mkrescue -o r2.iso iso/ \
    --modules="multiboot2 vbe video video_bochs video_cirrus gfxterm all_video"

Floppy Image

The OS provides a support for FAT12 filesystems, therefore a floppy image can be prepared to be mounted to the running system later.

shell
dd if=/dev/zero of=fat.img bs=512 count=2880
mkfs.fat -F 12 fat.img
echo "Hello from floppy!" > /tmp/hello.txt
mcopy -i fat.img /tmp/hello.txt ::HELLO.TXT

The generated FAT12-formatted image can be mounted like this (-fda fat.img flag):

shell
qemu-system-x86_64 \
    -boot d \
    -m 2G \
    -vga std \
    -cdrom r2.iso \
    -fda fat.img \
    -serial pty

Typical Usage

Typical command to boot the system from the ISO image (-cdrom flag) from drive (-boot d) with 2 GB of memory (-m 2G), with standard VGA graphics (-vga std), with serial port support via a psuedo teletype terminal (-serial pty), and with mounted floppy drive to read external data (-blockdev and -device flags):

shell
sudo qemu-system-x86_64 \
    -boot d \
    -m 2G \
    -vga std \
    -cdrom r2.iso \
    -serial pty \
    -blockdev host_device,node-name=floppy1,filename=/dev/sda \
    -device floppy,drive=floppy1

The sudo command is required as the access to the /dev/sda device (floppy drive) is restricted.

Initialization Procedure

As soon as the kernel is loaded and executed by the GRUB bootloader, the initialization procedure starts. This involves following checks:

  • Long CPU Mode check,
  • Multiboot2 tags reading,
  • FAT12 filesystem presence check,
  • VGA colors check.

After these checks, the ASCII art logo of the system is printed out and the control is given to the shell loop.

Graphics

One of the goals in the OS development is the ensure proper graphics environment. At the moment, only the VGA 80x25 Text Mode is supported. The framebuffer provided via the GRUB bootloader is to be utilized later on.

Filesystem

As far as the filesystem support is concerned, the FAT12 filesystem implementation is to be the first one in. The purpose is to support inter-OS file operation as one can now write and read files both in rou2exOS and Linux or Windows without any additional drivers needed.

FAT12 implementation provide:

  • generic file contents reading,
  • generic text file creation and overwriting,
  • file searching,
  • file deletion,
  • subdirectory creation and deleting.

In future, mainly FAT32 is waiting for its implementation in the OS.

fsck Tool

To check the filesystem consistency and correctness, a simple fsck tool is implemented.

Fig.: Filesystem check tool.

Network Stack

The OS itself has some kind of a simple networking implemented as well.

Serial-line and SLIP

As one of the first networking implementation, the serial line and the SLIP protocol to support IPv4 packets exchange has been chosen for its relative simplicity.

Ethernet and ARP

The Ethernet frame processing is supported, but is not thoroughly tested yet at the moment.

IPv4

Basic Internet Protocol version 4 packet handling and building is supported. Packets are primarily received and sent using the SLIP protocol through the serial line as was described already above.

rust
#[repr(C, packed)]
pub struct Ipv4Header {
    version_ihl: u8,
    dscp_ecn: u8,
    total_length: u16,
    identification: u16,
    flags_fragment_offset: u16,
    ttl: u8,
    pub protocol: u8,
   header_checksum: u16,
   pub source_ip: [u8; 4],
   pub dest_ip: [u8; 4],
}

Down below, the receive loop for the TCP datagrams processing is listed.

rust
pub fn receive_loop_tcp(
    conns: &mut [Option<tcp::TcpConnection>; MAX_CONNS], 
    callback: fn(conns: &mut [Option<tcp::TcpConnection>; MAX_CONNS], 
    packet: &[u8]) -> u8,
) -> u8 {
    let mut temp_buf: [u8; 2048] = [0; 2048];
    let mut packet_buf: [u8; 2048] = [0; 2048];
    let mut temp_len: usize = 0;

    serial::init();

    loop {
        // While the keyboard is idle...
        while port::read(0x64) & 1 == 0 {
            if serial::ready() &&  temp_len <= temp_buf.len() {
                if let Some(p) = temp_buf.get_mut(temp_len) {
                    *p = serial::read();
                }
                temp_len += 1;

                let temp_slice = temp_buf.get(..temp_len).unwrap_or(&[]);

                if let Some(packet_len) = slip::decode(temp_slice, &mut packet_buf) {
                    // Full packet decoded
                    let packet_slice = packet_buf.get(..packet_len).unwrap_or(&[]);
                    return callback(conns, packet_slice);
                }
            }
        }

        // If any key is pressed, break the loop and return.
        if port::read(0x60) & 0x80 == 0 {
            break;
        }
    }
    3
}

ICMP

A very simple implementation of the Internet Control Message Protocol (ICMP) is supported in the system as well. This covers a basic Echo message handling for Request and Reply types/codes.

rust
#[repr(C, packed)]
pub struct IcmpHeader {
    pub icmp_type: u8,
    pub icmp_code: u8,
    pub checksum: u16,
    pub identifier: u16,
    pub sequence_number: u16,
}

Response Handler

When run with QEMU with the -serial pty flag, a notice about mounted pseudo terminal is printed to the console:

shell
char device redirected to /dev/pts/5 (label serial0)

This device can be used to directly communicate with the OS itself over the emulated serial line.

The net-tools package has to be installed on the host system to provide support for the SLIP protocol. Also, the slip (Linux) kernel module has to be loaded.

shell
sudo modprobe slip
lsmod | grep slip

From now, one can start a SLIP listener and processor to create a sl0 network interface:

shell
sudo slattach -L -p slip -s 115200 /dev/pts/5

When the interface is created, the point-to-point link can be established with a proper IPv4 address pair assigned too (the second line prints the interface details):

shell
sudo ifconfig sl0 192.168.3.1 pointopoint 192.168.3.2 up
ip a show sl0

When the IPv4 addresses are assigned, SLIP listener is running, and the OS is run in the QEMU with proper flags, the following command can be executed in rou2exOS shell:

shell
response

This command starts an ICMP Echo Request handler. That means the system should now respond to a ping request:

shell
ping 192.168.3.2
Fig.: Ping Echo request from the Linux shell.
Fig.: Ping Echo reply from the rou2exOS ICMP handler.

UDP and TCP

Being the critical Layer 4 protocols, UDP and TCP are implemented too. TCP implementation provides a proper SYN/SYNACK connection control (connection state machine) for the basic network applications that kernel offers.

rust
#[repr(C, packed)]
pub struct UdpHeader {
    pub source_port: u16,
    pub dest_port: u16,
    pub length: u16,
    pub checksum: u16,
}
rust
#[repr(C, packed)]
pub struct TcpHeader {
    pub source_port: u16,
    pub dest_port: u16,
    pub seq_num: u32,
    pub ack_num: u32,
    pub data_offset_reserved_flags: u16,
    pub window_size: u16,
    pub checksum: u16,
   pub urgent_pointer: u16,
}

#[derive(PartialEq, Eq)]
pub enum TcpState {
    Closed,
    Listen,
    SynReceived,
    Established,
    FinWait1,
    FinWait2,
    Closing,
    TimeWait,
    CloseWait,
    LastAck,
}

pub struct TcpConnection {
    pub state: TcpState,
    pub src_ip: [u8; 4],
    pub dst_ip: [u8; 4],
    pub src_port: u16,
    pub dst_port: u16,
    pub seq_num: u32,
    pub ack_num: u32,
}

HTTP

As far as the Application Layer (Layer 7) is concerned, the HyperText Transmission Protocol (HTTP) was first to be implemented in the OS. Its implementation offers basic GET method requests handling, which is used for a very basic HTTP static site server.

rust
fn http_router(payload: &[u8], http_response: &mut [u8]) -> usize {
    let body: &str;
    let mut content_type: &str = "text/plain";

    if match_path(payload, b"/") || payload.starts_with(b"GET / HTTP/1.1") {
        body = "<html><body><h1>Welcome to rou2exOS HTTP server</h1></body></html>";
        content_type = "text/html";

    } else if match_path(payload, b"/rouring") {
        body = "<html><head><style>.index {width: 800px;margin-top: 70px;font-family: Helvetica;}.lefts2 {width: 200px;float: left;}.rights2 {width: 590px;float: right;}.foot {width: 550px;margin-top: 200px;font-family: Helvetica;clear: both;}</style><meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"><meta http-equiv=\"Content-language\" content=\"cs, en\"><title>The RouRa Project</title></head><body><center><div class=\"index\"><div class=\"lefts2\"><br><img src=\"https://rouring.net/plug.png\" width=\"200\"></div><div class=\"rights2\"><br><p style=\"font-size: 42px\">The RouRa Project</p><p style=\"font-size: 20px\">Už bude zase dobře</p></div></div><div class=\"foot\"><br><br><br><br>Rouring.net & ReRour 2k16</div></body></center></html>";
        content_type = "text/html";

    } else if match_path(payload, b"/json") {
        body = "{\"message\":\"Hello JSON\"}";
        content_type = "application/json";

    } else {
        body = "404 Not Found";
    }

    let body_len = body.len();
    let header = b"HTTP/1.1 200 OK\r\nContent-Type: ";
    let mut pos = 0;

    if let Some(slice) = http_response.get_mut(..header.len()) {
        slice.copy_from_slice(header);
    }
    pos += header.len();

    if let Some(slice) = http_response.get_mut(pos..pos + content_type.len()) {
        slice.copy_from_slice(content_type.as_bytes());
    }
    pos += content_type.len();

    if let Some(slice) = http_response.get_mut(pos..pos + 2) {
        slice.copy_from_slice(b"\r\n");
    }
    pos += 2;

    if let Some(slice) = http_response.get_mut(pos..pos + 16) {
        slice.copy_from_slice(b"Content-Length: ");
    }
    pos += 16;

    let response_slice = http_response.get_mut(pos..).unwrap_or(&mut []);
    pos += u32_to_ascii(body_len as u32, response_slice);

    if let Some(slice) = http_response.get_mut(pos..pos + 4) {
        slice.copy_from_slice(b"\r\n\r\n");
    }
    pos += 4;

    if let Some(slice) = http_response.get_mut(pos..pos + body_len) {
        slice.copy_from_slice(body.as_bytes());
    }
    pos += body_len;
    //http_response[pos..pos + body_pos].copy_from_slice(&body_slice);
    //pos += body_pos;

    pos
}

Snake Videogame

The Snake videogame is a simple experiment on how to provide a simple entertainment supplement to the system.

The multiplayer edition is coming soon too. The pure TCP connection is to be used for a simple P2P link.

Game Data

The game itself can run without any files provided in the current directory. That means an empty filesystem won't prevent the game from start.

However, if the filesystem is not writable, the High Scores data (SKSCORE.BIN) won't be persistently saved.

Fig.: The Snake videogame game data.

For the proper game to be rendered (red walls of danger), some level data are to be provided. rou2exos can read raw data from a generic TXT files, as shown below (LEVEL01.TXT).

Fig.: The Snake videogame: raw data for level 1.

The file itself contains lines of point touples, that encode the red wall coordinations (each touple per line, the read command cannot handle newline byte (\n) properly there).

Gameplay

The game is started using a simple snake command in the system shell. Game provides a start menu, where a new game can be started, some High Scores can be listed, or the player can exit back to shell (also using the ESC key). The arrow keys are to be used for the menu navigation (as well as for the snake navigation in the game).

Fig.: The Snake videogame menu.
Fig.: The Snake videogame High Scores.

When the level data are present in the current directory of the filesystem, those are loaded to render the red walls of danger. These walls prevent the snake from passing through, and results in the Game Over situation.

Fig.: The Snake videogame level 2 preview.

Player (snake) is to collect green asterisks (food) to grow and to gain the score points. When the point count reaches a multiple of 30, a new level is rendered and the snake is reset to the initial length and position.

Fig.: The Snake videogame game over.

When the game ends (also can be terminated using the ESC key), the score is used for the High Scores update procedure. This procedure compares the current score with other saved ones, and saves it when there is an empty slot (score 0), or the score is higher than the saved one.

Conclusion

While it may not seem like much, it has taught me a great deal, and it now supports complex features such as basic file handling and networking. I plan to continue developing this further.

Further references: