В этой статье мы пройдём путь создания простого, но функционального ядра операционной системы на языке Rust.
Разумеется, мы не будем собирать полноценную альтернативу Linux, но сосредоточимся на ключевых компонентах, которые лежат в основе любого ядра.

Мы реализуем:

  • Примитивный терминал — вывод текста прямо на экран.

  • Обработку команд — базовый ввод и реакция на команды пользователя.

  • Получение времени и даты — извлечение из прерываний RTC.

  • Динамическую память (кучу) — простую реализацию для хранения данных.

Цель статьи — показать, что даже с нуля можно создать ядро, способное выполнять базовые задачи низкоуровневого взаимодействия с оборудованием. Всё это — на современном, безопасном языке Rust.

Подготовка окружения

Для написания ядра мы будем использовать библиотеку bootloader, так как она обладает хорошей документацией и идеально подходит для таких low-level проектов.

Установка необходимых компонентов (для Linux)

Сначала установим все необходимые утилиты:

sudo apt update
sudo apt install -y qemu qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils virt-manager

Установим Rust и нужные инструменты:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Затем установим нужную версию Rust toolchain (важно использовать nightly):

rustup toolchain install nightly-2024-11-08
rustup default nightly-2024-11-08
rustup update nightly

Добавим необходимые компоненты для компиляции ядра:

rustup component add llvm-tools-preview
rustup component add rust-src

Проверим версию компилятора:

rustc --version

Установим bootloader :

cargo install bootimage

Начало

Главный вопрос почему без std? Объяснение #![no_std] и точки входа

Одной из ключевых особенностей написания ядра операционной системы на Rust является отказ от стандартной библиотеки (std). Но зачем?

Почему нельзя использовать std?

Стандартная библиотека Rust (std) зависит от операционной системы — она использует:

  • системные вызовы (например, для работы с файлами или потоками),

  • аллокацию через ОС (вроде malloc, read, write),

  • функции стандартной C-библиотеки.

А теперь главный вопрос:
Что, если никакой операционной системы ещё нет?
Верно — мы её только пишем! Поэтому:

std просто не может работать в среде, где нет ядра, системных вызовов и libc.

Что делать? Используем #![no_std]

Чтобы отключить стандартную библиотеку, в корневом файле проекта (обычно main.rs или lib.rs) указываем:

#![no_std]
#![no_main]

Это означает, что мы будем писать полностью автономный код, без зависимостей от ОС.

Точка входа в ядро: _start

После настройки #![no_std] нам нужно вручную определить точку входа в программу, потому что main() работать не будет.
Мы пишем:

#[no_mangle]
pub extern "C" fn _start() -> ! {
    // здесь начинается выполнение нашего ядра
}

Пояснение:

  • #[no_mangle] — отключает автоматическое изменение имени функции компилятором (чтобы оно осталось _start и было видно загрузчику).

  • extern "C" — говорит, что мы используем соглашение о вызовах C (важно для совместимости на уровне ABI).

  • -> ! — означает, что функция никогда не возвращает управление (что логично — это бесконечно работающее ядро).

Обработка ошибок: panic_handler

При возникновении ошибки компилятор вызывает panic!(). Но без std мы обязаны сами реализовать обработчик паники:

use core::panic::PanicInfo;

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

Это минимальный обработчик, который просто зацикливает выполнение (на практике туда добавляют вывод информации о панике в терминал или лог).

Скрытый текст

Код целиком

#![no_std]
#![no_main]

use core::panic::PanicInfo;

#[no_mangle]
pub extern "C" fn _start() -> ! {
    // здесь начинается выполнение нашего ядра
}

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

На данный момент наше ядро ничего не выводит, оно в принципе ничего не делает, а просто висит.
Надо это исправлять.

Для начала добавим вывод текста.


Вывод текста

Так как мы уже поняли что std не работает, а значит и стандартный println!() работать не будет. Вместо этого мы будет подавать byte code по адресу, для вывода на экран.

Для вывода текста используется VGA текстовый буфер с адресом 0xb8000 .

Что такое VGA текстовый режим?

VGA (Video Graphics Array) в текстовом режиме отображает 80 столбцов × 25 строк, т.е. 2000 символов.

Для начала разберём небольшой пример.

static HELLO: &[u8] = b"Hello World!";

#[unsafe(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 {}
}

Где мы выведим тест Hello World! на экран синим цветом.

Скрытый текст

Как это работает?

  1. мы создаём массив байт с текстом Hello World!

  2. Создаём ссылку на адрес 0xb8000.

  3. Перебираем наш массив на буйты и позиции этих байтов (enumerate).

  4. *vga_buffer.offset(i as isize 2) = byte; - записывает в буфер по позиции i код символа.

  5. *vga_buffer.offset(i as isize * 2 + 1) = 0xb; - записываем на позицию + 1 от символа его цвет.

Скрытый текст

Далее мы заменим этот код вывод на отдельные функции для удобного вызова

use crate::constants::{COLOR_STATUS_BAR, COLS, ROWS};

