В этой статье мы продолжим наш путь создания простого, но функционального ядра операционной системы на языке C.

Поговорим с вами о том как:

  • Запуск ядра в Long mode

  • Работа с Multiboot таблицами

  • Реализация графического режима

  • Реализация драйвера ATA (IDE)

  • Реализация драйвера PCI

  • Реализация переключателя Kernel mode / User mode


Запуск ядра в Long mode

В предыдущей статье мы рассматривали запуск ядра в Protected Mode, где работа велась с 32-битными регистрами семейства EXX (EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP, EIP, EFLAGS).

После перехода в Long Mode активируется поддержка нового набора 64-битных регистров семейства RXX (RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP, R8-R15, RIP, RFLAGS). Это позволяет оперировать данными типа uint64_t, увеличивая объём обрабатываемой информации и расширяя адресное пространство.

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

; kernel.asm
[BITS 32]

start:
    cli

    ; --- Включаем PAE (CR4.PAE = 1) ---
    mov eax, cr4
    bts eax, 5
    mov cr4, eax

    ; --- Устанавливаем CR3 = адрес pml4_table ---
    mov eax, pml4_table
    mov cr3, eax

    ; --- Включаем LME через MSR IA32_EFER (0xC0000080) ---
    mov ecx, 0xC0000080
    rdmsr
    bts eax, 8
    wrmsr

    ; --- Включаем paging (CR0.PG = 1) ---
    mov eax, cr0
    bts eax, 31
    mov cr0, eax

    ; --- Far jump в 64-bit код ---
    jmp 0x08:long_mode_entry

; -----------------------------------------------------------------------
; 64-bit entry
; -----------------------------------------------------------------------
[BITS 64]
long_mode_entry:
    mov ax, 0x10
    mov ds, ax
    mov es, ax
    mov ss, ax

    ; настроим стек
    lea rsp, [rel stack64_top]
    and rsp, -16

    ; вызов 64-битного kmain (собранного с -m64)
    mov rdi, rbx
    call kmain

.hang64:
    hlt
    jmp .hang64

; -----------------------------------------------------------------------
; место под стек 64-bit
; -----------------------------------------------------------------------
section .bss
align 16
stack64_bottom:
    resb 65536           ; резервируем 64 KiB под стек
stack64_top:

; -----------------------------------------------------------------------
; Конец
; -----------------------------------------------------------------------
section .note.GNU-stack
; empty

Данная настройка активирует в ядре поддержку 64-битного режима.

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

P.S.

В предыдущей статье в файле link.ld мы использовали отдельную кучу для пользовательских приложений.

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

OUTPUT_FORMAT(elf64-x86-64)
ENTRY(start)

PHDRS
{
  text PT_LOAD FLAGS(5); /* PF_R | PF_X */
  data PT_LOAD FLAGS(6); /* PF_R | PF_W */
}

SECTIONS
{
  . = 0x00100000;

  .multiboot ALIGN(8) : { KEEP(*(.multiboot)) } :text

  .text : {
    *(.text)
    *(.text*)
    *(.rodata)
    *(.rodata*)
  } :text

  .data : {
    *(.data)
    *(.data*)
  } :data

  .bss : {
    *(.bss)
    *(.bss*)
    *(COMMON)
  } :data

  .heap (NOLOAD) : {
    . = ALIGN(8);
    _heap_start = .;
    . = . + 512 * 1024 * 1024;
    _heap_end = .;
  } :data
}

P.P.S.

После перехода в Long Mode и использования новых регистров примеры кода обработки прерываний из прошлой статьи в новом ядре работать не будут.

Смотрите новый код здесь.


Работа с Multiboot таблицами

Поскольку ядро для своей загрузки использует загрузчик GRUB, который выполняет первоначальную настройку системы и запускает ядро, мы можем использовать Multiboot-таблицы.

С их помощью можно как настраивать GRUB, так и получать от него полезную информацию о системе. Полную спецификацию структуры Multiboot-таблиц можно найти в официальной документации.

В текущей реализации мы будем использовать Multiboot-таблицы для двух целей:

  1. Обеспечения поддержки запуска ядра в режиме UEFI (через Multiboot v1).

  2. Реализации графического видеорежима (с помощью Multiboot v2).

Для этого, перед запуском ядра, на этапе начальной загрузки, необходимо в секции .text прописать ключевые настройки, которые сообщат GRUB о наших требованиях.

; kernel.asm
[BITS 32]

section .text
    align 8

    ; --------------------
    ; Multiboot v1 (минимальный)
    ; --------------------
    align 4
mb1_start:
    dd 0x1BADB002              ; magic
    dd 0x00000000              ; flags = 0 (минимальный)
    dd -(0x1BADB002 + 0x00000000) ; checksum

    ; --------------------
    ; Multiboot2 (минимальный)
    ; --------------------
    align 8
mb2_start:
    dd 0xE85250D6              ; magic (multiboot2)
    dd 0x00000000              ; architecture (0 = i386)
    dd mb2_end - mb2_start     ; header length (в байтах)
    ; checksum = -(magic + arch + length)  (выражение ниже даёт 32-bit)
    dd 0x100000000 - (0xE85250D6 + 0x00000000 + (mb2_end - mb2_start))

    ; --- Framebuffer (header) tag: request preferred mode ---
    align 8
    dw 5        ; tag type = 5 (framebuffer request in header)
    dw 0        ; flags (0 = required; if you want optional set bit0)
    dd 20       ; size of this tag (header + payload: 2+2+4 + 3*4 = 20)
    dd 1920     ; width (px)  <-- поменяйте на нужную
    dd 1080      ; height (px) <-- поменяйте на нужную
    dd 32       ; bpp (bits per pixel) <-- 32 обычно

    ; --- End tag ---
    align 8
    dw 0
    dw 0
    dd 8

mb2_end:

Данная настройка обеспечивает загрузку ядра через UEFI (если система его поддерживает) и инициализацию framebuffer'а с заданным разрешением. GRUB также передаст в регистре EBX указатель на структуру Multiboot.

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


Реализация графического режима

После настройки таблиц и получения в регистре EBX указателя на структуру, можно приступать к реализации графического режима.

Первым шагом является передача этого указателя в код на C. Для этого необходимо переместить значение из регистра RBX в регистр RAX.

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

Почему RBX, если был EBX?

Когда CPU находится в 64-битном режиме (Long Mode), любой доступ к 32-битному регистру EAX/EBX/ECX/… автоматически выполняет zero-extend в соответствующий 64-битный регистр RAX/RBX/RCX/...

mov ebx, value

в Long mode запишет низшие 32 бита RBX, а старшие 32 бита RBX будут обнулены.

; kernel.asm
; вызов 64-битного kmain (собранного с -m64)
    mov rdi, rbx
    call kmain
// kernel.c
/*-------------------------------------------------------------
    Основная функция ядра
-------------------------------------------------------------*/
void kmain(uint64_t mb2_addr)
{
}

После получения адреса структуры Multiboot 2 в переменной mb2_addr мы можем приступить к её обработке. Для этого необходимо:

  1. Описать в коде структуры данных в соответствии со спецификацией Multiboot 2.

  2. Реализовать парсер для обхода и извлечения информации из тегов, расположенных по этому адресу.

// graphics/mb2/mb2.c
/* Глобальная структура с информацией о фреймбуфере */
static framebuffer_info_t fb_info;

/* Вспомогательная функция - выравнивает значение вверх до 8 байт */
static inline size_t align_up8(size_t x)
{
    return (x + (MB2_TAG_ALIGN - 1)) & ~(MB2_TAG_ALIGN - 1);
}

/* Безопасное чтение 32-битного значения из памяти
   (используется memcpy, чтобы избежать проблем с невыравненными адресами) */
static inline uint32_t read_u32(const void *p)
{
    uint32_t v;
    memcpy(&v, p, sizeof(v));
    return v;
}

/* Безопасное чтение 64-битного значения */
static inline uint64_t read_u64(const void *p)
{
    uint64_t v;
    memcpy(&v, p, sizeof(v));
    return v;
}

static void process_framebuffer_tag(uint8_t *payload, size_t payload_len)
{
    /* Современный формат: 64-битный адрес + pitch + width + height + bpp + тип */
    if (payload_len >= MB2_FB_PAYLOAD_MODERN)
    {
        uint64_t addr64 = read_u64(payload + 0);
        uint32_t pitch = read_u32(payload + 8);
        uint32_t width = read_u32(payload + 12);
        uint32_t height = read_u32(payload + 16);

        uint8_t bpp = 0;
        uint8_t fbtype = 0;
        memcpy(&bpp, payload + 20, 1);
        memcpy(&fbtype, payload + 21, 1);

        fb_info.addr = addr64;
        fb_info.pitch = pitch;
        fb_info.width = width;
        fb_info.height = height;
        fb_info.bpp = bpp;
        fb_info.fb_type = fbtype;
    }
    /* Старый формат: 32-битный адрес + pitch + width + height */
    else if (payload_len >= MB2_FB_PAYLOAD_LEGACY)
    {
        uint32_t addr32 = read_u32(payload + 0);
        uint32_t pitch = read_u32(payload + 4);
        uint32_t width = read_u32(payload + 8);
        uint32_t height = read_u32(payload + 12);

        fb_info.addr = (uint64_t)addr32;
        fb_info.pitch = pitch;
        fb_info.width = width;
        fb_info.height = height;
        fb_info.bpp = 0;
        fb_info.fb_type = 0;
    }
    else if (payload_len >= MB2_FB_PAYLOAD_MINIMAL)
    {
        uint64_t addr64 = read_u64(payload + 0);
        fb_info.addr = addr64;
        fb_info.pitch = 0;
        fb_info.width = 0;
        fb_info.height = 0;
        fb_info.bpp = 0;
        fb_info.fb_type = 0;
    }
    /* Иначе данных недостаточно - игнорируем */
}

/* Функция вычисляет примерный размер фреймбуфера в байтах.
   Если высота неизвестна - возвращает минимум 2 МиБ. */
uint64_t fb_calc_size(const framebuffer_info_t *fb)
{
    if (!fb)
        return DEFAULT_FB_SIZE;

    /* Если неизвестен pitch (байт на строку) или высота - возвращаем 2 МиБ */
    if (fb->pitch == 0 || fb->height == 0)
        return DEFAULT_FB_SIZE;

    if (fb->height > UINT64_MAX / fb->pitch)
        return DEFAULT_FB_SIZE;

    /* Общий объём памяти, занимаемой изображением */
    uint64_t bytes = (uint64_t)fb->pitch * (uint64_t)fb->height;

    /* Округляем вверх до ближайшего кратного 2 МиБ */
    uint64_t rounded = (bytes + 0x1FFFFF) & ~((uint64_t)0x1FFFFF);
    if (rounded == 0)
        rounded = DEFAULT_FB_SIZE;

    return rounded;
}

/* Основная функция разбора структуры Multiboot2 */
void mb2_parse(uint64_t mb2_addr)
{
    if (mb2_addr == 0)
        return;

    /* Обнуляем структуру с информацией о фреймбуфере */
    memset(&fb_info, 0, sizeof(fb_info));

    /* Указатель на начало Multiboot2-заголовка */
    uint8_t *base = (uint8_t *)(uintptr_t)mb2_addr;

    /* Первые 8 байт: общий размер и резерв */
    uint32_t total_size = read_u32(base + 0);

    /* Проверяем корректность размера */
    if (total_size < MB2_TAG_HDR_SIZE)
        return;

    uint8_t *end = base + total_size;       /* конец всей структуры */
    uint8_t *ptr = base + MB2_TAG_HDR_SIZE; /* первый тег идёт сразу после заголовка */

    /* Проходим по всем тегам */
    while (ptr + MB2_TAG_HDR_SIZE <= end)
    {
        /* Читаем заголовок тега */
        uint32_t tag_type = read_u32(ptr);
        uint32_t tag_size = read_u32(ptr + 4);

        /* Проверяем корректность размера тега */
        if (tag_size < MB2_TAG_HDR_SIZE)
            break;

        /* Вычисляем смещение к следующему тегу, с выравниванием */
        size_t aligned_size = align_up8((size_t)tag_size);
        uint8_t *next = ptr + aligned_size;

        if (next > end || next <= ptr)
            break; /* повреждённая структура или переполнение - выходим */

        /* Полезная нагрузка идёт сразу после 8-байтного заголовка */
        uint8_t *payload = ptr + MB2_TAG_HDR_SIZE;
        size_t payload_len = (size_t)tag_size - MB2_TAG_HDR_SIZE;

        /* Обрабатываем тег по типу */
        switch (tag_type)
        {
        /* Тег конца списка - выходим */
        case MB2_TAG_TYPE_END:
            return;

        /* Тег с информацией о framebuffer */
        case MB2_TAG_TYPE_FRAMEBUFFER:
            process_framebuffer_tag(payload, payload_len);
            break;

        /* Все прочие теги игнорируем */
        default:
            break;
        }

        /* Переходим к следующему тегу */
        ptr = next;
    }
}

/* Возвращает указатель на заполненную структуру framebuffer_info */
framebuffer_info_t *get_framebuffer_info(void)
{
    return &fb_info;
}
Скрытый текст

Это отрывок кода парсера, полный код здесь.

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

Формат работы с буфером:

Пиксели располагаются строка за строкой, сверху вниз:

addr + 0*pitch … первая строка
addr + 1*pitch … вторая строка
addr + y*pitch … строка y

Внутри строки пиксели идут слева направо:

pixel(x) = base + x * (bpp / 8)

Адрес конкретного пикселя вычисляется по формуле:

addr + y*pitch + x*(bpp/8)

Мы получили физический адрес буфера, по которому расположена видеопамять. Однако в операционных системах с включенной виртуальной памятью прямое обращение к физическим адресам невозможно - все программы работают с виртуальными адресами.

Почему запись не отображается на экране?
Если попытаться записать данные по полученному физическому адресу, изменений на экране не произойдет, так как процессор использует механизм виртуальной памяти и интерпретирует все адреса как виртуальные. Для доступа к оборудованию необходимо настроить отображение виртуальных адресов на физические.

Решение: Identity Mapping
В данном случае реализован частный случай отображения памяти - Identity Map (тождественное отображение). Вся физическая память в диапазоне от 0 до 4 ГБ напрямую отображается на идентичные виртуальные адреса.