pub fn write_char(row: usize, col: usize, character: u8, color: u8) {
    let vga_buffer = 0xb8000 as *mut u8; // Адрес VGA буфера

    // Рассчитываем смещение, используя строки и столбцы
    let offset = (row * 80 + col) * 2;

    // Записываем символ в VGA буфер
    unsafe {
        *vga_buffer.offset(offset as isize) = character;
        *vga_buffer.offset(offset as isize + 1) = color;
    }
}

pub fn clear_screen(width: u16, height: u16) {
    let vga_buffer = 0xb8000 as *mut u8;
    for i in 0..(width as usize * height as usize * 2) {
        unsafe {
            *vga_buffer.offset(i as isize) = 0;
        }
    }
}

pub fn print_buffer(buffer: *mut [[u8; COLS]; ROWS]) {
    let width = COLS;
    let vga_buffer = 0xb8000 as *mut u8;
    unsafe {
        for row in 0..ROWS {
            for col in 0..COLS {
                if row == 24 {
                    *vga_buffer.offset((24 as isize * 80 as isize + col as isize) * 2) = b'_';
                    *vga_buffer.offset((24 as isize * 80 as isize + col as isize) * 2 + 1) =
                        COLOR_STATUS_BAR;
                }
                if (*buffer)[row][col] != 0 {
                    *vga_buffer.offset((row as isize * width as isize + col as isize) * 2) =
                        (*buffer)[row][col];
                    *vga_buffer.offset((row as isize * width as isize + col as isize) * 2 + 1) =
                        0x07;
                }
            }
        }
    }
}

pub fn write_string(row: usize, col: usize, s: &str, color: u8) {
    for (i, byte) in s.bytes().enumerate() {
        write_char(row, col + i, byte, color);
    }
}

В этом коде мы сразу добавили ручную задачу позиции ввода символа, это нам понадобится для реализации полноценного терминала.

Мы расмотрели вывод текста на экран.


Ввод текста

Ввод текста неотъемлемая часть терминала.
Для того чтобы реализовать ввод текста с клавиатуры и отображения его на экране нам необходимо использовать прерывание.

Теория: что такое прерывание?

Прерывание — это способ, с помощью которого устройства (например, клавиатура, таймер, сетевая карта и т.д.) могут прервать основной поток выполнения и попросить процессор выполнить обработчик события.

Пример:

  • Пользователь нажимает клавишу.

  • Клавиатура отправляет сигнал (IRQ1) в контроллер прерываний (PIC).

  • PIC уведомляет процессор.

  • Процессор приостанавливает текущее выполнение и вызывает обработчик прерывания клавиатуры.

use crate::datetime::{CURRENT_TIME, TICKS};
use crate::pic::{ChainedPics, PIC_1_OFFSET, PIC_2_OFFSET};
use core::sync::atomic::Ordering;
use x86_64::instructions::port::Port;
use x86_64::structures::idt::{InterruptDescriptorTable, InterruptStackFrame};

static mut IDT: InterruptDescriptorTable = InterruptDescriptorTable::new();
static PICS: spin::Mutex<ChainedPics> =
    spin::Mutex::new(unsafe { ChainedPics::new(PIC_1_OFFSET, PIC_2_OFFSET) });

#[derive(Debug, Clone, Copy)]
#[repr(u8)]
pub enum InterruptIndex {
    Timer = PIC_1_OFFSET,
    Keyboard = PIC_1_OFFSET + 1,
}

impl InterruptIndex {
    fn as_u8(self) -> u8 {
        self as u8
    }

    fn as_usize(self) -> usize {
        usize::from(self.as_u8())
    }
}

extern "x86-interrupt" fn pit_interrupt_handler(_stack_frame: InterruptStackFrame) {
    TICKS.fetch_add(1, Ordering::Relaxed);

    if TICKS.load(Ordering::Relaxed) % 1000 == 0 {
        let mut time = CURRENT_TIME.lock();
        time.update();
    }

    unsafe {
        PICS.lock()
            .notify_end_of_interrupt(InterruptIndex::Timer.as_u8());
    }
}

extern "x86-interrupt" fn keyboard_interrupt_handler(_stack_frame: InterruptStackFrame) {
    unsafe {
        let mut port = Port::new(0x60);
        let _scancode: u8 = port.read();

        // Здесь можно добавить обработку кода клавиши

        PICS.lock()
            .notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8());
    }
}

pub fn init_idt() {
    unsafe {
        IDT[InterruptIndex::Timer.as_usize()].set_handler_fn(pit_interrupt_handler);
        IDT[InterruptIndex::Keyboard.as_usize()].set_handler_fn(keyboard_interrupt_handler);
        let idt = &raw mut IDT;
        idt.as_ref().expect("IDT is None").load();
        PICS.lock().initialize();
    }
}

pub fn enable_interrupts() {
    x86_64::instructions::interrupts::enable();
}
use x86_64::instructions::port::Port;