; kernel.asm
; -----------------------------------------------------------------------
; Identity-map 0..4GiB (2MiB pages)
; -----------------------------------------------------------------------
section .data
align 4096
pml4_table:
    dq pdpt_table + 0x007    ; PML4[0] -> PDPT (flags in low bits)

align 4096
pdpt_table:
    dq pd_table0 + 0x007     ; PDPT[0] -> PD0 (0..1GiB)
    dq pd_table1 + 0x007     ; PDPT[1] -> PD1 (1..2GiB)
    dq pd_table2 + 0x007     ; PDPT[2] -> PD2 (2..3GiB)
    dq pd_table3 + 0x007     ; PDPT[3] -> PD3 (3..4GiB)
    ; остальные записи нулевые (по умолчанию)

; PD0: maps 0x0000_0000 .. 0x3FF_FFFF (1 GiB)
align 4096
pd_table0:
%assign j 0
%rep 512
    dq j * 0x200000 + 0x087
%assign j j + 1
%endrep

; PD1: maps 0x4000_0000 .. 0x7FF_FFFF
align 4096
pd_table1:
%assign j 512
%rep 512
    dq j * 0x200000 + 0x087
%assign j j + 1
%endrep

; PD2: maps 0x8000_0000 .. 0xBFF_FFFF
align 4096
pd_table2:
%assign j 1024
%rep 512
    dq j * 0x200000 + 0x087
%assign j j + 1
%endrep

; PD3: maps 0xC000_0000 .. 0xFFF_FFFF  (сюда попадает 0xFD000000)
align 4096
pd_table3:
%assign j 1536
%rep 512
    dq j * 0x200000 + 0x087
%assign j j + 1
%endrep

После активации данной таблицы страниц (Paging) создается прямое соответствие: каждый физический адрес в диапазоне 0-4 ГБ доступен по идентичному виртуальному адресу. Это означает, что физический адрес видеобуфера 0xFD000000 автоматически становится доступным по тому же виртуальному адресу 0xFD000000.

Итог: Благодаря identity mapping запись данных по виртуальному адресу, равному физическому адресу видеобуфера, теперь напрямую изменяет изображение на экране.


Реализация драйвера ATA (IDE)

ATA/IDE — это протокол, по которому ОС взаимодействует с жёсткими дисками, SSD (старые), CD/DVD-ROM и другими устройствами хранения.

Структура системы

CPU → I/O порты → IDE контроллер → Устройство (HDD/CD-ROM)

Драйвер поддерживает архитектуру из двух каналов: основного (Primary) и дополнительного (Secondary). Каждый из этих каналов, в свою очередь, обеспечивает управление парой устройств - ведущим (Master) и ведомым (Slave).

      ┌─────────────────────────────────────────────────────┐
      │                 КОНТРОЛЛЕР IDE                      │
      ├─────────────────────────────────────────────────────┤
      │  PRIMARY CHANNEL (0x1F0-0x1F7, 0x3F6)               │
      │    ┌──────────────┐        ┌──────────────┐         │
      │    │   MASTER     │        │   SLAVE      │         │
      │    │  (hda)       │        │  (hdb)       │         │
      │    └──────────────┘        └──────────────┘         │
      │                                                     │
      │  SECONDARY CHANNEL (0x170-0x177, 0x376)             │
      │    ┌──────────────┐        ┌──────────────┐         │
      │    │   MASTER     │        │   SLAVE      │         │
      │    │  (hdc)       │        │  (hdd)       │         │
      │    └──────────────┘        └──────────────┘         │
      └─────────────────────────────────────────────────────┘

Регистры

0x00: DATA      - Чтение/запись 16-битных слов
0x01: ERROR     - Код ошибки (чтение)
0x02: NSECT     - Количество секторов
0x03: SECTOR    - LBA0 / номер сектора CHS
0x04: LCYL      - LBA1 / младший цилиндр CHS  
0x05: HCYL      - LBA2 / старший цилиндр CHS
0x06: SELECT    - Выбор устройства (бит 4: 0=master,1=slave)
0x07: STATUS    - Статус операции (чтение)
0x07: COMMAND   - Команда контроллеру (запись)

Биты STATUS (0x07):

BSY(7)=1 - устройство занято
DRDY(6)=1 - устройство готово
DRQ(3)=1 - данные готовы к передаче
ERR(0)=1 - произошла ошибка
// drivers/ide.c
#include "ide.h"
#include "../portio/portio.h"

/* Небольшая задержка: четыре чтения ALTSTATUS (~100ns) */
static inline void io_delay(uint16_t ctrl_port)
{
    (void)inb(ctrl_port + IDE_ALTSTATUS);
    (void)inb(ctrl_port + IDE_ALTSTATUS);
    (void)inb(ctrl_port + IDE_ALTSTATUS);
    (void)inb(ctrl_port + IDE_ALTSTATUS);
}

/* Ждём BSY=0 */
static int wait_bsy_clear(uint16_t base_port, uint16_t ctrl_port, uint32_t timeout)
{
    for (uint32_t i = 0; i < timeout; ++i)
    {
        uint8_t s = inb(base_port + IDE_STATUS);
        if (!(s & IDE_STATUS_BSY))
            return IDE_OK;
        if ((i & 0xFF) == 0)
            io_delay(ctrl_port);
    }
    return IDE_ERR_TIMEOUT;
}

/* Ждём DRQ=1 и BSY=0; если ERR — возвращаем ошибку */
static int wait_drq_or_err(uint16_t base_port, uint16_t ctrl_port, uint32_t timeout)
{
    for (uint32_t i = 0; i < timeout; ++i)
    {
        uint8_t s = inb(base_port + IDE_STATUS);
        if (s & IDE_STATUS_ERR)
            return IDE_ERR_DEVICE;
        if (!(s & IDE_STATUS_BSY) && (s & IDE_STATUS_DRQ))
            return IDE_OK;
        if ((i & 0xFF) == 0)
            io_delay(ctrl_port);
    }
    return IDE_ERR_TIMEOUT;
}

/* Проверить ERR и прочитать регистр ошибок */
static int check_err_and_clear(uint16_t base_port)
{
    uint8_t s = inb(base_port + IDE_STATUS);
    if (s & IDE_STATUS_ERR)
    {
        (void)inb(base_port + IDE_ERROR);
        return IDE_ERR_DEVICE;
    }
    return IDE_OK;
}

/* Выбрать устройство (LBA/CHS) и задержка */
static void select_device_and_delay(uint16_t base, uint16_t ctrl, uint8_t drive, int lba_flag, uint8_t head_high4)
{
    uint8_t value = (lba_flag ? 0xE0 : 0xA0) | ((drive & 1) << 4) | (head_high4 & 0x0F);
    outb(base + IDE_SELECT, value);
    io_delay(ctrl);
}

/* Собрать 4 слова идентификатора в uint64_t (малый порядок слов) */
static uint64_t ident_words_to_u64(const uint16_t ident[256], int w)
{
    uint64_t v = 0;
    v |= (uint64_t)ident[w + 0];
    v |= (uint64_t)ident[w + 1] << 16;
    v |= (uint64_t)ident[w + 2] << 32;
    v |= (uint64_t)ident[w + 3] << 48;
    return v;
}