pub const PIC_1_OFFSET: u8 = 32;
pub const PIC_2_OFFSET: u8 = PIC_1_OFFSET + 8;

pub struct ChainedPics {
    master_command: Port<u8>,
    master_data: Port<u8>,
    slave_command: Port<u8>,
    slave_data: Port<u8>,
}

impl ChainedPics {
    pub const unsafe fn new(_offset1: u8, _offset2: u8) -> Self {
        ChainedPics {
            master_command: Port::new(0x20),
            master_data: Port::new(0x21),
            slave_command: Port::new(0xA0),
            slave_data: Port::new(0xA1),
        }
    }

    pub unsafe fn initialize(&mut self) {
        let icw1 = 0x11; // Начальная команда
        let icw4 = 0x01; // 8086/88 (MCS-80/85) mode

        self.master_command.write(icw1);
        self.slave_command.write(icw1);

        self.master_data.write(PIC_1_OFFSET);
        self.slave_data.write(PIC_2_OFFSET);

        self.master_data.write(4); // Указывает на подключение Slave PIC на IRQ2
        self.slave_data.write(2); // Указывает на линию Slave PIC

        self.master_data.write(icw4);
        self.slave_data.write(icw4);
    }

    pub unsafe fn notify_end_of_interrupt(&mut self, irq: u8) {
        if irq >= 8 {
            self.slave_command.write(0x20);
        }
        self.master_command.write(0x20);
    }
}

На Timer пока не обращайте внимание. Мы поговорим о нём позже в блоке даты и времени.

Данный код реализовывает прерывание через IDT.

Скрытый текст

IDT (Interrupt Descriptor Table) — это таблица дескрипторов прерываний в архитектуре x86. Она сообщает процессору, какую функцию нужно вызвать при возникновении определённого прерывания или исключения.

В данной таблице мы добавили таймер и нажатие клавиатуры.
Контролируем прерывания мы через PIC.
В данном случае с двумя PIC:

Master PIC

0x20–0x21

Slave PIC

0xA0–0xA1

Далее мы задаём адреса для наших PIC_1_OFFSET = 32, PIC_2_OFFSET = 40 и сообщаешь PIC, какому IRQ какому вектору IDT соответствует.

Настраиваем master ↔ slave связь.

self.master_data.write(4); // Slave на IRQ2
self.slave_data.write(2);  // Подтверждаем на slave

Принцип работы данного прирывания

Когда пользователь нажимает клавишу:

  • Клавиатура вызывает IRQ1 (вектор 33 = 32 + 1)

  • CPU вызывает keyboard_interrupt_handler

extern "x86-interrupt" fn keyboard_interrupt_handler(_stack_frame: InterruptStackFrame) {
    let mut port = Port::new(0x60);
    let _scancode: u8 = port.read(); // Считываем код клавиши
    ...
}
  • Порт 0x60 — это входной порт клавиатуры, откуда мы считываем scancode (код клавиши).

  • После обработки ты вызываешь PICS.lock().notify_end_of_interrupt(...) — это обязательно, чтобы сообщить PIC: "я обработал прерывание, можешь отправлять следующее".

Читаем scancode клавиатуры

Для чтения кодов реализовываем функцию get_key()

#![feature(abi_x86_interrupt)]

fn get_key() -> Option<u8> {
    let mut port = Port::new(0x60);
    let scancode: u8 = unsafe { port.read() };

    static mut LAST_SCANCODE: u8 = 0;
    unsafe {
        if scancode == 0x0E {
            delay(200000);
            Some(scancode)
        } else if scancode != LAST_SCANCODE {
            LAST_SCANCODE = scancode;
            Some(scancode)
        } else {
            None
        }
    }
}

В данном коде мы получаем коды с адреса 0x60 (клавиатура) и обрабатываем.

loop {
    if let Some(key) = get_key() {
        print_key(key, screen_width, screen_height);
    }
}

Таким образом мы реализовали получение и обработку нажатых клавишь на клавиатуре.


Реалицашия терминала

Полноценный терминал это неотъемлемая часть ядра и любой операционный системы.
В этом блоке мы рассмотрим как создать полноценный ввод-вывод терминала.

Для терминала используем матрицу 80x25 (разрешение VGA).

use constants::{COLS, ROWS};

static mut BUFFER: [[u8; COLS]; ROWS] = [[0; COLS]; ROWS];

Мы будем заполнять его вводом обновлять после наждого ввода экран.
Так же создадим значёк ввода $: и сдвиг курсора на +1 вправо после ввода символа и +1 вниз и вернём коретку в начало.
Так же добавим скрол, чтобы дайдя до конца терминала, он поднимался вверх и создавал новую сточку.

pub const ROWS: usize = 25;
pub const COLS: usize = 80;
pub const MSG: &[u8; 3] = b"$: ";

pub static mut CURRENT_ROW: usize = 0;
pub static mut CURRENT_COL: usize = 0;
use constants::{
    COLOR_INFO, COLS, CURRENT_COL, CURRENT_ROW, MSG, ROWS,
};

static mut BUFFER: [[u8; COLS]; ROWS] = [[0; COLS]; ROWS];
static mut CURSOR_POSITION_ROW: usize = 0;
static mut CURSOR_POSITION_COL: usize = 0;
static mut INPUT_BUFFER: String = String::new();

unsafe {
  loop {
    scroll_status();
    if let Some(key) = get_key() {
        print_key(key, screen_width, screen_height);
    }
}

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

fn delay(a: u32) {
    for _ in 0..a {
        unsafe { core::ptr::read_volatile(&0) };
    }
}

fn get_key() -> Option<u8> {
    let mut port = Port::new(0x60);
    let scancode: u8 = unsafe { port.read() };

    static mut LAST_SCANCODE: u8 = 0;
    unsafe {
        if scancode == 0x0E {
            delay(200000);
            Some(scancode)
        } else if scancode != LAST_SCANCODE {
            LAST_SCANCODE = scancode;
            Some(scancode)
        } else {
            None
        }
    }
}

fn print_key(key: u8, width: u16, height: u16) {
    unsafe {
        if key == 0x0E {
            // Обработка Backspace
            if CURRENT_COL > MSG.len() {
                BUFFER[CURRENT_ROW][CURRENT_COL] = 0;
                CURRENT_COL -= 1;
                BUFFER[CURRENT_ROW][CURRENT_COL] = 0;
                INPUT_BUFFER.pop();
            }
        } else if let Some(character) = SCANCODE_MAP[key as usize] {
            if character == '\n' {
                // Выполнение команды и отображение текущей строки
                let stat: bool = commands::command_fn(&raw mut BUFFER, CURRENT_ROW, &INPUT_BUFFER);
                if !stat {
                    CURRENT_ROW += 2;
                }

                // Очистка буфера после выполнения команды
                INPUT_BUFFER.clear();

                CURRENT_COL = 0;
                if CURRENT_ROW >= 24 {
                    scroll();
                    CURRENT_ROW -= 1;
                    CURRENT_COL = 0;
                }
                // Печать приглашения
                CURRENT_COL = print_prompt(CURRENT_ROW, CURRENT_COL);
            } else {
                if CURRENT_COL < COLS {
                    if CURRENT_COL > 78 {
                        CURRENT_COL = 0;
                        CURRENT_ROW += 1;
                    }
                    BUFFER[CURRENT_ROW][CURRENT_COL] = character as u8;
                    INPUT_BUFFER.push(character);
                    CURRENT_COL += 1;
                }
            }
        }

        // Обновление текущей позиции курсора
        CURSOR_POSITION_ROW = CURRENT_ROW;
        CURSOR_POSITION_COL = CURRENT_COL;

        // Очищаем экран
        vga::clear_screen(width, height);

        // Печать буфера на экране
        vga::print_buffer(&raw mut BUFFER);

        // Отображение курсора на текущей позиции
        let cursor_row = CURSOR_POSITION_ROW;
        let cursor_col = CURSOR_POSITION_COL;
        let vga_buffer = 0xb8000 as *mut u8;
        *vga_buffer.offset((cursor_row as isize * width as isize + cursor_col as isize) * 2) = b'_';
        *vga_buffer.offset((cursor_row as isize * width as isize + cursor_col as isize) * 2 + 1) =
            0x07;
    }
}

fn scroll() {
    unsafe {
        for i in 0..24 {
            BUFFER[i] = BUFFER[i + 1];
        }
        BUFFER[24] = [0; COLS];
    }
}

fn scroll_status() {
    unsafe {
        if CURRENT_ROW == 24 {
            scroll();
            CURRENT_ROW -= 1;
            CURRENT_COL = 0;
            CURRENT_COL = print_prompt(CURRENT_ROW, CURRENT_COL);
        }
    }
}

На данный момент код руботать не будет так как в коде используется String который требует кучу для работы.
Кучу и аллокацию мы пройдём позже.
Так что после части по созданию кучи вернитесь сюда и проверьте всё ещё раз.

Таким образом у нас есть полноценный терминал, который получает в сетя текст с клавиатуры и выводит его на экран.

Но теперь было бы хорошо не просто вводить, но и получать нужный ответ.
Для этого добавим обработчик команд.


Команды

Для того чтобы создать обработчик команд, мы долным получать вводимый с клавиатуры текст, записывать его в переменную и парсить его и выдовать действия (например, вывод в терминал).

Так как у нас уже есть получение команд с клавиатуры и запись их в строку (После части: Куча и аллокация.) мы можем обрабабатывать текст в этой строке и выдовать ответ на этот текст (команду).

Создадим обработчик команд.

use crate::constants::{COLS, CURRENT_COL, CURRENT_ROW, ROWS};
use crate::datetime::{get_date, get_time, set_date, set_time};
use crate::vga::{clear_screen, write_char};
use core::arch::asm;

use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;

struct Command<'a> {
    name: &'a str,
    action: fn(*mut [[u8; COLS]; ROWS], usize) -> bool,
}