/* Прочитать один сектор (256 слов) в aligned_words */
static void read_sector_words_to(uint16_t base, uint16_t aligned_words[256])
{
    for (int i = 0; i < 256; ++i)
        aligned_words[i] = inw(base + IDE_DATA);
}

/* Записать один сектор (256 слов) из aligned_words */
static void write_sector_words_from(uint16_t base, const uint16_t aligned_words[256])
{
    for (int i = 0; i < 256; ++i)
        outw(base + IDE_DATA, aligned_words[i]);
}

/* IDENTIFY: заполняет ident_buffer и поля disk */
int ide_identify(ide_disk_t *disk, uint16_t ident_buffer[256])
{
    if (!disk || !ident_buffer)
        return IDE_ERR_INVALID;

    disk->type = IDE_TYPE_NONE;
    disk->supports_lba48 = 0;
    disk->sector_size = 512;
    disk->total_sectors = 0;

    /* Выбор устройства (CHS) */
    select_device_and_delay(disk->base_port, disk->ctrl_port, disk->drive, 0, 0);

    /* Отправка IDENTIFY */
    outb(disk->base_port + IDE_COMMAND, IDE_CMD_IDENTIFY);
    io_delay(disk->ctrl_port);

    uint8_t status = inb(disk->base_port + IDE_STATUS);
    if (status == 0)
        return IDE_ERR_DEVICE;

    /* Если ERR — возможно ATAPI */
    if (status & IDE_STATUS_ERR)
    {
        uint8_t cl = inb(disk->base_port + IDE_LCYL);
        uint8_t ch = inb(disk->base_port + IDE_HCYL);
        if ((cl == 0x14 && ch == 0xEB) || (cl == 0x69 && ch == 0x96))
        {
            outb(disk->base_port + IDE_COMMAND, IDE_CMD_IDENTIFY_PACKET);
            if (wait_bsy_clear(disk->base_port, disk->ctrl_port, IDE_TIMEOUT_LOOPS) != IDE_OK)
                return IDE_ERR_TIMEOUT;
            if (check_err_and_clear(disk->base_port) != IDE_OK)
                return IDE_ERR_DEVICE;
            if (wait_drq_or_err(disk->base_port, disk->ctrl_port, IDE_TIMEOUT_LOOPS) != IDE_OK)
                return IDE_ERR_TIMEOUT;
            for (int i = 0; i < 256; ++i)
                ident_buffer[i] = inw(disk->base_port + IDE_DATA);
            disk->type = IDE_TYPE_ATAPI;
            disk->sector_size = 2048;
            disk->total_sectors = 0;
            return IDE_OK;
        }
        else
        {
            return IDE_ERR_DEVICE;
        }
    }

    if (wait_bsy_clear(disk->base_port, disk->ctrl_port, IDE_TIMEOUT_LOOPS) != IDE_OK)
        return IDE_ERR_TIMEOUT;
    int rc = wait_drq_or_err(disk->base_port, disk->ctrl_port, IDE_TIMEOUT_LOOPS);
    if (rc != IDE_OK)
        return rc;

    for (int i = 0; i < 256; ++i)
        ident_buffer[i] = inw(disk->base_port + IDE_DATA);

    if (ident_buffer[0] == 0)
        return IDE_ERR_DEVICE;

    disk->type = IDE_TYPE_ATA;

    /* LBA28 (words 60-61) */
    uint32_t lba28 = ((uint32_t)ident_buffer[61] << 16) | ident_buffer[60];
    disk->total_sectors = lba28;

    /* Проверка LBA48 (word 83 bit 10) */
    if (ident_buffer[83] & (1u << 10))
    {
        disk->supports_lba48 = 1;
        uint64_t lba48 = ident_words_to_u64(ident_buffer, 100);
        if (lba48 != 0)
            disk->total_sectors = lba48;
    }
    else
    {
        disk->supports_lba48 = 0;
    }

    disk->sector_size = 512;
    return IDE_OK;
}

/* Инициализация структуры и вызов IDENTIFY */
int ide_init(ide_disk_t *disk, ide_channel_t channel, uint8_t drive)
{
    if (!disk || drive > 1)
        return IDE_ERR_INVALID;

    if (channel == IDE_CHANNEL_PRIMARY)
    {
        disk->base_port = IDE_BASE_PRIMARY;
        disk->ctrl_port = IDE_CTRL_PRIMARY;
    }
    else
    {
        disk->base_port = IDE_BASE_SECONDARY;
        disk->ctrl_port = IDE_CTRL_SECONDARY;
    }

    disk->drive = drive & 1;
    disk->channel = channel;
    disk->type = IDE_TYPE_NONE;
    disk->sector_size = 512;
    disk->supports_lba48 = 0;
    disk->total_sectors = 0;

    uint16_t ident[256];
    int rc = ide_identify(disk, ident);
    if (rc != IDE_OK)
        return rc;

    return IDE_OK;
}

/* Настройка регистров для LBA28 */
static void setup_lba28_regs(uint16_t base, uint16_t ctrl, uint32_t lba, uint8_t count, uint8_t drive)
{
    select_device_and_delay(base, ctrl, drive, 1, (uint8_t)((lba >> 24) & 0x0F));
    outb(base + IDE_NSECT, count);
    outb(base + IDE_SECTOR, (uint8_t)(lba & 0xFF));
    outb(base + IDE_LCYL, (uint8_t)((lba >> 8) & 0xFF));
    outb(base + IDE_HCYL, (uint8_t)((lba >> 16) & 0xFF));
}

/* Настройка регистров для LBA48 */
static void setup_lba48_regs(uint16_t base, uint16_t ctrl, uint64_t lba, uint16_t count, uint8_t drive)
{
    select_device_and_delay(base, ctrl, drive, 1, (uint8_t)((lba >> 24) & 0x0F));
    io_delay(ctrl);

    outb(base + IDE_NSECT, (uint8_t)((count >> 8) & 0xFF)); /* SECCOUNT1 */
    outb(base + IDE_SECTOR, (uint8_t)((lba >> 24) & 0xFF)); /* LBA3 */
    outb(base + IDE_LCYL, (uint8_t)((lba >> 32) & 0xFF));   /* LBA4 */
    outb(base + IDE_HCYL, (uint8_t)((lba >> 40) & 0xFF));   /* LBA5 */

    outb(base + IDE_NSECT, (uint8_t)(count & 0xFF));      /* SECCOUNT0 */
    outb(base + IDE_SECTOR, (uint8_t)(lba & 0xFF));       /* LBA0 */
    outb(base + IDE_LCYL, (uint8_t)((lba >> 8) & 0xFF));  /* LBA1 */
    outb(base + IDE_HCYL, (uint8_t)((lba >> 16) & 0xFF)); /* LBA2 */
}

/* Проверка выравнивания по 2 байтам */
static inline int is_aligned_2(const void *ptr)
{
    return (((uintptr_t)ptr) & 1u) == 0;
}