impl<'a> Command<'a> {
    fn new(name: &'a str, action: fn(*mut [[u8; COLS]; ROWS], usize) -> bool) -> Self {
        Command { name, action }
    }
}

fn hello_action(buffer: *mut [[u8; COLS]; ROWS], row: usize) -> bool {
    unsafe {
        let msg = b"HELLO!";
        for (i, &byte) in msg.iter().enumerate() {
            write_char(row + 1, i, byte, 0x07); // Печатает на строке row + 1
            (*buffer)[row + 1][i] = byte; // Записываем в буфер
        }
        false
    }
}

fn time_action(buffer: *mut [[u8; COLS]; ROWS], row: usize) -> bool {
    unsafe {
        let time = get_time();
        let time_str = format!("{:02}:{:02}:{:02}", time.0, time.1, time.2);

        for (i, byte) in time_str.bytes().enumerate() {
            write_char(row + 1, i, byte, 0x07); // Печатает на строке row + 1
            (*buffer)[row + 1][i] = byte; // Записываем в буфер
        }
        false
    }
}

fn date_action(buffer: *mut [[u8; COLS]; ROWS], row: usize) -> bool {
    unsafe {
        let date = get_date();
        let date_str = format!("{:02}.{:02}.{:04}", date.0, date.1, date.2);

        for (i, byte) in date_str.bytes().enumerate() {
            write_char(row + 1, i, byte, 0x07); // Печатает на строке row + 1
            (*buffer)[row + 1][i] = byte; // Записываем в буфер
        }
        false
    }
}

fn date_set_action(buffer: *mut [[u8; COLS]; ROWS], row: usize) -> bool {
    unsafe {
        let command: &[u8] = &(*buffer)[row][12..22]; // Извлекаем аргументы после `time_add`

        let command_str = core::str::from_utf8(command).unwrap_or("").trim();

        let mut parts = command_str.split('.');

        if let (Some(d), Some(m), Some(y)) = (parts.next(), parts.next(), parts.next()) {
            if let (Ok(day), Ok(month), Ok(year)) =
                (d.parse::<u8>(), m.parse::<u8>(), y.parse::<u16>())
            {
                set_date(day, month, year);
                let msg = b"Date set!";
                for (i, &byte) in msg.iter().enumerate() {
                    write_char(row + 1, i, byte, 0x07); // Печатает на строке row + 1
                    (*buffer)[row + 1][i] = byte; // Записываем в буфер
                }
                return false;
            }
        }

        let msg = b"Invalid date format!";
        for (i, &byte) in msg.iter().enumerate() {
            write_char(row + 1, i, byte, 0x07); // Печатает на строке row + 1
            (*buffer)[row + 1][i] = byte; // Записываем в буфер
        }
        false
    }
}

fn time_set_action(buffer: *mut [[u8; COLS]; ROWS], row: usize) -> bool {
    unsafe {
        let command: &[u8] = &(*buffer)[row][12..20]; // Извлекаем аргументы после `time_add`

        let command_str = core::str::from_utf8(command).unwrap_or("").trim();

        let mut parts = command_str.split(':');

        if let (Some(h), Some(m), Some(s)) = (parts.next(), parts.next(), parts.next()) {
            if let (Ok(hours), Ok(minutes), Ok(seconds)) =
                (h.parse::<u8>(), m.parse::<u8>(), s.parse::<u8>())
            {
                set_time(hours, minutes, seconds);
                let msg = b"Time set!";
                for (i, &byte) in msg.iter().enumerate() {
                    write_char(row + 1, i, byte, 0x07); // Печатает на строке row + 1
                    (*buffer)[row + 1][i] = byte; // Записываем в буфер
                }
                return false;
            }
        }

        let msg = b"Invalid time format!";
        for (i, &byte) in msg.iter().enumerate() {
            write_char(row + 1, i, byte, 0x07); // Печатает на строке row + 1
            (*buffer)[row + 1][i] = byte; // Записываем в буфер
        }
        false
    }
}

fn error_action(buffer: *mut [[u8; COLS]; ROWS], row: usize) -> bool {
    unsafe {
        let msg = b"Error: command";
        for (i, &byte) in msg.iter().enumerate() {
            write_char(row + 1, i, byte, 0x07); // Печатает на строке row + 1
            (*buffer)[row + 1][i] = byte; // Записываем в буфер
        }
        false
    }
}

fn reboot_action(buffer: *mut [[u8; COLS]; ROWS], row: usize) -> bool {
    unsafe {
        let msg = b"Rebooting...";
        for (i, &byte) in msg.iter().enumerate() {
            write_char(row + 1, i, byte, 0x07); // Печатает на строке row + 1
            (*buffer)[row + 1][i] = byte; // Записываем в буфер
        }

        asm!(
            "cli",            // Отключаем прерывания
            "out 0x64, al",   // Отправляем команду на контроллер клавиатуры
            "2: hlt",         // Метка 2: останавливаем процессор
            "jmp 2b",         // Переход к метке 2, чтобы создать бесконечный цикл
            in("al") 0xFEu8   // Значение 0xFE для команды перезагрузки
        );
        false
    }
}

fn shutdown_action(buffer: *mut [[u8; COLS]; ROWS], row: usize) -> bool {
    unsafe {
        let msg = b"Shutting down...";
        for (i, &byte) in msg.iter().enumerate() {
            write_char(row + 1, i, byte, 0x07); // Печатает на строке row + 1
            (*buffer)[row + 1][i] = byte; // Записываем в буфер
        }

        asm!(
            "cli",            // Отключаем прерывания
            "mov ax, 0x5301", // Подключаемся к APM API
            "xor bx, bx",
            "int 0x15",
            "mov ax, 0x530E", // Устанавливаем версию APM на 1.2
            "xor bx, bx",
            "mov cx, 0x0102",
            "int 0x15",
            "mov ax, 0x5307", // Выключаем систему
            "mov bx, 0x0001",
            "mov cx, 0x0003",
            "int 0x15",
            "hlt", // Останавливаем процессор
            options(noreturn, nostack)
        );
    }
}

fn clear(buffer: *mut [[u8; COLS]; ROWS], _: usize) -> bool {
    let screen_width = 80;
    let screen_height = 25;
    clear_screen(screen_width, screen_height);

    unsafe {
        for row in (*buffer).iter_mut() {
            for cell in row.iter_mut() {
                *cell = 0;
            }
        }
        CURRENT_COL = 0;
        CURRENT_ROW = 0;
    }

    true // Возвращаем true
}

pub fn command_fn(buffer: *mut [[u8; COLS]; ROWS], row: usize, command: &String) -> bool {
    let (cmd, _) = match command.find(' ') {
        Some(pos) => command.split_at(pos),
        None => (command.as_str(), ""),
    };

    let comm = cmd.trim();

    // Фильтруем только непустые и ненулевые байты
    let mut comm_filtered: Vec<u8> = Vec::new();
    for &byte in comm.as_bytes().iter() {
        if byte != 0 && !byte.is_ascii_whitespace() {
            comm_filtered.push(byte);
        }
    }

    let commands: [Command; 9] = [
        Command::new("hello", hello_action),
        Command::new("time", time_action),
        Command::new("time_set", time_set_action),
        Command::new("date", date_action),
        Command::new("date_set", date_set_action),
        Command::new("error", error_action),
        Command::new("reboot", reboot_action),
        Command::new("shutdown", shutdown_action),
        Command::new("clear", clear),
    ];

    for cmd in commands.iter() {
        let cmd_name_bytes: Vec<u8> = cmd.name.bytes().collect();

        if comm_filtered == cmd_name_bytes {
            let result = (cmd.action)(buffer, row);
            if result {
                return true;
            }
            return false; // Завершите цикл, если команда найдена, но не вернула true
        }
    }

    error_action(buffer, row);
    false // Возвращаем false, если команда не найдена
}

В данном коде, мы получаем строку и парсим из неё данные для понимания, что это за команда и что с ней нужно делать.

Таким образом можно добавлять новые системные команды.

Время и дата

Теперь доббавим поддержку даты и времени.

Для этого мы будем использовать прерывание как и для ввода текста.

use crate::datetime::{CURRENT_TIME, TICKS};
use crate::pic::{ChainedPics, PIC_1_OFFSET, PIC_2_OFFSET};
use core::sync::atomic::Ordering;
use x86_64::instructions::port::Port;
use x86_64::structures::idt::{InterruptDescriptorTable, InterruptStackFrame};

static mut IDT: InterruptDescriptorTable = InterruptDescriptorTable::new();
static PICS: spin::Mutex<ChainedPics> =
    spin::Mutex::new(unsafe { ChainedPics::new(PIC_1_OFFSET, PIC_2_OFFSET) });

#[derive(Debug, Clone, Copy)]
#[repr(u8)]
pub enum InterruptIndex {
    Timer = PIC_1_OFFSET,
    Keyboard = PIC_1_OFFSET + 1,
}

impl InterruptIndex {
    fn as_u8(self) -> u8 {
        self as u8
    }

    fn as_usize(self) -> usize {
        usize::from(self.as_u8())
    }
}

extern "x86-interrupt" fn pit_interrupt_handler(_stack_frame: InterruptStackFrame) {
    TICKS.fetch_add(1, Ordering::Relaxed);

    if TICKS.load(Ordering::Relaxed) % 1000 == 0 {
        let mut time = CURRENT_TIME.lock();
        time.update();
    }

    unsafe {
        PICS.lock()
            .notify_end_of_interrupt(InterruptIndex::Timer.as_u8());
    }
}

extern "x86-interrupt" fn keyboard_interrupt_handler(_stack_frame: InterruptStackFrame) {
    unsafe {
        let mut port = Port::new(0x60);
        let _scancode: u8 = port.read();

        // Здесь можно добавить обработку кода клавиши

        PICS.lock()
            .notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8());
    }
}