/* Чтение секторов */
int ide_read_sectors(ide_disk_t *disk, uint64_t lba, uint32_t count, void *buffer)
{
    if (!disk || !buffer || count == 0)
        return IDE_ERR_INVALID;

    if (disk->total_sectors && lba > disk->total_sectors - (uint64_t)count)
        return IDE_ERR_INVALID;

    const uint32_t max_per_op = 256;
    uint8_t *user_buf = (uint8_t *)buffer;
    uint32_t remaining = count;

    uint16_t tmp_sector_words[256];

    while (remaining)
    {
        uint32_t chunk = remaining > max_per_op ? max_per_op : remaining;

        for (uint32_t s = 0; s < chunk; ++s)
        {
            uint64_t cur_lba = lba + (count - remaining) + s;

            if (disk->supports_lba48 && (cur_lba > 0x0FFFFFFF))
            {
                setup_lba48_regs(disk->base_port, disk->ctrl_port, cur_lba, 1, disk->drive);
                outb(disk->base_port + IDE_COMMAND, IDE_CMD_READ_SECTORS_EXT);
            }
            else
            {
                uint32_t cur_lba32 = (uint32_t)cur_lba;
                setup_lba28_regs(disk->base_port, disk->ctrl_port, cur_lba32, 1, disk->drive);
                outb(disk->base_port + IDE_COMMAND, IDE_CMD_READ_SECTORS);
            }

            int rc = wait_bsy_clear(disk->base_port, disk->ctrl_port, IDE_TIMEOUT_LOOPS);
            if (rc != IDE_OK)
                return rc;

            rc = wait_drq_or_err(disk->base_port, disk->ctrl_port, IDE_TIMEOUT_LOOPS);
            if (rc != IDE_OK)
                return rc;

            int bytes_per_sector = disk->sector_size;
            int words_per_sector = bytes_per_sector / 2;

            uint8_t *dest = user_buf + ((count - remaining) + s) * (size_t)bytes_per_sector;
            if (is_aligned_2(dest) && bytes_per_sector == 512)
            {
                uint16_t *wptr = (uint16_t *)dest;
                for (int i = 0; i < words_per_sector; ++i)
                    wptr[i] = inw(disk->base_port + IDE_DATA);
            }
            else
            {
                if (bytes_per_sector == 512)
                {
                    read_sector_words_to(disk->base_port, tmp_sector_words);
                    memcpy(dest, tmp_sector_words, 512);
                }
                else
                {
                    for (int i = 0; i < words_per_sector; ++i)
                    {
                        uint16_t w = inw(disk->base_port + IDE_DATA);
                        dest[2 * i + 0] = (uint8_t)(w & 0xFF);
                        dest[2 * i + 1] = (uint8_t)((w >> 8) & 0xFF);
                    }
                }
            }
        }

        user_buf += (size_t)chunk * disk->sector_size;
        remaining -= chunk;
        lba += chunk;
    }

    return IDE_OK;
}

/* Запись секторов */
int ide_write_sectors(ide_disk_t *disk, uint64_t lba, uint32_t count, const void *buffer)
{
    if (!disk || !buffer || count == 0)
        return IDE_ERR_INVALID;

    if (disk->total_sectors && lba > disk->total_sectors - (uint64_t)count)
        return IDE_ERR_INVALID;

    const uint32_t max_per_op = 256;
    const uint8_t *user_buf = (const uint8_t *)buffer;
    uint32_t remaining = count;
    int used_lba48 = 0;

    uint16_t tmp_sector_words[256];

    while (remaining)
    {
        uint32_t chunk = remaining > max_per_op ? max_per_op : remaining;

        for (uint32_t s = 0; s < chunk; ++s)
        {
            uint64_t cur_lba = lba + (count - remaining) + s;

            int use_lba48 = disk->supports_lba48 && (cur_lba > 0x0FFFFFFF);

            if (use_lba48)
            {
                setup_lba48_regs(disk->base_port, disk->ctrl_port, cur_lba, 1, disk->drive);
                outb(disk->base_port + IDE_COMMAND, IDE_CMD_WRITE_SECTORS_EXT);
                used_lba48 = 1;
            }
            else
            {
                uint32_t cur_lba32 = (uint32_t)cur_lba;
                setup_lba28_regs(disk->base_port, disk->ctrl_port, cur_lba32, 1, disk->drive);
                outb(disk->base_port + IDE_COMMAND, IDE_CMD_WRITE_SECTORS);
            }

            int rc = wait_bsy_clear(disk->base_port, disk->ctrl_port, IDE_TIMEOUT_LOOPS);
            if (rc != IDE_OK)
                return rc;

            rc = wait_drq_or_err(disk->base_port, disk->ctrl_port, IDE_TIMEOUT_LOOPS);
            if (rc != IDE_OK)
                return rc;

            int bytes_per_sector = disk->sector_size;
            int words_per_sector = bytes_per_sector / 2;
            const uint8_t *src = user_buf + ((count - remaining) + s) * (size_t)bytes_per_sector;

            if (is_aligned_2(src) && bytes_per_sector == 512)
            {
                const uint16_t *wptr = (const uint16_t *)src;
                for (int i = 0; i < words_per_sector; ++i)
                    outw(disk->base_port + IDE_DATA, wptr[i]);
            }
            else
            {
                if (bytes_per_sector == 512)
                {
                    memcpy(tmp_sector_words, src, 512);
                    for (int i = 0; i < words_per_sector; ++i)
                        outw(disk->base_port + IDE_DATA, tmp_sector_words[i]);
                }
                else
                {
                    for (int i = 0; i < words_per_sector; ++i)
                    {
                        uint16_t w = (uint16_t)src[2 * i] | ((uint16_t)src[2 * i + 1] << 8);
                        outw(disk->base_port + IDE_DATA, w);
                    }
                }
            }

            if (wait_bsy_clear(disk->base_port, disk->ctrl_port, IDE_TIMEOUT_LOOPS) != IDE_OK)
                return IDE_ERR_TIMEOUT;

            if (check_err_and_clear(disk->base_port) != IDE_OK)
                return IDE_ERR_DEVICE;
        }

        user_buf += (size_t)chunk * disk->sector_size;
        remaining -= chunk;
        lba += chunk;
    }

    /* Flush cache */
    if (used_lba48)
        outb(disk->base_port + IDE_COMMAND, IDE_CMD_CACHE_FLUSH_EXT);
    else
        outb(disk->base_port + IDE_COMMAND, IDE_CMD_CACHE_FLUSH);

    if (wait_bsy_clear(disk->base_port, disk->ctrl_port, IDE_TIMEOUT_LOOPS) != IDE_OK)
        return IDE_ERR_TIMEOUT;
    if (check_err_and_clear(disk->base_port) != IDE_OK)
        return IDE_ERR_DEVICE;

    return IDE_OK;
}

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


Реализация драйвера PCI

PCI драйвер - это программное обеспечение, которое обеспечивает взаимодействие между операционной системой и устройствами, подключенными к шине PCI (Peripheral Component Interconnect).

Примеры устройств, требующих PCI драйверов:

  • Видеокарты (GPU)

  • Сетевые карты (NIC)

  • Звуковые карты

  • USB контроллеры

  • RAID контроллеры

К��к работает доступ к конфигурации?

В x86 системах используются два портовых адреса для доступа к PCI конфигурационному пространству:

  • Порт 0xCF8 - адресный порт. В этот порт записывается адрес того устройства и регистра, который нужно прочитать или записать.

  • Порт 0xCFC - портовый адрес данных. Из этого порта читаются или в этот порт записываются данные.

Принцип простой: сначала указываем адрес (outl(0xCF8, адрес)), потом читаем/пишем данные (inl(0xCFC) или outl(0xCFC, данные)).

Формат адреса конфигурации

Адрес - это 32-битное число, в котором закодирована информация о том, какое устройство и регистр нам нужны:

Бит 31        → 1 (включить доступ)
Биты 23-16    → номер шины (Bus)
Биты 15-11    → номер слота/устройства (Device)
Биты 10-8     → номер функции (Function)
Биты 7-2      → смещение в конфиг. пространстве (Offset)

Функция pci_conf_addr() формирует такой адрес из компонентов.

Основной алгоритм сканирования

Драйвер работает по схеме:

  1. Инициализация → Программа вызывает pci_init(). Она проверяет, мультифункциональное ли устройство 0:0:0 (первое устройство на первой шине). Если да, то в системе несколько контроллеров, и нужно сканировать шины 0-7. Если нет, то сканируется только шина 0.

  2. Сканирование шиныpci_scan_bus() перебирает все 32 возможных устройства на шине (номера 0-31).

  3. Проверка устройстваpci_check_device() проверяет, есть ли на этом слоте устройство. Если есть, проверяет, поддерживает ли оно несколько функций. Устройство может иметь до 8 функций (0-7). Обычно одна функция, но некоторые сложные устройства имеют несколько.

  4. Проверка функцииpci_check_function() читает параметры функции из конфигурационного пространства:

    1. Vendor ID (смещение 0x00) - производитель

    2. Device ID (смещение 0x02) - идентификатор устройства

    3. Class Code (смещение 0x08) - класс устройства (видеокарта, сетевая карта и т.д.)

    4. Subclass (смещение 0x08) - подкласс

    5. Header Type (смещение 0x0C) - тип заголовка конфигурации

  5. Зондирование BARpci_probe_bars() определяет адреса и размеры памяти/портов, которые использует устройство. BAR расшифровывается как Base Address Register - регистры, где хранятся адреса ресурсов устройства. Их может быть до 6 на устройство.

  6. Регистрация → Найденное устройство добавляется в глобальный массив g_pci_devices[].

  7. Рекурсия → Если найденное устройство является PCI-to-PCI bridge (класс 0x06, подкласс 0x04), это значит, что за ним стоит ещё одна PCI шина (secondary bus). Драйвер читает номер этой шины из смещения 0x19 и рекурсивно её сканирует.

Код драйвера:

// drivers/pci.c
#include "pci.h"
#include "../portio/portio.h"
#include "../malloc/malloc.h"
#include "../graphics/exception_handler/kprint.h"
#include <string.h>
#include <stdint.h>

#define CONFIG_ADDRESS_PORT 0xCF8
#define CONFIG_DATA_PORT 0xCFC

static pci_device_t *g_pci_devices = NULL;
static int g_pci_dev_count = 0;
static int g_pci_dev_capacity = 0;

static void ensure_capacity(void)
{
    if (!g_pci_devices)
    {
        g_pci_dev_capacity = 32;
        g_pci_devices = (pci_device_t *)malloc(sizeof(pci_device_t) * g_pci_dev_capacity);
        g_pci_dev_count = 0;
    }
    else if (g_pci_dev_count >= g_pci_dev_capacity)
    {
        int nc = g_pci_dev_capacity * 2;
        g_pci_devices = (pci_device_t *)realloc(g_pci_devices, sizeof(pci_device_t) * nc);
        g_pci_dev_capacity = nc;
    }
}

/* Формирование 32-битного адреса конфигурации (Mechanism #1) */
static uint32_t pci_conf_addr(uint8_t bus, uint8_t device, uint8_t function, uint8_t offset)
{
    return (uint32_t)((1u << 31) |
                      ((uint32_t)bus << 16) |
                      ((uint32_t)device << 11) |
                      ((uint32_t)function << 8) |
                      (offset & 0xFC));
}

/* Чтение / запись конфигурационного пространства */
static uint32_t pci_config_read32(uint8_t bus, uint8_t device, uint8_t function, uint8_t offset)
{
    uint32_t addr = pci_conf_addr(bus, device, function, offset);
    outl(CONFIG_ADDRESS_PORT, addr);
    return inl(CONFIG_DATA_PORT);
}

static void pci_config_write32(uint8_t bus, uint8_t device, uint8_t function, uint8_t offset, uint32_t value)
{
    uint32_t addr = pci_conf_addr(bus, device, function, offset);
    outl(CONFIG_ADDRESS_PORT, addr);
    outl(CONFIG_DATA_PORT, value);
}

static uint16_t pci_config_read16(uint8_t bus, uint8_t device, uint8_t function, uint8_t offset)
{
    uint32_t v = pci_config_read32(bus, device, function, offset & 0xFC);
    int shift = (offset & 2) * 8;
    return (uint16_t)((v >> shift) & 0xFFFF);
}

static uint8_t pci_config_read8(uint8_t bus, uint8_t device, uint8_t function, uint8_t offset)
{
    uint32_t v = pci_config_read32(bus, device, function, offset & 0xFC);
    int shift = (offset & 3) * 8;
    return (uint8_t)((v >> shift) & 0xFF);
}

/* Добавление устройства в список */
static void pci_register_device(const pci_device_t *dev)
{
    ensure_capacity();
    g_pci_devices[g_pci_dev_count++] = *dev;
}