pub fn init_idt() {
    unsafe {
        IDT[InterruptIndex::Timer.as_usize()].set_handler_fn(pit_interrupt_handler);
        IDT[InterruptIndex::Keyboard.as_usize()].set_handler_fn(keyboard_interrupt_handler);
        let idt = &raw mut IDT;
        idt.as_ref().expect("IDT is None").load();
        PICS.lock().initialize();
    }
}

pub fn enable_interrupts() {
    x86_64::instructions::interrupts::enable();
}
use core::sync::atomic::AtomicUsize;
use spin::Mutex;

#[derive(Debug, Clone, Copy)]
pub struct DateTime {
    pub day: u8,
    pub month: u8,
    pub year: u16,
    pub hours: u8,
    pub minutes: u8,
    pub seconds: u8,
}

pub static TICKS: AtomicUsize = AtomicUsize::new(0);
pub static CURRENT_TIME: Mutex<DateTime> = Mutex::new(DateTime {
    day: 1,
    month: 1,
    year: 2023,
    hours: 12,
    minutes: 0,
    seconds: 0,
});

impl DateTime {
    pub fn update(&mut self) {
        self.seconds += 1;
        if self.seconds >= 60 {
            self.seconds = 0;
            self.minutes += 1;
            if self.minutes >= 60 {
                self.minutes = 0;
                self.hours += 1;
                if self.hours >= 24 {
                    self.hours = 0;
                    self.day += 1;
                    if self.day > days_in_month(self.month, self.year) {
                        self.day = 1;
                        self.month += 1;
                        if self.month > 12 {
                            self.month = 1;
                            self.year += 1;
                        }
                    }
                }
            }
        }
    }
}

fn days_in_month(month: u8, year: u16) -> u8 {
    match month {
        1 => 31,
        2 => {
            if is_leap_year(year) {
                29
            } else {
                28
            }
        }
        3 => 31,
        4 => 30,
        5 => 31,
        6 => 30,
        7 => 31,
        8 => 31,
        9 => 30,
        10 => 31,
        11 => 30,
        12 => 31,
        _ => 30,
    }
}