/* Пробирование BAR: возвращает base и size (size==0 => не выделен) */
static void pci_probe_bars(pci_device_t *dev)
{
    /* Сохраним Command Register и временно выключим I/O и Memory responses */
    uint16_t cmd = pci_config_read16(dev->bus, dev->device, dev->function, 0x04);
    uint16_t cmd_saved = cmd;
    cmd &= ~(1u << 0); /* I/O Space */
    cmd &= ~(1u << 1); /* Memory Space */
    pci_config_write32(dev->bus, dev->device, dev->function, 0x04, (uint32_t)cmd);

    for (int i = 0; i < 6; ++i)
    {
        dev->bar_addr[i] = 0;
        dev->bar_size[i] = 0;
        dev->bar_is_io[i] = 0;

        uint8_t off = 0x10 + i * 4;
        uint32_t orig = pci_config_read32(dev->bus, dev->device, dev->function, off);
        if (orig == 0)
        {
            continue;
        }

        /* Определить тип */
        if (orig & 0x1)
        {
            /* I/O Space */
            dev->bar_is_io[i] = 1;
            pci_config_write32(dev->bus, dev->device, dev->function, off, 0xFFFFFFFF);
            uint32_t val = pci_config_read32(dev->bus, dev->device, dev->function, off);
            uint32_t mask = val & 0xFFFFFFFC;
            uint32_t size = (~mask) + 1;
            if (size == 0)
            {
                size = 0;
            }
            dev->bar_size[i] = (uint64_t)size;
            dev->bar_addr[i] = (uint64_t)(orig & 0xFFFFFFFC);
            pci_config_write32(dev->bus, dev->device, dev->function, off, orig);
        }
        else
        {
            /* Memory Space */
            uint32_t type = (orig >> 1) & 0x3;
            if (type == 0x2)
            {
                /* 64-bit BAR: consumes two entries */
                uint32_t orig_hi = pci_config_read32(dev->bus, dev->device, dev->function, off + 4);
                uint64_t full_orig = ((uint64_t)orig_hi << 32) | (orig & 0xFFFFFFF0);

                /* write all ones to both */
                pci_config_write32(dev->bus, dev->device, dev->function, off, 0xFFFFFFFF);
                pci_config_write32(dev->bus, dev->device, dev->function, off + 4, 0xFFFFFFFF);

                uint32_t val_lo = pci_config_read32(dev->bus, dev->device, dev->function, off) & 0xFFFFFFF0;
                uint32_t val_hi = pci_config_read32(dev->bus, dev->device, dev->function, off + 4);

                uint64_t mask = ((uint64_t)val_hi << 32) | val_lo;
                uint64_t size = (~mask) + 1;
                dev->bar_size[i] = size;
                dev->bar_addr[i] = full_orig;

                /* restore original low/high */
                pci_config_write32(dev->bus, dev->device, dev->function, off, (uint32_t)(full_orig & 0xFFFFFFFF));
                pci_config_write32(dev->bus, dev->device, dev->function, off + 4, (uint32_t)(full_orig >> 32));

                /* отметим следующий BAR как пропущенный (занят 64-bit) */
                ++i;
            }
            else
            {
                /* 32-bit memory BAR */
                pci_config_write32(dev->bus, dev->device, dev->function, off, 0xFFFFFFFF);
                uint32_t val = pci_config_read32(dev->bus, dev->device, dev->function, off);
                uint32_t mask = val & 0xFFFFFFF0;
                uint32_t size = (~mask) + 1;
                dev->bar_size[i] = (uint64_t)size;
                dev->bar_addr[i] = (uint64_t)(orig & 0xFFFFFFF0);
                /* restore */
                pci_config_write32(dev->bus, dev->device, dev->function, off, orig);
            }
        }
    }

    /* восстановим Command Register */
    pci_config_write32(dev->bus, dev->device, dev->function, 0x04, (uint32_t)cmd_saved);
}

/* Обрабатываем одну функцию устройства */
static void pci_check_function(uint8_t bus, uint8_t device, uint8_t function)
{
    uint16_t vendor = pci_config_read16(bus, device, function, 0x00);
    if (vendor == 0xFFFF)
        return;

    pci_device_t dev;
    memset(&dev, 0, sizeof(dev));
    dev.bus = bus;
    dev.device = device;
    dev.function = function;
    dev.vendor_id = vendor;
    dev.device_id = pci_config_read16(bus, device, function, 0x02);

    uint32_t reg = pci_config_read32(bus, device, function, 0x08);
    dev.class_code = (reg >> 24) & 0xFF;
    dev.subclass = (reg >> 16) & 0xFF;
    dev.prog_if = (reg >> 8) & 0xFF;

    dev.header_type = pci_config_read8(bus, device, function, 0x0C);

    /* Probe BARs */
    pci_probe_bars(&dev);

    /* Зарегистрируем устройство */
    pci_register_device(&dev);

    /* Если это PCI-to-PCI bridge (base class 6, subclass 4) - рекурсивно сканируем secondary bus */
    if (dev.class_code == 0x06 && dev.subclass == 0x04)
    {
        uint8_t secondary = pci_config_read8(bus, device, function, 0x19);
        if (secondary != 0)
        {
            /* рекурсивный проход */
            /* простая защита от глубокой рекурсии: если secondary == bus - пропускаем */
            if (secondary != bus)
            {
                extern void pci_scan_bus(uint8_t bus);
                pci_scan_bus(secondary);
            }
        }
    }
}

/* Обрабатываем устройство (включая мульти-Функции) */
static void pci_check_device(uint8_t bus, uint8_t device)
{
    uint16_t vendor = pci_config_read16(bus, device, 0, 0x00);
    if (vendor == 0xFFFF)
        return;

    /* Проверим header type для мультифункции */
    uint8_t header = pci_config_read8(bus, device, 0, 0x0C);
    pci_check_function(bus, device, 0);
    if (header & 0x80)
    {
        for (uint8_t f = 1; f < 8; ++f)
        {
            if (pci_config_read16(bus, device, f, 0x00) != 0xFFFF)
            {
                pci_check_function(bus, device, f);
            }
        }
    }
}

/* Сканирование одной шины: 32 устройства */
void pci_scan_bus(uint8_t bus)
{
    for (uint8_t dev = 0; dev < 32; ++dev)
    {
        pci_check_device(bus, dev);
    }
}

void pci_init(void)
{
    /* инициализация контейнера */
    if (!g_pci_devices)
    {
        ensure_capacity();
    }

    /* Проверка: если устройство 0:0:0 мультифункциональное, возможно несколько host controllers */
    uint8_t header = pci_config_read8(0, 0, 0, 0x0C);
    if ((header & 0x80) == 0)
    {
        /* single host controller */
        pci_scan_bus(0);
    }
    else
    {
        /* multi host controllers: функция i управляет шинами i */
        for (uint8_t f = 0; f < 8; ++f)
        {
            if (pci_config_read16(0, 0, f, 0x00) == 0xFFFF)
                break;
            pci_scan_bus(f);
        }
    }
}

int pci_get_device_count(void)
{
    return g_pci_dev_count;
}

pci_device_t *pci_get_device(int idx)
{
    if (idx < 0 || idx >= g_pci_dev_count)
        return NULL;
    return &g_pci_devices[idx];
}

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


Реализация переключателя Kernel mode / User mode

Переключатель Kernel Mode / User Mode - это аппаратный механизм процессора, позволяющий операционной системе переводить процессор между привилегированным режимом ядра и непривилегированным пользовательским режимом для защиты системных ресурсов от несанкционированного доступа приложений.

Kernel Mode - это привилегированный режим работы процессора, в котором исполняется код операционной системы (ядро). В этом режиме:

  • Полный доступ ко всем ресурсам компьютера (память, диски, порты)

  • Возможность выполнять привилегированные инструкции процессора

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

  • Ошибка в коде ядра может привести к краху всей системы

User Mode - это непривилегированный режим, в котором исполняются приложения пользователей. В этом режиме:

  • Нет прямого доступа к системным ресурсам

  • Нельзя выполнять привилегированные инструкции

  • Изолированы друг от друга - ошибка в приложении не влияет на систему

  • Вызовы ядра выполняются через системные вызовы (syscalls)

Kernel Mode (DPL=0, Code ring 0x08) → User Mode (DPL=3, Code ring 0x1B) переключаются через GDT дескрипторы и стек в памяти.

Создание User-задачи

utask_create() вызвана
  ↓
malloc(task_t) + malloc(kstack)
  ↓
prepare_initial_stack(..., user_mode=1)
  ↓
На стеке ядра готовится фрейм:
┌─────────────────┐
│  SS    (USER)   │  ← 0x23 (Ring 3)
│  RSP (user_mem) │
│  RFLAGS (IF=1)  │
│  CS    (USER)   │  ← 0x1B (Ring 3)
│  RIP (entry)    │
│  ... regs ...   │
│  int_no = 32    │
└─────────────────┘
  ↓
add_to_ring() → задача в очередь READY

Что происходит:

  • Когда позже выполнится IRET, CPU видит Ring 3 в CS/SS

  • CPU автоматически переключается в user mode (более низкий привилегия)

  • Стек переходит с kernel stack на user stack