fn is_leap_year(year: u16) -> bool {
    (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}

pub fn get_time() -> (u8, u8, u8) {
    let time = CURRENT_TIME.lock();
    (time.hours, time.minutes, time.seconds)
}

pub fn get_date() -> (u8, u8, u16) {
    let time = CURRENT_TIME.lock();
    (time.day, time.month, time.year)
}

pub fn set_time(hours: u8, minutes: u8, seconds: u8) {
    let mut time = CURRENT_TIME.lock();
    time.hours = hours;
    time.minutes = minutes;
    time.seconds = seconds;
}

pub fn set_date(day: u8, month: u8, year: u16) {
    let mut time = CURRENT_TIME.lock();
    time.day = day;
    time.month = month;
    time.year = year;
}
use x86_64::instructions::port::Port;

pub fn init_pit() {
    let frequency: u16 = 1193; // Частота таймера ~1мс (1193182 / 1000)

    unsafe {
        let mut command_port = Port::new(0x43);
        command_port.write(0x34 as u8); // Управление PIT: канал 0, режим 2

        let mut data_port = Port::new(0x40);
        data_port.write((frequency & 0xFF) as u8); // Младший байт
        data_port.write((frequency >> 8) as u8); // Старший байт
    }
}

В этом коде мы вызываем прерывание времени каждые 1000 тактов и обновляем время.

Активируем таймер:

pub extern "C" fn _start() -> ! {
  init_idt();
  init_pit();
  enable_interrupts();
  
  unsafe {
    loop {
      scroll_status();
      date_status();
      time_status();
      if let Some(key) = get_key() {
          print_key(key, screen_width, screen_height);
      }
    }
  }
}

Чтобы получить время мы реализуем две функции.
Одна для получения времени (hh:mm:ss), другая для получения даты (dd:mm:yyyy).

fn time_status() {
    let time = get_time();
    let time_str = format!("{:02}:{:02}", time.0, time.1);

    for (i, byte) in time_str.bytes().enumerate() {
        write_char(24, i + 74, byte, COLOR_INFO); // Печатает на строке row + 1
    }
}

fn date_status() {
    let date = get_date();
    let date_str = format!("{:02}.{:02}.{:04}", date.0, date.1, date.2);

    for (i, byte) in date_str.bytes().enumerate() {
        write_char(24, i + 62, byte, COLOR_INFO); // Печатает на строке row + 1
    }
}

Таким образом мы реализовали время, через прерывания процессора и получение данных через функции.

В данный момент вывод в функциях работать не будет (format!) , так как для его работы нужна куча и аллокатор.


Куча и аллокация

Так как мы не можем расчитывать всё в голове сколько выделить той или иной переменной определённое количество доступных байт в массиве, так как оно может быть не достаточно для некоторых пользователей и это не всегда удобно.

И для этого нам нужна куча.

Скрытый текст

Куча (heap) — это область памяти, предназначенная для динамического (вручную управляемого) выделения памяти во время выполнения программы.

Куча выглядит примерно так:

Heap (память):
[____] [####] [____] [####]
 128     64    256     128   ← байты

_ = свободно  
# = занято

То это выделенная свободная память в ОЗУ.
Обычно в ней достаточно большой размер зарезервированной памяти, чтобы хватило под все данные.

Как работает куча?

Когда нам нудно поместить новое данное пример векторный массив Vec! и его данные, то выделяется нужное место и перемещает туда данные и возвращает ссылку на начало ячейки памяти обратно переменной, чтобы мы могли работать в с Vec.

И при добавленнии новых данных в этот массив мы увеличиваем её размер и добавляем в неё новые данные.

Что будет если при выделении места перед ячейкой уже начало новой памяти?

Для этого мы будем использовать аллокатор.

Скрытый текст

Аллокатор (от англ. allocator) — это механизм, который управляет динамической памятью: он выделяет и освобождает участки памяти из области, называемой кучей (heap).

В данной ситуации когда у нас память выглядит примерно так:

+----------------+----------------+---------------------------------+
|     Занято I   |    Занято II   |           Свободно              |
|    (128 байт)  |   (64 байта)   |          (512 байт)             |
+----------------+----------------+---------------------------------+

Так как мы не можем продолжить выделять память дальше, так как залезим на данные кучи II.
Мы используем аллокацию памяти.

Как это?

Аллокатор пройдётся по всей памяти и найдёт новое пространство для памяти, которое больше текущего и в которое влезит + ещё новая инормация, когда он её находит, то он выделяет новую ячейку нужного размера и копирует туда старые данные, освобождая при этом старую яцейку памяти, чистит память и дописывает новые данные в конец новой ячейки памяти.
При этом передаёт переменной новый адрес начала ячейки памяти.

Таким образом после работы аллокатора у нас будет новая картина:

+----------------+----------------+------------------+---------------+
|     Свободно   |    Занято II   |     Занято I     |   Свободно    |
|    (128 байт)  |   (64 байта)   |    (256 байт)    |  (256 байт)   |
+----------------+----------------+------------------+-------------- +

Без кучи мы не можем использовать такие типы данных как: String, Vec и другие. Которые поддерживают в своей основе бесконечное выделение памяти под данные, под добавление новвых данных.

Так, в предыдущей части мы уже использовали String, но он не заработает без кучи.

Создадим кучу.

Для её создания используем готовые библиотеки:

use core::mem::MaybeUninit;
use linked_list_allocator::LockedHeap;

Выделим кучу в пямяти размеров 1MB.

pub const HEAP_SIZE: usize = 1024 * 1024; // 1 MiB
pub const PARTITION_OFFSET: usize = 1048576; // 1 MiБ
#![feature(global_allocator)]
#![feature(alloc_error_handler)]

extern crate alloc;

#[global_allocator]
static ALLOCATOR: LockedHeap = LockedHeap::empty();

fn init_heap() {
    static mut HEAP_MEMORY: MaybeUninit<[u8; HEAP_SIZE]> = MaybeUninit::uninit();

    unsafe {
        let heap_start = HEAP_MEMORY.as_mut_ptr() as *mut u8;
        ALLOCATOR.lock().init(heap_start, HEAP_SIZE);
    }
}

#[alloc_error_handler]
fn alloc_error_handler(_layout: core::alloc::Layout) -> ! {
    loop {}
}

Теперь у нас есть полноценная куча размером в 1MB (Можно увеличить) в которую мы теперь можем помещать данные.
То есть теперь у нас работают все типы даннх для которых нужна куча:

  • String

  • Vec

  • Box

  • и тд.

Теперь код выше будет работать, так как теперь у него есть всё для запуска.

Вывод

В этой статье мы не затронули такие важные аспекты, как управление памятью, файловую систему, планировщик задач, драйверы и многие другие элементы полноценного ядра. Тем не менее, мы прошли через основы — с нуля реализовали терминал, обработку команд, таймер, отображение времени и простую реализацию кучи.

Это уже достаточно, чтобы понять, как устроено ядро ОС на низком уровне, и с чего начинается путь системного программирования на Rust.

? Полный исходный код проекта, а также пошаговые инструкции по сборке и запуску доступны здесь:
? https://github.com/Elieren/NeonForge

? Спасибо, что дочитали до конца! Если статья оказалась полезной, интересной или вдохновляющей — значит, я всё сделал не зря. До новых встреч на пути системного программирования и Rust-магии! ?✨

Комментарии (3)


  1. ivanuzzo
    21.06.2025 11:30

    Хорошее изложение ! Статья очень хорошо ложится после вот этой и некоторой базы про Rust embedded. Просто идеально заполняет пробелы. Благодарю вас за труд.


    1. Elieren Автор
      21.06.2025 11:30

      Спасибо большое.
      Я рад, что статья была для вас интересна.


  1. lesha108
    21.06.2025 11:30

    Здорово, но очень много опечаток в тексте (