Переход User → Kernel (при прерывании)

User-задача выполняется (ring 3)
  ↓
Прерывание: Timer / Syscall / Exception
  ↓
CPU (автоматически!) проверяет GDT текущего CS:
  "Прерывание в ring 3 → нужно switch в ring 0"
  ↓
CPU берёт RSP0 из TSS → это kernel stack
  ↓
CPU сохраняет на kernel stack:
  ┌──────────────┐
  │ SS (0x23)    │ ← старый user stack selector
  │ RSP (user)   │ ← старый user stack pointer
  │ RFLAGS       │
  │ CS (0x1B)    │ ← откуда пришли
  │ RIP (адрес)  │
  │ [ERR_CODE]   │ ← опционально
  └──────────────┘
  ↓
Переходим в kernel mode (ring 0, SS=0x10, RSP=kernel_stack)
  ↓
ISR обработчик выполняется в kernel

Возврат Kernel → User (IRET)

Syscall / Exception обработана в kernel
  ↓
schedule_from_isr() выбрал user задачу
  ↓
ISR выполняет: IRET (pop из kernel stack)
  ↓
IRET pop:
  ┌────────────────┐
  │ RIP  (entry)   │ → register
  │ CS   (0x1B)    │ → register  ← RING 3!
  │ RFLAGS         │ → register
  │ RSP  (user)    │ → register
  │ SS   (0x23)    │ → register
  └────────────────┘
  ↓
CPU видит CS=0x1B (ring 3)
  ↓
CPU переключает режим: ring 0 → ring 3
  ↓
CPU переключает stack: kernel → user_mem
  ↓
Выполняется user-код с ring 3 привилегиями

Ключевым компонентом при реализации пользовательского режима (User Mode) является таблица дескрипторов.

; kernel.asm
; -----------------------------------------------------------------------
; GDT с поддержкой Ring 3 и TSS
; -----------------------------------------------------------------------
align 8
gdt:
    dq 0x0000000000000000     ; Null descriptor
    dq 0x00AF9A000000FFFF     ; 0x08: Kernel Code (L=1, DPL=0)
    dq 0x00AF92000000FFFF     ; 0x10: Kernel Data (L=1, DPL=0)
    dq 0x00AFFA000000FFFF     ; 0x18: User Code (L=1, DPL=3)
    dq 0x00AFF2000000FFFF     ; 0x20: User Data (L=1, DPL=3)
    ; TSS descriptor (64-bit требует 2 записи)
    dq 0x0000000000000000     ; 0x28: TSS lower 8 bytes (будет заполнен далее)
    dq 0x0000000000000000     ; 0x30: TSS upper 8 bytes
gdt_end:

gdt_desc:
    dw gdt_end - gdt - 1
    dq gdt

Фрагменты кода реализации поддержки переключения режимов.

// multitask/multitask.c
#define USER_CS ((uint64_t)0x18 | 3) /* 0x1B */
#define USER_SS ((uint64_t)0x20 | 3) /* 0x23 */

static uint64_t *prepare_initial_stack(void (*entry)(void),
                                       void *kstack_top,
                                       void *user_stack_top,
                                       int argc,
                                       uintptr_t argv_ptr,
                                       int user_mode)
{
    const int FRAME_QWORDS = 22;
    uint64_t *sp = (uint64_t *)kstack_top;
    sp = (uint64_t *)(((uintptr_t)sp) & ~0xFULL); /* align down 16 */
    sp -= FRAME_QWORDS;

    sp[0] = 32;                  /* int_no (dummy) */
    sp[1] = 0;                   /* err_code */
    sp[2] = 0;                   /* r15 */
    sp[3] = 0;                   /* r14 */
    sp[4] = 0;                   /* r13 */
    sp[5] = 0;                   /* r12 */
    sp[6] = 0;                   /* r11 */
    sp[7] = 0;                   /* r10 */
    sp[8] = 0;                   /* r9  */
    sp[9] = 0;                   /* r8  */
    sp[10] = (uint64_t)argc;     /* rdi */
    sp[11] = (uint64_t)argv_ptr; /* rsi */
    sp[12] = 0;                  /* rbp */
    sp[13] = 0;                  /* rbx */
    sp[14] = 0;                  /* rdx */
    sp[15] = 0;                  /* rcx */
    sp[16] = 0;                  /* rax */
    sp[17] = (uint64_t)entry;    /* rip */
    sp[19] = 0x202;              /* rflags (IF=1) */

    if (user_mode)
    {
        sp[18] = USER_CS;
        sp[20] = (uint64_t)user_stack_top;
        sp[21] = USER_SS;
    }
    else
    {
        sp[18] = 0x08;
        sp[20] = (uint64_t)kstack_top;
        sp[21] = 0x10;
    }

    return sp;
}
// tss/tss.c
extern tss_t tss_buffer;
extern uint64_t gdt[];
extern uint64_t stack64_top;

static tss_t *tss = NULL;

void tss_init(void)
{
    tss = &tss_buffer;

    /* Очищаем TSS */
    for (int i = 0; i < sizeof(tss_t); i++)
        ((uint8_t *)tss)[i] = 0;

    /* Инициализируем rsp0 с начального kernel stack */
    tss->rsp0 = (uint64_t)&stack64_top;

    tss->iopb_offset = sizeof(tss_t);

    /* Формируем 64-bit TSS descriptor согласно Intel manual */
    uint64_t tss_addr = (uint64_t)tss;
    uint16_t tss_limit = sizeof(tss_t) - 1;

    /* Lower 8 bytes: limit(16) | base_low(24) | access(8) | flags(4) | base_mid(16) */
    uint64_t lower = 0;
    lower |= ((uint64_t)tss_limit & 0xFFFF);        /* bits 0-15: limit */
    lower |= (((tss_addr) & 0xFFFFFF) << 16);       /* bits 16-39: base_low */
    lower |= (0x89ULL << 40);                       /* bits 40-47: access (P=1, DPL=0, Type=9) */
    lower |= ((((tss_addr) >> 24) & 0xFFFF) << 48); /* bits 48-63: base_mid */

    /* Upper 8 bytes: base_high(32) | reserved(32) */
    uint64_t upper = (tss_addr >> 40) & 0xFFFFFFFF;

    gdt[5] = lower;
    gdt[6] = upper;

    /* Загружаем TSS */
    asm volatile("mov $0x28, %%ax; ltr %%ax" ::: "ax");
}

void tss_update_rsp0(uint64_t rsp0)
{
    if (tss)
        tss->rsp0 = rsp0;
}

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


Итог:

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

  1. Запуск в Long Mode для работы в современном 64-битном пространстве.

  2. Поддержку Multiboot для стандартной загрузки и получения информации о системе.

  3. Графический режим, обеспечив базовый вывод на экран.

  4. Драйвер ATA (IDE) для доступа к дисковым накопителям.

  5. Драйвер PCI для обнаружения и конфигурации оборудования.

  6. Переключатель привилегий (Kernel/User Mode), заложив основу безопасности и изоляции.

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

Полный исходный код проекта, а также инструкция по сборке и запуску доступны здесь: GitHub


Спасибо за прочтение статьи!

Надеюсь, она была интересна для вас, и вы узнали что-то новое.

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